Spring Boot JWT登录授权使用指南(无感刷新)

一、引言

在分布式系统和前后端分离架构中,传统的基于 Session 的认证方式存在跨域难处理、服务端存储压力大等问题。JWT(JSON Web Token) 作为一种轻量级的身份认证与授权方案,凭借其无状态、可跨域、易于扩展的特性,成为 Spring Boot 项目中实现认证授权的主流选择。本文将从环境搭建、核心实现到进阶优化,完整讲解 Spring Boot 整合 JWT 实现登录认证与接口授权的全流程。

二、技术栈与环境准备

1. 核心依赖

在 Spring Boot 项目的pom.xml(Maven)或build.gradle(Gradle)中引入以下

  • Spring Security:提供认证与授权的基础框架
  • JJWT:Java 领域主流的 JWT 工具库,支持 JWT 的生成、解析与验证
  • Spring Web:用于编写接口测试认证授权流程

Maven 依赖配置示例:

xml 复制代码
<!-- Spring Boot Starter Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<!-- JWT -->
<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>

2. 核心概念说明

JWT 结构: 由 Header(头部)、Payload(载荷)、Signature(签名)三部分组成,以.分隔,例如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

  • Header:存储算法类型和令牌类型,如{"alg":"HS256","typ":"JWT"}
  • Payload:存储用户身份信息(如用户名、角色)和过期时间等声明,不建议存放敏感信息
  • Signature:通过 Header 指定的算法,结合密钥对 Header 和 Payload 进行加密,保证令牌不被篡改

认证流程: 用户登录成功后,服务端生成 JWT 返回给客户端;客户端后续请求携带 JWT,服务端验证令牌有效性后完成授权

三、核心功能实现

1. JWT 工具类封装

java 复制代码
@Component
public class JwtUtils {

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

    @Value("${jwt.access-token-expire-time}")
    private long accessTokenExpireTime;

    @Value("${jwt.refresh-token-expire-time}")
    private long refreshTokenExpireTime;

    /**
     * 生成访问令牌
     */
    public String generateAccessToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        return generateToken(claims, username, accessTokenExpireTime);
    }

    /**
     * 生成刷新令牌
     */
    public String generateRefreshToken(String username) {
        Map<String, Object> claims = new HashMap<>();
        return generateToken(claims, username, refreshTokenExpireTime);
    }

    /**
     * 生成token
     */
    private String generateToken(Map<String, Object> claims, String subject, long expireTime) {
        SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expireTime))
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();
    }

    /**
     * 从token中获取用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getSubject();
    }

    /**
     * 验证token是否有效
     */
    public boolean validateToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
            Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 判断token是否过期
     */
    public boolean isTokenExpired(String token) {
        Claims claims = getClaimsFromToken(token);
        Date expiration = claims.getExpiration();
        return expiration.before(new Date());
    }

    /**
     * 从token中获取claims
     */
    private Claims getClaimsFromToken(String token) {
        SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

2. 实现认证过滤器

java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) 
            throws ServletException, IOException {
        
        // 跳过登录和刷新令牌的接口
        String requestURI = request.getRequestURI();
        if (requestURI.equals("/api/auth/login") || requestURI.equals("/api/auth/refresh")) {
            chain.doFilter(request, response);
            return;
        }

        // 从请求头获取token
        String token = getTokenFromRequest(request);

        if (StringUtils.hasText(token) && jwtUtils.validateToken(token)) {
            String username = jwtUtils.getUsernameFromToken(token);
            
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            // 设置认证信息到上下文
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
            SecurityContextHolder.clearContext();
        }

        chain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

3. 实现无感刷新令牌过滤器

创建JwtRefreshFilter实现令牌的无感刷新:

java 复制代码
@Component
public class JwtRefreshFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    // 当token剩余有效期小于10分钟时,自动刷新
    private static final long REFRESH_THRESHOLD = 10 * 60 * 1000;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
            throws ServletException, IOException {
        
        // 跳过登录和刷新接口
        String requestURI = request.getRequestURI();
        if (requestURI.equals("/api/auth/login") || requestURI.equals("/api/auth/refresh")) {
            filterChain.doFilter(request, response);
            return;
        }

        // 获取请求头中的token
        String authorizationHeader = request.getHeader("Authorization");
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authorizationHeader.substring(7);
        try {
            if (jwtUtils.validateToken(token)) {
                // 检查token是否即将过期
                long remainingTime = jwtUtils.getTokenRemainingTime(token);
                if (remainingTime < REFRESH_THRESHOLD) {
                    String username = jwtUtils.getUsernameFromToken(token);
                    String newAccessToken = jwtUtils.generateAccessToken(username);
                    // 将新token添加到响应头
                    response.setHeader("Authorization", "Bearer " + newAccessToken);
                }
            }
        } catch (Exception e) {
            SecurityContextHolder.clearContext();
        }
        
        filterChain.doFilter(request, response);
    }
}

