java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Boot JWT Spring Security 无状态登录

Spring Boot基于 JWT 优化 Spring Security 无状态登录实战指南

作者:Java天梯之路

本文介绍如何使用JWT优化SpringSecurity实现无状态登录,提高接口安全性,并通过实际操作步骤展示了如何配置JWT参数、实现JWT登录接口、认证过滤器等,感兴趣的朋友跟随小编一起看看吧

Spring Boot 实战:基于 JWT 优化 Spring Security 无状态登录

在上一篇文章中,我们通过 Spring Security 实现了基础的接口权限控制,但采用的 HTTP Basic 登录存在明显缺陷:安全性,用户名和密码只是简单的通过 Base64 编码之后就开始传送了,很容易被破解,进而暴露用户信息。

本文将引入 JWT(JSON Web Token) 技术,重构登录流程,让接口即安全又贴合企业级项目需求。

一、先搞懂:为什么需要 JWT?

Jwt的核心优势是 无状态

二、准备工作:添加 JWT 依赖

pom.xml<dependencies> 标签中,新增 JWT 相关依赖(基于 JJWT 框架,Spring 官方推荐):

        <!-- JWT 核心依赖(JJWT) -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <!-- JWT 实现依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <!-- JWT 加密算法依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

添加后点击 IDEA 右上角的 “Load Maven Changes” 刷新依赖,确保无红色报错。

三、第一步:实现 JWT 工具类(核心)

JWT 的核心操作包括 生成 Token验证 Token解析 Token 中的用户信息,我们创建一个工具类封装这些逻辑,方便后续调用。

com.example.firstspringbootproject.utils 包下创建 JwtUtil 类:

package com.example.firstspringbootproject.utils;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
    // 1. 从配置文件读取 JWT 关键参数(避免硬编码)
    @Value("${jwt.secret}") // 密钥(必须保密,建议在生产环境用环境变量配置)
    private String secret;
    @Value("${jwt.expiration}") // Token 过期时间(单位:毫秒,这里配置 2 小时)
    private long expiration;
    @Value("${jwt.header}") // 请求头中携带 Token 的字段名(如 Authorization)
    private String tokenHeader;
    @Value("${jwt.prefix}") // Token 前缀(如 Bearer,规范要求加空格)
    private String tokenPrefix;
    // 2. 生成 Token:基于用户信息(用户名、角色)
    public String generateToken(UserDetails userDetails) {
        // 存储 Token 中的自定义信息(Payload)
        Map<String, Object> claims = new HashMap<>();
        // 将用户角色存入 Token(后续验证权限用)
        claims.put("roles", userDetails.getAuthorities().stream()
                .map(authority -> authority.getAuthority())
                .toList());
        // 构建 Token 并返回
        return Jwts.builder()
                .setClaims(claims) // 自定义信息
                .setSubject(userDetails.getUsername()) // 用户名(唯一标识)
                .setIssuedAt(new Date()) // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration)) // 过期时间
                .signWith(SignatureAlgorithm.HS512, secret) // 加密算法(HS512)+ 密钥
                .compact();
    }
    // 3. 从 Token 中获取用户名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    // 4. 验证 Token 是否有效(未过期 + 用户名匹配)
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUsernameFromToken(token);
        // 验证逻辑:用户名一致 + Token 未过期 + Token 未被篡改
        return username.equals(userDetails.getUsername())
                && !isTokenExpired(token);
    }
    // 5. 从 Token 中获取自定义角色信息
    public String getRoleFromToken(String token) {
        Claims claims = getAllClaimsFromToken(token);
        // 从自定义信息中获取角色列表(这里简化为单个角色,多角色可返回 List)
        return ((List<String>) claims.get("roles")).get(0);
    }
    // ------------------------------
    // 以下是内部工具方法(无需外部调用)
    // ------------------------------
    // 解析 Token,获取所有自定义信息(Payload)
    private Claims getAllClaimsFromToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret) // 用密钥解密
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException | MalformedJwtException | SignatureException
                 | IllegalArgumentException | UnsupportedJwtException e) {
            // 捕获 Token 异常(过期、格式错误、签名错误等)
            throw new RuntimeException("无效的 Token:" + e.getMessage());
        }
    }
    // 从 Token 中获取指定信息(通用方法)
    private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
    // 判断 Token 是否过期
    private boolean isTokenExpired(String token) {
        final Date expirationDate = getClaimFromToken(token, Claims::getExpiration);
        return expirationDate.before(new Date());
    }
    // 6. 辅助方法:从请求头中提取 Token(去除前缀)
    public String extractTokenFromHeader(String header) {
        if (header != null && header.startsWith(tokenPrefix)) {
            // 例如:header = "Bearer eyJhbGciOiJIUzI1NiJ9...",返回后面的 Token 部分
            return header.substring(tokenPrefix.length()).trim();
        }
        return null;
    }
    // Getter 方法(供外部获取配置参数)
    public String getTokenHeader() {
        return tokenHeader;
    }
    public String getTokenPrefix() {
        return tokenPrefix;
    }
}

