Spring Boot 3.x + Spring Security 搭建前后端分离JWT登录认证服务

1. 引言

JSON Web Token(JWT)作为一种轻量级的认证机制,是目前非常流行的一种认证方式,它具有无状态、可扩展、跨语言等优点,非常适合前后端分离的应用架构。

本文将详细介绍如何使用Spring Boot 3.x和Spring Security搭建一个基于JWT的安全认证服务,包括项目初始化、核心功能实现、配置等内容。

2. 技术栈

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

3. 项目初始化

3.1 创建Spring Boot项目

使用Spring Boot Initializr创建一个新的Spring Boot项目,取名如xxx-security-authentication-server,添加以下依赖:

  • Spring Security
  • Spring Boot Starter Web
  • Lombok

3.2 添加依赖

pom.xml文件中添加JSON,Nimbus JOSE JWT等依赖:

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

        <!-- Nimbus JOSE + JWT -->
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>${nimbus-jose-jwt.version}</version>
        </dependency>

4. 核心功能实现

4.1 创建会话用户类

创建会话用户类User.java


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

创建认证响应类AuthResponse.java


@Data
public class AuthResponse {
    private String accessToken;
    private String refreshToken;
    private String tokenType;
    private long expiresIn;
}

4.2 JWT工具类

创建JWT生成工具类JwtTokenGenerator.java


public class JwtTokenGenerator {
    private final JWSSigner signer;
    private final JwtProperties jwtProperties;

    public JwtTokenGenerator(JwtProperties jwtProperties, PrivateKey privateKey) {
        this.jwtProperties = jwtProperties;
        this.signer = createSigner(privateKey);
    }

    private JWSSigner createSigner(PrivateKey privateKey) {
        return new RSASSASigner(privateKey);
    }

    public String generateRefreshToken(String subject) {
        return generateToken(subject, Map.of("type", "refresh"),jwtProperties.getRefreshExpirationTimeInMinutes() * 60);
    }

    /**
     * 生成jwt token
     * @param subject 主题
     * @param claims 声明
     * @return jwt token
     */
    public String generateToken(String subject, Map<String, Object> claims) {
        return generateToken(subject, claims,jwtProperties.getExpirationTimeInMinutes() * 60);
    }

    /**
     * 生成jwt token
     * @param subject 主题
     * @param claims 声明
     * @param expireTimeInSeconds 超时时间
     * @return jwt token
     */
    public String generateToken(String subject, Map<String, Object> claims, long expireTimeInSeconds) {
        try {
            Instant now = Instant.now();
            Instant expiration = now.plus(expireTimeInSeconds, ChronoUnit.SECONDS);

            JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder()
                    .subject(subject)
                    .issuer(jwtProperties.getIssuer())
                    .issueTime(Date.from(now))
                    .expirationTime(Date.from(expiration));

            claims.forEach(claimsSetBuilder::claim);

            JWTClaimsSet claimsSet = claimsSetBuilder.build();

            SignedJWT signedJWT = new SignedJWT(
                    new JWSHeader.Builder(JWSAlgorithm.RS256).build(),
                    claimsSet
            );

            signedJWT.sign(signer);

            return signedJWT.serialize();
        } catch (JOSEException e) {
            throw new JwtGenerationException("Failed to generate JWT token", e);
        }
    }

}

创建JWT验证工具类JwtTokenValidator.java


public class JwtTokenValidator {
    private final JWSVerifier verifier;
    private final String issuer;

    public JwtTokenValidator(JwtProperties jwtProperties, PublicKey publicKey) {
        this.issuer = jwtProperties.getIssuer();
        this.verifier = createVerifier(publicKey);
    }

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

    /**
     * 验证jwt token
     * @param token token
     * @return true 通过 false 失败
     */
    public boolean validateToken(String token) {
        try {
            SignedJWT signedJWT = SignedJWT.parse(token);

            // Verify the signature
            boolean signatureValid = signedJWT.verify(verifier);
            if (!signatureValid) {
                return false;
            }

            // Check if token is expired
            if (signedJWT.getJWTClaimsSet().getExpirationTime().before(new Date())) {
                return false;
            }

            // Check issuer
            if (!issuer.equals(signedJWT.getJWTClaimsSet().getIssuer())) {
                return false;
            }

            return true;
        } catch (ParseException | JOSEException e) {
            throw new JwtValidationException("Failed to validate JWT token", e);
        }
    }

