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 的认证过滤器
  • 令牌刷新机制与无感刷新实现
  • 完整的登录与授权流程

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

相关推荐
没有bug.的程序员2 小时前
服务安全:内部服务如何防止“裸奔”?
java·网络安全·云原生安全·服务安全·零信任架构·微服务安全·内部鉴权
一线大码2 小时前
SpringBoot 3 和 4 的版本新特性和升级要点
java·spring boot·后端
weixin_440730503 小时前
java数组整理笔记
java·开发语言·笔记
weixin_425023003 小时前
Spring Boot 配置文件优先级详解
spring boot·后端·python
weixin_425023003 小时前
Spring Boot 实用核心技巧汇总:日期格式化、线程管控、MCP服务、AOP进阶等
java·spring boot·后端
一线大码3 小时前
Java 8-25 各个版本新特性总结
java·后端
2501_906150563 小时前
私有部署问卷系统操作实战记录-DWSurvey
java·运维·服务器·spring·开源
better_liang3 小时前
每日Java面试场景题知识点之-TCP/IP协议栈与Socket编程
java·tcp/ip·计算机网络·网络编程·socket·面试题
VX:Fegn08954 小时前
计算机毕业设计|基于springboot + vue校园社团管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
niucloud-admin4 小时前
java服务端——controller控制器
java·开发语言