四、第二步:配置 JWT 参数(避免硬编码)

src/main/resources/application.yml(或 application.properties)中,添加 JWT 相关配置(替换硬编码,方便后续修改):

# JWT 配置
jwt:
  secret: firstspringbootproject2025secretkeyfirstspringbootproject2025secretkeyfirstspringbootproject2025secretkey # 密钥(生产环境建议用 64 位以上随机字符串)
#  expiration: 7200000 # Token 过期时间(7200000 毫秒 = 2 小时)
  expiration: 1000 # Token 过期时间(1000 毫秒 = 1 秒)
  header: Authorization # 请求头字段名
  prefix: Bearer  # Token 前缀(注意末尾有空格)

五、第三步:实现 JWT 登录接口(替换 HTTP Basic)

HTTP Basic 登录是通过浏览器弹窗输入账号密码,体验较差;我们需要自定义一个 登录接口(如 /api/login),客户端通过 JSON 提交账号密码,服务器验证通过后返回 JWT Token。

1. 创建登录请求参数实体类

com.example.firstspringbootproject.dto 包下创建 LoginRequestDTO(接收客户端提交的账号密码):

package com.example.firstspringbootproject.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(name = "LoginRequestDTO", description = "登录请求参数")
public class LoginRequestDTO {
    @NotBlank(message = "用户名不能为空")
    @Schema(description = "用户名", example = "admin")
    private String username;
    @NotBlank(message = "密码不能为空")
    @Schema(description = "密码", example = "123456")
    private String password;
}

2. 创建登录控制器

com.example.firstspringbootproject.controller 包下创建 AuthController,实现登录接口:

package com.example.firstspringbootproject.controller;
import com.example.firstspringbootproject.dto.LoginRequestDTO;
import com.example.firstspringbootproject.common.Result;
import com.example.firstspringbootproject.utils.JwtUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
@Tag(name = "认证接口", description = "登录、Token 相关接口")
public class AuthController {
    @Autowired
    private AuthenticationManager authenticationManager; // Spring Security 认证管理器
    @Autowired
    private JwtUtil jwtUtil; // 自定义 JWT 工具类
    /**
     * 登录接口:接收账号密码,验证通过后返回 JWT Token
     */
    @PostMapping("/login")
    @Operation(summary = "用户登录", description = "提交用户名和密码,获取 JWT Token")
    public Result<Map<String, String>> login(@Valid @RequestBody LoginRequestDTO loginRequest) {
        // 1. 调用 Spring Security 认证管理器,验证账号密码
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );
        // 2. 认证通过:从认证结果中获取用户信息
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        // 3. 生成 JWT Token
        String token = jwtUtil.generateToken(userDetails);
        // 4. 构建返回结果(包含 Token 和过期提示)
        Map<String, String> resultMap = new HashMap<>();
        resultMap.put("token", token);
        resultMap.put("expiration", "Token 有效期 2 小时,请及时刷新");
        resultMap.put("role", jwtUtil.getRoleFromToken(token)); // 返回用户角色,方便前端处理
        return Result.success(resultMap);
    }
}