    public String getSubject(String token) {
        try {
            SignedJWT signedJWT = SignedJWT.parse(token);
            return signedJWT.getJWTClaimsSet().getSubject();
        } catch (ParseException e) {
            throw new JwtValidationException("Failed to parse JWT token", e);
        }
    }

    public <T> T getClaim(String token, String claimName, Class<T> claimType) {
        try {
            SignedJWT signedJWT = SignedJWT.parse(token);
            return claimType.cast(signedJWT.getJWTClaimsSet().getClaim(claimName));
        } catch (ParseException e) {
            throw new JwtValidationException("Failed to parse JWT token", e);
        }
    }

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

4.3 认证服务类

创建认证服务AuthenticationService接口:


public interface AuthenticationService {

    /**
     * 根据用户名创建 Jwt Token
     * @param username 用户名
     * @param type token 类型
     * @return JwtToken
     */
    String generateToken(String username, String type);

    /**
     * 根据用户创建 JwtToken 响应
     * @param user 用户
     * @return JwtToken 响应
     */
    AuthResponse generateAuthResponse(User user);

    /**
     * 根据refreshToken生成JwtToken 响应
     * @param refreshToken 刷新 Token
     * @return JwtToken 响应
     */
    AuthResponse refreshToken(String refreshToken);

}

认证服务实现类AuthenticationServiceImpl:

public class AuthenticationServiceImpl implements AuthenticationService {

    private final JwtTokenGenerator tokenGenerator;
    private final JwtTokenValidator tokenValidator;
    private final TokenBlacklist tokenBlacklist;
    private final JwtProperties jwtProperties;
    private final UserDetailsService userDetailsService;

    @Override
    public String generateToken(String username, String type) {
        return tokenGenerator.generateToken(username, Map.of("type", type));
    }

    @Override
    public AuthResponse generateAuthResponse(User user) {
        String accessToken = tokenGenerator.generateToken(user.getUsername(), getClaims(user));
        String refreshToken = tokenGenerator.generateRefreshToken(user.getUsername());

        return new AuthResponse(accessToken, refreshToken, jwtProperties.getExpirationTimeInMinutes() * 60);
    }

    @Override
    public AuthResponse refreshToken(String refreshToken) {
        if (tokenBlacklist.isTokenBlacklisted(refreshToken)) {
            throw new RuntimeException("Refresh token has been revoked");
        }

        if(!tokenValidator.validateToken(refreshToken)) {
            throw new BadCredentialsException("Token Invalid");
        }

        String tokenType = tokenValidator.getClaim(refreshToken, "type", String.class);
        if (!"refresh".equals(tokenType)) {
            throw new RuntimeException("Invalid refresh token");
        }

        String username = tokenValidator.getSubject(refreshToken);
        User user = (User) userDetailsService.loadUserByUsername(username);

        String newAccessToken = tokenGenerator.generateToken(user.getUsername(), getClaims(user));
        String newRefreshToken = tokenGenerator.generateRefreshToken(user.getUsername());

        // Add old refresh token to blacklist
        tokenBlacklist.addToken(refreshToken);

        return new AuthResponse(newAccessToken, newRefreshToken, jwtProperties.getExpirationTimeInMinutes() * 60);
    }

    private Map<String, Object> getClaims(User user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", user.getUserId());
        claims.put("username", user.getUsername());
        List<String> roles = new ArrayList<>();
        for(GrantedAuthority authority : user.getAuthorities()) {
            roles.add(authority.getAuthority());
        }
        claims.put("authorities", roles);
        return claims;
    }
}

4.5 登录认证成功处理,返回JWT Token响应

创建LoginAuthenticationSuccessHandler.java类:


@RequiredArgsConstructor
public class LoginAuthenticationSuccessHandler extends AbstractAuthenticationSuccessHandler {
    protected final AuthenticationService authenticationService;
    private final JwtTokenValidator jwtTokenValidator;
    private final ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        User user = (User) authentication.getPrincipal();

        String userId = user.getUserId();
        String username = user.getUsername();
        LoginSuccessEvent loginSuccessEvent = new LoginSuccessEvent(this, username
                , userId, null);
        applicationEventPublisher.publishEvent(loginSuccessEvent);

        AuthResponse authResponse = authenticationService.generateAuthResponse(user);
        bindThirdPartyUser(request, response, username, userId);
        writeResponse(response, authResponse);
    }

    @SneakyThrows
    public void writeResponse(HttpServletResponse response, Object result) {
        response.setContentType("application/json");
        response.getWriter().write(JSONUtil.toJsonStr(result));
    }

}

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

演示地址:mwcs

Logo

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

更多推荐