Java 22 Spring Boot JWT 认证与授权方案详解
2024-12-23 16:18:21
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 进行权限认证是可靠的方案。 以上代码提供了一个较为全面的实现。请务必在实际项目中结合业务需求进行定制和安全增强。