Spring Boot 3.x + Spring Security 对 JWT 进行鉴权

1. 引言

JSON Web Token(JWT)作为一种轻量级的认证机制,在现代Web应用中得到了广泛应用。它具有无状态、可扩展、跨平台等优点,特别适合前后端分离架构和微服务场景。

本文将详细介绍如何在Spring Boot 3.x项目中,使用Spring Security对JWT进行鉴权,包括JWT的生成、验证、刷新等核心功能的实现。

2. 技术栈

  • Spring Boot 3.5.x
  • Spring Security 6.5.x
  • Nimbus JOSE + JWT 10.x

3. JWT原理与结构

3.1 JWT的组成

JWT由三部分组成,用点(.)分隔:

  • Header:包含令牌类型和签名算法
  • Payload:包含声明(claims),如用户ID、角色等
  • Signature:使用密钥对前两部分进行签名
Header.Payload.Signature

3.2 JWT的工作流程

┌──────────────┐                        ┌──────────────┐
│  客户端      │  1. 登录请求            │  认证服务器   │
│              │ ─────────────────────> │              │
│              │                        │              │
└──────────────┘                        └──────────────┘
                                            │
                                            │  2. 验证用户凭证
                                            │
                                            ▼
┌──────────────┐                        ┌──────────────┐
│  客户端      │  3. 返回JWT Token       │  认证服务器   │
│              │ <───────────────────── │              │
│              │                        │              │
└──────────────┘                        └──────────────┘
        │
        │  4. 携带Token请求受保护资源
        ▼
┌──────────────┐                        ┌──────────────┐
│  客户端      │  5. 请求携带JWT Token   │  资源服务器   │
│              │ ─────────────────────> │              │
│              │                        │              │
└──────────────┘                        └──────────────┘
                                            │
                                            │  6. 验证Token
                                            │
                                            ▼
┌──────────────┐                        ┌──────────────┐
│  客户端      │  7. 返回资源            │  资源服务器   │
│              │ <───────────────────── │              │
│              │                        │              │
└──────────────┘                        └──────────────┘

4. 项目初始化

4.1 创建Spring Boot项目

使用Spring Boot Initializr创建项目,添加以下依赖:

  • Spring Web
  • Spring Security
  • Lombok

4.2 添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.8</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>xxx-security-authentication-client</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>${project.artifactId}</name>

    <properties>
        <java.version>21</java.version>
        <nimbus-jose-jwt.version>10.0.2</nimbus-jose-jwt.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>${nimbus-jose-jwt.version}</version>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

5. 核心功能实现

5.1 配置文件

application.yml中添加配置:

server:
  port: 8080
  
spring:
  application:
    name: xxx-security-authentication-client

logging:
  level:
    org.springframework.security: DEBUG

5.2 JWT工具类

创建JwtUtil.java

public class JwtUtil {

    private final JWSVerifier verifier;
    private final PublicKey publicKey;

    public JwtUtil(PublicKey publicKey) {
        this.verifier = createVerifier(publicKey);
        this.publicKey = publicKey;
    }

    private JWSVerifier createVerifier(PublicKey publicKey) {
        return new RSASSAVerifier((RSAPublicKey) publicKey);
    }

    public boolean validateToken(String token) throws JOSEException, ParseException {
        try {
            SignedJWT signedJWT = SignedJWT.parse(token);
            boolean verified = signedJWT.verify(verifier);
            boolean notExpired = signedJWT.getJWTClaimsSet().getExpirationTime().after(new Date());
            return verified && notExpired;
        } catch (JOSEException | ParseException e) {
            throw e;
        }
    }

    public JWTClaimsSet getClaims(String token) throws ParseException {
        SignedJWT signedJWT = SignedJWT.parse(token);
        return signedJWT.getJWTClaimsSet();
    }

    public String getSubject(String token) throws ParseException {
        return getClaims(token).getSubject();
    }

    public List<String> getAuthorities(String token) throws ParseException {
        return (List<String>) getClaims(token).getClaim("authorities");
    }

    public String getUsername(String token) throws ParseException {
        return getClaims(token).getStringClaim("username");
    }

    public String getUserId(String token) throws ParseException {
        return getClaims(token).getStringClaim("userId");
    }
}

5.3 会话用户实体类

创建AuthUser.java

@Data
public class AuthUser implements UserDetails {
    private String userId;
    private String username;
    private String password;
    private Collection<GrantedAuthority> authorities = new HashSet<>();
    private boolean enabled = true;
    private boolean accountNonExpired = true;
    private boolean accountNonLocked = true;
    private boolean credentialsNonExpired = true;
}

5.4 创建认证Token转换器AuthenticationConverter

创建AuthTokenAuthenticationConverter.java

public class AuthTokenAuthenticationConverter implements AuthenticationConverter {

    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_PREFIX = "Bearer ";

    @Override
    public Authentication convert(HttpServletRequest request) {
        String token = extractToken(request);
        if(!StringUtils.hasText(token)) {
            return null;
        }
        return new AuthTokenAuthenticationToken(token);
    }

    private String extractToken(HttpServletRequest request) {
        String token = request.getHeader(AUTHORIZATION_HEADER);
        if(!StringUtils.hasText(token)) {
            return null;
        }
        if (token.startsWith(BEARER_PREFIX)) {
            token = token.substring(BEARER_PREFIX.length());
        }
        return token;
    }
}

5.5 创建认证Token认证提供者AuthenticationProvider

创建AuthTokenAuthenticationProvider.java

@Slf4j
public final class AuthTokenAuthenticationProvider implements AuthenticationProvider {
    private final JwtUtil jwtUtil;

    public AuthTokenAuthenticationProvider(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        AuthTokenAuthenticationToken jwtAuthenticationToken = (AuthTokenAuthenticationToken) authentication;
        Object credentials = jwtAuthenticationToken.getCredentials();
        try {
            String token = (String) credentials;
            if (jwtUtil.validateToken(token)) {
                JWTClaimsSet claimsSet = jwtUtil.getClaims(token);
                String username = claimsSet.getStringClaim("username");
                String userId = claimsSet.getStringClaim("userId");
                List<String> roles = (List<String>) claimsSet.getClaim("authorities");

                List<GrantedAuthority> authorities = roles.stream()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

                AuthUser authUser = new AuthUser();
                authUser.setUsername(username);
                authUser.setUserId(userId);
                authUser.setAuthorities(authorities);

                return new AuthTokenAuthenticationToken(authUser, credentials, authorities);
            }else {
                throw new BadCredentialsException("Invalid Jwt Token.");
            }
        } catch (AuthenticationException e) {
            throw  e;
        } catch (Exception e) {
            throw new BadCredentialsException("Jwt Token Exception.");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return AuthTokenAuthenticationToken.class.isAssignableFrom(authentication);
    }

}

5.6 JWT认证过滤器

配置认证过滤器AuthenticationFilter:

    public AuthenticationFilter authClientAuthenticationFilter() {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager, new AuthTokenAuthenticationConverter());
        authenticationFilter.setSuccessHandler(new AuthClientAuthenticationSuccessHandler());
        return authenticationFilter;
    }

6. 参考资源

完整代码请参考项目仓库:mwc-security-authentication-client

演示地址:mwcs

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