返回

Java 22 Spring Boot JWT 认证与授权方案详解

java

Java 22 环境下的 JWT 认证与授权方案

Java 22 的发布带来了诸多改进,但在 Web 应用安全方面,仍然需要依赖成熟的认证与授权机制,其中 JSON Web Token (JWT) 凭借其无状态和易扩展的特性成为主流选择。本文将探讨在 Java 22 环境下,如何在 Spring Boot 项目中,利用分层架构(实体、服务、仓库、DTO、映射器、控制器、配置)有效地实现 JWT 认证和授权。

JWT 认证流程概述

核心概念是:客户端使用用户名和密码进行登录,服务端验证身份后签发 JWT。这个 JWT 将在后续的请求中被客户端携带,服务器解析 JWT 并据此进行用户认证和授权。 简单来说,JWT 本身包含已验证用户的必要信息,服务端不再需要维持会话状态,大幅度减少了资源占用,更易于构建高可用和可扩展的系统。

基于 Spring Security 的 JWT 实现

Spring Security 提供了强大的认证与授权框架,与 JWT 的整合也是常见的方案。下面演示详细实现步骤,及其代码示例。

步骤 1:添加依赖

首先在 pom.xml 文件中引入必要的依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-api</artifactId>
	<version>0.11.5</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-impl</artifactId>
	<version>0.11.5</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-jackson</artifactId>
	<version>0.11.5</version>
	<scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

这里加入了 spring-boot-starter-security, spring-boot-starter-web, 以及 jjwt (json web token 的 java 实现)相关依赖。 lombok 提供 setter/getter 功能, 可以帮助减少重复代码。

步骤 2:用户实体类(Entity)
创建一个 User 实体类,表示数据库中存储的用户信息。

import lombok.Data;

import jakarta.persistence.*;

@Entity
@Table(name = "users")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String role;
    //其他字段,比如email等
}

此处的username,password,role在认证过程中会被使用。 务必根据项目实际情况进行字段调整。

步骤 3:数据访问层 (Repository)

UserRepository 接口负责与数据库进行交互。

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import your.package.User;

import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

   Optional<User> findByUsername(String username);

}

findByUsername 方法将会用于验证用户登录。

步骤 4: 数据传输对象(DTO)
定义用于传递登录信息的 LoginRequest DTO:

import lombok.Data;

@Data
public class LoginRequest {

    private String username;
    private String password;
}

步骤 5:用户服务层(Service)

UserService 处理用户身份验证并生成 JWT。

import your.package.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Collections;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

@Service
public class UserService  implements UserDetailsService{
  private final UserRepository userRepository;
  private final BCryptPasswordEncoder encoder;


	public UserService(UserRepository userRepository, BCryptPasswordEncoder encoder){
		this.userRepository = userRepository;
		this.encoder = encoder;

	}


	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
		 your.package.User user= userRepository.findByUsername(username).orElseThrow(()->new UsernameNotFoundException("username not found"));

		 return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),Collections.emptyList());

	}


  public your.package.User register(your.package.User user) {

        user.setPassword(encoder.encode(user.getPassword()));
        return userRepository.save(user);

    }
}

loadUserByUsername 是Spring Security 进行用户查找和登录的地方。register 用于用户注册时加密密码,确保用户密码的安全性。

步骤 6:JWT 生成器

创建 JWT 生成器工具类 JwtTokenUtil,负责生成和解析 JWT。

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;


@Component
public class JwtTokenUtil {


  @Value("${jwt.secret}")
  private String SECRET;

  @Value("${jwt.expiration}")
  private long JWT_EXPIRATION;


    public String generateToken(String userName){

        return generateToken(new HashMap<>(),userName);

    }


    public String generateToken(Map<String,Object> claims,String userName){


      return  Jwts.builder()
        .setClaims(claims)
         .setSubject(userName)
          .setIssuedAt(new Date(System.currentTimeMillis()))
          .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION ))
            .signWith(getSignInKey(),SignatureAlgorithm.HS256)
          .compact();

    }



    private Key getSignInKey(){
        byte[] keyBytes = Decoders.BASE64.decode(SECRET);
        return Keys.hmacShaKeyFor(keyBytes);
    }




     public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
     }

       public  Date extractExpiration(String token){

        return  extractClaim(token,Claims::getExpiration);
     }



     private <T> T extractClaim(String token , Function<Claims, T> claimResolver){

        final  Claims claims=extractAllClaims(token);

        return claimResolver.apply(claims);

     }



     private Claims extractAllClaims(String token){
         return  Jwts.parserBuilder()
                    .setSigningKey(getSignInKey())
                      .build()
                  .parseClaimsJws(token)
                  .getBody();

     }




      public Boolean isTokenExpired(String token){
        return extractExpiration(token).before(new Date());
      }




       public Boolean isTokenValid(String token,UserDetails userDetails){
           final String userName=extractUsername(token);
           return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token) );

        }



}