4. 配置 Spring Security

创建SecurityConfig配置安全规则:

java 复制代码
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

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

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthFilter, 
                                                  JwtRefreshFilter jwtRefreshFilter) throws Exception {
        http
            .cors().and().csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests()
            .antMatchers("/api/auth/**", "/api/public/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .exceptionHandling()
            .authenticationEntryPoint((request, response, ex) -> {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json");
                response.getWriter().write("{\"error\":\"Unauthorized\",\"message\":\"Authentication required\"}");
            });

        // 添加JWT过滤器
        http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(jwtRefreshFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

5. 实现认证服务

创建AuthService接口和实现类处理登录和刷新令牌业务:

java 复制代码
public interface AuthService {
    LoginResponseDTO login(LoginRequestDTO loginRequest);
    LoginResponseDTO refreshToken(RefreshTokenRequestDTO refreshTokenRequest);
}

@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${jwt.refresh-token-expire-time}")
    private long refreshTokenExpireTime;

    @Override
    public LoginResponseDTO login(LoginRequestDTO loginRequest) {
        // 验证输入
        if (loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername()) || 
            StringUtils.isEmpty(loginRequest.getPassword())) {
            throw new BadCredentialsException("Invalid login request");
        }
        
        try {
            // 执行认证
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(), 
                    loginRequest.getPassword()
                )
            );
            
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 生成token
            String username = loginRequest.getUsername();
            String accessToken = jwtUtils.generateAccessToken(username);
            String refreshToken = jwtUtils.generateRefreshToken(username);
            
            // 存储refresh token到Redis
            String refreshTokenKey = "refresh_token:" + username;
            stringRedisTemplate.opsForValue().set(
                refreshTokenKey, 
                refreshToken, 
                refreshTokenExpireTime, 
                TimeUnit.MILLISECONDS
            );
            
            // 构建响应
            LoginResponseDTO response = new LoginResponseDTO();
            response.setAccessToken(accessToken);
            response.setRefreshToken(refreshToken);
            response.setExpiresIn(refreshTokenExpireTime);
            
            return response;
        } catch (AuthenticationException e) {
            throw new BadCredentialsException("Invalid username or password");
        }
    }

    @Override
    public LoginResponseDTO refreshToken(RefreshTokenRequestDTO refreshTokenRequest) {
        // 验证输入
        if (refreshTokenRequest == null || StringUtils.isEmpty(refreshTokenRequest.getRefreshToken())) {
            throw new BadCredentialsException("Invalid refresh token request");
        }
        
        String refreshToken = refreshTokenRequest.getRefreshToken();
        
        // 验证refresh token
        if (!jwtUtils.validateToken(refreshToken)) {
            throw new BadCredentialsException("Invalid refresh token");
        }
        
        String username = jwtUtils.getUsernameFromToken(refreshToken);
        if (StringUtils.isEmpty(username)) {
            throw new BadCredentialsException("Invalid refresh token");
        }
        
        // 验证Redis中存储的refresh token
        String storedToken = stringRedisTemplate.opsForValue().get("refresh_token:" + username);
        if (storedToken == null || !storedToken.equals(refreshToken)) {
            throw new BadCredentialsException("Invalid refresh token");
        }
        
        // 生成新的token
        String newAccessToken = jwtUtils.generateAccessToken(username);
        String newRefreshToken = jwtUtils.generateRefreshToken(username);
        
        // 更新Redis中的refresh token
        stringRedisTemplate.opsForValue().set(
            "refresh_token:" + username, 
            newRefreshToken, 
            refreshTokenExpireTime, 
            TimeUnit.MILLISECONDS
        );
        
        LoginResponseDTO response = new LoginResponseDTO();
        response.setAccessToken(newAccessToken);
        response.setRefreshToken(newRefreshToken);
        response.setExpiresIn(refreshTokenExpireTime);

        return response;
    }
}