六、第四步:实现 JWT 认证过滤器(核心拦截逻辑)

客户端登录成功后,后续请求会在 Authorization 头中携带 Token(格式:Bearer eyJhbGciOiJIUzI1NiJ9...)。我们需要自定义一个 过滤器,在请求到达接口前拦截 Token,完成以下操作:

com.example.firstspringbootproject.filter 包下创建 JwtAuthenticationFilter 类:

package com.example.firstspringbootproject.filter;
import com.example.firstspringbootproject.utils.JwtUtil;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
// 自定义 JWT 认证过滤器:每次请求都会执行(OncePerRequestFilter 确保一次请求只执行一次)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private UserDetailsService userDetailsService; // 之前实现的用户查询服务
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain
    ) throws ServletException, IOException {
        try {
            // 1. 从请求头中提取 Token
            String token = jwtUtil.extractTokenFromHeader(
                    request.getHeader(jwtUtil.getTokenHeader())
            );
            // 2. 验证 Token:非空 + 有效
            if (token != null) {
                // 2.1 从 Token 中获取用户名
                String username = jwtUtil.getUsernameFromToken(token);
                // 2.2 若用户名存在,且 Spring Security 上下文未存储用户信息(未登录)
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    // 2.3 从数据库查询用户完整信息(UserDetails)
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    // 2.4 验证 Token 有效性(未过期 + 用户名匹配)
                    if (jwtUtil.validateToken(token, userDetails)) {
                        // 3. 构建认证对象,存入 Spring Security 上下文
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(
                                        userDetails, // 用户信息
                                        null, // 密码(已验证,无需存储)
                                        userDetails.getAuthorities() // 用户权限(角色)
                                );
                        // 设置请求详情(如 IP、会话 ID)
                        authentication.setDetails(
                                new WebAuthenticationDetailsSource().buildDetails(request)
                        );
                        // 将认证对象存入上下文:后续接口权限校验会从这里获取用户信息
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        } catch (Exception e) {
            // Token 验证失败(如过期、篡改),打印日志但不阻断请求(后续会返回 401)
            logger.error("JWT Token 验证失败:" + e.getMessage());
        }
        // 4. 继续执行过滤器链(让请求到达后续接口或过滤器)
        filterChain.doFilter(request, response);
    }
}

七、第五步:重构 SecurityConfig(适配 JWT)

之前的 SecurityConfig 基于 HTTP Basic 登录,现在需要修改为 JWT 无状态登录,核心调整点:

修改 com.example.firstspringbootproject.config.SecurityConfig 类:

package com.example.firstspringbootproject.config;
import com.example.firstspringbootproject.common.Result;
import com.example.firstspringbootproject.entity.SysUser;
import com.example.firstspringbootproject.filter.JwtAuthenticationFilter;
import com.example.firstspringbootproject.mapper.SysUserMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用方法级权限控制(如 @PreAuthorize("hasRole('ADMIN')"))
public class SecurityConfig {
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter; // 自定义 JWT 过滤器
    // 1. 密码编码器(不变)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    // 2. 用户详情服务(不变)
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            SysUser sysUser = sysUserMapper.findByUsername(username);
            if (sysUser == null) {
                throw new UsernameNotFoundException("用户不存在: " + username);
            }
            return sysUser;
        };
    }
    // 3. 认证提供者(不变)
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService());
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }
    // 4. 认证管理器(不变)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    // 5. 核心规则配置(重点修改:适配 JWT)
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 1. 关闭 CSRF(前后端分离必须关)
                .csrf(csrf -> csrf.disable())
                // 2. 关闭 Session(无状态登录核心:不创建和使用 Session)
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                // 3. 配置接口访问规则(调整放行接口)
                .authorizeHttpRequests(auth -> auth
                        // ① 放行:接口文档(Knife4j)、登录接口
                        .requestMatchers("/doc.html", "/webjars/**", "/v3/api-docs/**", "/api/login").permitAll()
                        // ② 管理员接口:仅 ADMIN 可访问
                        .requestMatchers("/api/user/all").hasRole("ADMIN")
                        // ③ 普通用户接口:USER/ADMIN 可访问
                        .requestMatchers("/api/user/**", "/api/product/**").hasAnyRole("USER", "ADMIN")
                        // ④ 其他接口:必须登录(Token 有效)
                        .anyRequest().authenticated()
                )
                // 4. 加入 JWT 过滤器:在 UsernamePasswordAuthenticationFilter 之前执行
                // (先验证 Token,再执行后续认证逻辑)
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                // 5. 统一异常处理(不变:未登录返回 401,权限不足返回 403)
                .exceptionHandling(ex -> ex
                        .authenticationEntryPoint((request, response, authException) -> {
                            response.setContentType("application/json;charset=UTF-8");
                            Result<Void> result = Result.error(401, "未登录或 Token 已过期,请重新登录");
                            response.getWriter().write(objectMapper.writeValueAsString(result));
                        })
                        .accessDeniedHandler((request, response, accessDeniedException) -> {
                            response.setContentType("application/json;charset=UTF-8");
                            Result<Void> result = Result.error(403, "权限不足,无法访问");
                            response.getWriter().write(objectMapper.writeValueAsString(result));
                        })
                );
        // 关联认证提供者
        http.authenticationProvider(authenticationProvider());
        return http.build();
    }
}