此类负责 JWT 的生成,校验和解析。 需要注意,SECRET 必须存储在一个安全的地方,并不要泄露给任何未授权的用户。

步骤 7:安全配置类(Configuration)
创建一个 SecurityConfig 类配置 Spring Security。


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import your.package.filters.JwtAuthFilter;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {


	@Autowired
	private JwtAuthFilter authFilter;


	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
        return http.csrf(csrf->csrf.disable())

		   .authorizeHttpRequests(auth-> auth.requestMatchers( "/register","/authenticate")
                .permitAll().anyRequest().authenticated() )

         .sessionManagement(sess->sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
			   .authenticationProvider(authenticationProvider())

	     .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)


            .build();


    }

	@Bean
  	 public AuthenticationProvider authenticationProvider() {
		 DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider();
			authenticationProvider.setUserDetailsService(userDetailsService());
		   authenticationProvider.setPasswordEncoder(passwordEncoder());
		 return authenticationProvider;


	 }




	@Bean
	  public BCryptPasswordEncoder passwordEncoder(){
	        return  new BCryptPasswordEncoder();
	    }


	   @Bean
	    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{
	    	return config.getAuthenticationManager();
	   }




	    @Bean
	  public UserDetailsService userDetailsService(){
		   return username ->
        {  return userDetailsService()
                .loadUserByUsername(username); };

	}



}

在此类中,禁用了 csrf, 所有匹配 /register 或者 /authenticate 的请求均无需认证,其他请求均需通过认证。 用户密码使用 BCryptPasswordEncoder 加密存储。添加了JwtAuthFilter这个filter.

步骤 8: JWT 过滤器(Filter)

创建 JwtAuthFilter 过滤器类,解析和校验 JWT 。


import your.package.JwtTokenUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;

import your.package.UserService;


@Component
public class JwtAuthFilter extends OncePerRequestFilter {

	@Autowired
    private JwtTokenUtil jwtTokenUtil;


    @Autowired
    private UserService userDetailsService;



    @Override
	 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    	  String authHeader= request.getHeader("Authorization");

	      String token =null;
	     String userName=null;
         if(authHeader != null && authHeader.startsWith("Bearer "))
		   { token=authHeader.substring(7);

		    userName= jwtTokenUtil.extractUsername(token);


          }



		 if(userName !=null && SecurityContextHolder.getContext().getAuthentication() ==null ){

		 UserDetails userDetails=userDetailsService.loadUserByUsername(userName);

           if(jwtTokenUtil.isTokenValid(token,userDetails)){
              UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());

		      authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
			  SecurityContextHolder.getContext().setAuthentication(authToken);
			  }


        }
	     filterChain.doFilter(request,response);

    }



}

此 Filter 提取请求头部的 Authorization 中的 JWT,解析用户名,检查 JWT 是否有效。如果有效, 则 Spring Security 的 context 中更新用户信息, 后续请求则无需再次认证。

步骤 9:控制器层 (Controller)
定义控制器,实现登录认证。

import your.package.UserService;
import your.package.LoginRequest;
import your.package.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import your.package.User;

@RestController
public class AuthController {
  @Autowired
    private  UserService userService;
	@Autowired
	  private AuthenticationManager authenticationManager;

	@Autowired
  private JwtTokenUtil jwtTokenUtil;



    @PostMapping("/register")
    public  ResponseEntity<?> addNewUser(@RequestBody User user){


         User savedUser = userService.register(user);


            return new ResponseEntity<>(savedUser,HttpStatus.CREATED);
      }


		@PostMapping("/authenticate")

     public  String  authenticateAndGetToken(@RequestBody LoginRequest authRequest){

		    Authentication authentication = authenticationManager
						 .authenticate(new UsernamePasswordAuthenticationToken(authRequest.getUsername(),authRequest.getPassword()));
					    if(authentication.isAuthenticated()){
				      return  jwtTokenUtil.generateToken(authRequest.getUsername());
		} else {
				       throw new RuntimeException("invalid access");
				    }



     }




	    @GetMapping("/welcome")
	     public  String  welcome()
			{
				  return "welcome this is secured route ";


			}
}

/register 注册新用户, /authenticate 进行用户登录并颁发 JWT, /welcome 是一个测试认证结果的 endpoint.

安全提示

  • 密钥管理: JWT 签名密钥的保护至关重要。不要将密钥硬编码在代码中。使用环境变量或专门的密钥管理服务。
  • 令牌过期时间: 设置合理的令牌过期时间。 过长的过期时间可能导致令牌被窃取后,在一段时间内都能被滥用。 过短的过期时间则可能导致频繁登录, 用户体验下降。
  • 传输加密: 使用 HTTPS 协议,防止令牌被窃听。
  • 刷新令牌: 可以考虑引入刷新令牌机制,减少用户频繁登录的次数,但也要注意刷新令牌的安全管理。

总而言之, 基于 Spring Security 和 JWT 进行权限认证是可靠的方案。 以上代码提供了一个较为全面的实现。请务必在实际项目中结合业务需求进行定制和安全增强。