四、相关注解总结

注解名称 核心作用 使用场景 关键注意事项
@EnableWebSecurity 开启 Spring Security 的 Web 安全功能,加载安全过滤器链和相关配置 标注在自定义的 Spring Security 配置类上 必须搭配@Configuration使用,否则配置不生效
@EnableGlobalMethodSecurity 开启方法级权限控制,支持多种权限注解 标注在 Security 配置类上,需指定启用的注解类型 常用属性:prePostEnabled=true(启用@PreAuthorize等)、securedEnabled=true(启用@Secured
@PreAuthorize 方法执行前校验权限,支持 SpEL 表达式,可实现复杂权限判断 控制器接口方法、服务层方法的权限控制(细粒度权限) 依赖@EnableGlobalMethodSecurity(prePostEnabled=true),支持角色、权限、请求参数校验
@PostAuthorize 方法执行后校验权限,基于方法返回值判断 需根据返回结果控制权限的场景(极少使用,避免方法执行产生副作用) SpEL 表达式中用returnObject指代方法返回值
@Secured 基于角色的粗粒度权限控制 控制器或服务层方法的角色校验 需启用securedEnabled=true,角色名称必须以ROLE_为前缀
@RolesAllowed JSR-250 规范注解,基于角色的权限控制 控制器或服务层方法的多角色授权 需启用jsr250Enabled=true,角色名称可省略ROLE_前缀(框架自动补充)
@AuthenticationPrincipal 直接获取当前认证用户的UserDetails或自定义用户信息 控制器方法参数中,需获取当前登录用户信息时 无需手动从SecurityContextHolder获取,直接注入即可
@PreFilter 方法执行前,对集合类型参数进行过滤,仅保留符合权限的元素 数据查询前的参数过滤(数据级权限控制) 仅对集合类型参数生效,SpEL 表达式用filterObject指代集合元素
@PostFilter 方法执行后,对集合类型返回值进行过滤,仅保留符合权限的元素 数据返回后的结果过滤(数据级权限控制) 仅对集合类型返回值生效,依赖@EnableGlobalMethodSecurity启用

总结

本指南介绍了如何在 Spring Boot 应用中实现基于 JWT 的认证授权功能,包括:

  • JWT 令牌的生成与验证
  • 基于 Spring Security 的认证过滤器
  • 令牌刷新机制与无感刷新实现
  • 完整的登录与授权流程

通过这种方式,我们可以实现无状态的认证系统,适合分布式应用和前后端分离架构。实际应用中,还可以根据需要添加更多功能,如令牌撤销、角色权限控制等。

相关推荐
带刺的坐椅11 小时前
Solon AI Skills 会是 Agent 的未来吗?
java·agent·langchain4j·solon-ai
jacGJ11 小时前
记录学习--文件读写
java·前端·学习
花间相见11 小时前
【JAVA开发】—— Nginx服务器
java·开发语言·nginx
扶苏-su12 小时前
Java---Properties 类
java·开发语言
cypking12 小时前
四、CRUD操作指南
java
2301_7806698612 小时前
文件字节流输出、文件复制、关闭流的方法
java
剑锋所指,所向披靡!13 小时前
C++之类模版
java·jvm·c++
Coder_Boy_14 小时前
基于SpringAI的在线考试系统-0到1全流程研发:DDD、TDD与CICD协同实践
java·人工智能·spring boot·架构·ddd·tdd
sheji341614 小时前
【开题答辩全过程】以 面向高校校园的物物交换系统设计与实现为例,包含答辩的问题和答案
java·eclipse
卓怡学长14 小时前
m115乐购游戏商城系统
java·前端·数据库·spring boot·spring·游戏