八、第六步:测试 JWT 无状态登录流程

启动项目,用 Apifox 测试完整流程,验证无状态登录和权限控制是否生效:

1. 测试 1:调用登录接口,获取 Token

{
    "username": "admin",
    "password": "123456"
}
{
    "code": 200,
    "msg": "success",
    "data": {
        "role": "ROLE_ADMIN",
        "expiration": "Token 有效期 2 小时,请及时刷新",
        "token": "eyJhbGciOiJIUzUxMiJ9.eyJyb2xlcyI6WyJST0xFX0FETUlOIl0sInN1YiI6ImFkbWluIiwiaWF0IjoxNzYzNDgxMDk0LCJleHAiOjE3NjM0ODgyOTR9.hSxBCTlQo5tUai-knfM2Sh4nVmehizOslcHkvG86jdC-U7-EztskPkF0r4G0DcaTMV3eFcJUu4pCewDdnniq2A"
    }
}

复制返回的 token,后续请求会用到。

2. 测试 2:携带 Token 访问管理员接口

{
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "name": "小明",
            "age": 20,
            "phone": "13800138000"
        },
        {
            "id": 2,
            "name": "小红",
            "age": 19,
            "phone": "13900139000"
        },
        {
            "id": 3,
            "name": "小李",
            "age": 22,
            "phone": "13700137000"
        }
    ]
}

3. 测试 3:普通用户 Token 访问管理员接口(权限不足)

{
    "code": 403,
    "msg": "权限不足,无法访问",
    "data": null
}

4. 测试 4:Token 过期(模拟)

{
    "code": 401,
    "msg": "未登录或 Token 已过期,请重新登录",
    "data": null
}

九、常见问题与优化建议

1. 问题:Token 被盗用怎么办?

  1. 缩短 Token 过期时间(如 30 分钟),同时实现 Token 刷新接口(用旧 Token 换取新 Token,避免频繁登录);
  2. 维护一个 黑名单(如 Redis),用户登出或 Token 被盗时,将 Token 加入黑名单,验证时先检查是否在黑名单中。

2. 问题:密钥(secret)泄露怎么办?

  1. 生产环境中,密钥通过 环境变量配置中心 注入,不写入代码或配置文件;
  2. 使用 非对称加密算法(如 RS256),用私钥生成 Token,公钥验证 Token,私钥严格保密。

## 十、总结:JWT 无状态登录的核心价值

通过本文的改造,我们实现了 Spring Boot + Spring Security + JWT 的无状态登录,核心优势总结如下:

至此,恭喜你已具备初级程序员开发水平,可以试着投简历找工作啦。

到此这篇关于Spring Boot基于 JWT 优化 Spring Security 无状态登录实战指南的文章就介绍到这了,更多相关Spring Boot JWT Spring Security 无状态登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文