最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
🌞《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合
- 1. 前言
- 2.JWT 基本原理
- 3.Spring Security 与 JWT 整合思路
- 4. 实战开始
- ❶ 配置JWT工具类
- ❷ JwtAuthFilter:JWT 过滤器
- ❸ 安全配置类
- ❹ 配置测试controller
- ❹ 运行测试
- 5. 基于RBAC角色模型的升级
- ❶ 复用第五章节RBAC角色模型代码
- ❷ 自定义UserDetailsService实现
- ❸ 改造JwtAuthFilter
- ❹ 创建authService处理用户登陆
- ❺ 调整JwtSecurityConfig配置
- ❺ 定义接口统一返回Result + 全局异常处理
- ❺ controller中登陆、测试方法
- ❻ 运行测试
- 6. 结语
回顾链接:
最新Spring Security实战教程(一)初识Spring Security安全框架
最新Spring Security实战教程(二)表单登录定制到处理逻辑的深度改造
最新Spring Security实战教程(三)Spring Security 的底层原理解析
最新Spring Security实战教程(四)基于内存的用户认证
最新Spring Security实战教程(五)基于数据库的动态用户认证传统RBAC角色模型实战开发
最新Spring Security实战教程(六)最新Spring Security实战教程(六)基于数据库的ABAC属性权限模型实战开发
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用
最新Spring Security实战教程(八)Remember-Me实现原理 - 持久化令牌与安全存储方案
专栏更新完毕后,博主将会上传所有章节代码到CSDN资源免费给大家下载,如你不想等后续章节代码需提前获取,可以私信或留言!
1. 前言
又是新的一周,博主继续l来给大家更新 Spring Security 实战教程系列啦~ 通过前面的章节教程从认证到授权,相信大家已经基本了解 Spring Security的工作原理。
但在前后端分离架构成为主流的今天,传统的Session-Cookie认证模式面临跨域限制、服务端状态维护等难题。JWT(JSON Web Token)作为无状态令牌方案,凭借其自包含、易扩展的特性,成为现代分布式系统的首选认证方案。
那么本章节,博主就带着大家一起来进行 SpringSecurity 前后端分离认证实战,手把手教构建安全的JWT认证体系!
2.JWT 基本原理
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间以 JSON 对象安全地传输信息。其主要特点包括:
- 无状态:服务端无需保存会话信息,降低了服务端压力(传统Session是保存服务端)
- 跨域支持:适用于前后端分离应用场景
JWT 通常由三部分组成:Header、Payload 和 Signature。在认证场景中,用户登录后服务器生成一个包含用户信息的Token,前端将该 Token 存储在本地,并在后续请求中携带到 HTTP Header中。服务端通过解析和验证 Token,完成用户身份认证。
3.Spring Security 与 JWT 整合思路
整合 JWT 与 Spring Security 的关键在于:
-
无状态配置:关闭 Spring Security 默认的 Session 管理,采用无状态认证
-
自定义认证入口:提供一个登录接口,验证用户凭据,生成 JWT
-
JWT 拦截过滤器:在请求到达业务逻辑前,拦截 HTTP 请求,解析和验证 JWT,将用户认证信息写入 SecurityContext
完整流程图如下:
4. 实战开始
本次演示我们先简单模拟集成,还是在之前的Maven项目中新建子模块 命名:jwt-spring-security
Maven配置文件追加JWT库
io.jsonwebtoken jjwt 0.12.6
YML配置文件中添加密钥以及Token 有效期
jwt: secret: "dGhpcyBpcyBhIHNlY3JldCBrZXkgZm9yIG15IGFwcA==" # Base64编码密钥 expiration: 900000 # 15分钟
❶ 配置JWT工具类
如果你的系统使用了 Hutool 工具包,可以直接调用 JWTUtil 来创建以及验证JWT,具体参考 https://doc.hutool.cn/pages/JWTUtil/
这里我们使用的是 io.jsonwebtoken 来自定义JWT
@Component public class JwtUtils { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private long expiration; // 生成令牌时设置subject public String generateToken(String username) { return Jwts.builder() .subject(username) // 关键:设置用户名到subject .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expiration)) .signWith(getSigningKey()) .compact(); } // 用户名提取方法 public String extractUsername(String token) { return parseClaims(token).getSubject(); } // 统一的令牌验证方法 public boolean validateToken(String token) { parseClaims(token); // 复用解析逻辑 return true; } // 校验Token是否过期 public boolean isTokenExpired(String token) { Claims claims = parseClaims(token); return claims.getExpiration().before(new Date()); } // 私有方法:统一解析令牌声明 private Claims parseClaims(String token) { return Jwts.parser() .verifyWith(getSigningKey()) .build() .parseSignedClaims(token) .getPayload(); } // 密钥生成方法 private SecretKey getSigningKey() { byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8); return Keys.hmacShaKeyFor(keyBytes); } }
❷ JwtAuthFilter:JWT 过滤器
在前面章节我们讲解 Spring Security 底层原理的时候,我们知道 Spring Security 默认 DefaultSecurityFilterChain 启动的时候,会通过多个 Filter 来逐层检查,实际上同理只需要我们自定义JWT 过滤器来实现我们所需业务即可
@Component @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求头获取 Token,约定使用 "Authorization" 且前缀为 "Bearer " String token = parseToken(request); if (token != null && jwtUtils.validateToken(token)) { String username = jwtUtils.extractUsername(token); // 如果 token 存在且 SecurityContext 为空,设置用户认证 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 这里只是示例,实际应用中应加载用户详情信息 UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 将认证信息放入上下文中 SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } private String parseToken(HttpServletRequest request) { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { return header.substring(7); } return null; } }
❸ 安全配置类
Spring Security 配置类主要关闭 session 管理,并追加自定义 JWT Filter
放行/api/auth/login 接口地址,其余均需要验证JWT
@Configuration //开启方法级的安全控制 @EnableMethodSecurity @RequiredArgsConstructor public class JwtSecurityConfig { private final JwtAuthFilter jwtAuthFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 禁用 CSRF,因为使用 JWT 方式无需 Session .csrf(csrf -> csrf.disable()) // 设置无状态 Session 管理 .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) ; return http.build(); } }
❹ 配置测试controller
编写一个登陆接口 /api/auth/login 以及一个验证接口 /api/auth/verify
@RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthController { private final JwtUtils jwtUtils; // 简单示例,真实场景中应从数据库加载用户信息并校验密码 @PostMapping("/login") public ResponseEntity login(@RequestBody LoginRequest loginRequest) { // 这里假设用户名为 "admin",密码为 "admin" if ("admin".equals(loginRequest.getUsername()) && "admin".equals(loginRequest.getPassword())) { String token = jwtUtils.generateToken(loginRequest.getUsername()); return ResponseEntity.ok(token); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误"); } // 简单示例,真实场景中应从数据库加载用户信息并校验密码 @GetMapping("/verify") public ResponseEntity verify() { return ResponseEntity.ok("验证用户访问成功"); } }
❹ 运行测试
这里博主使用的 Apifox 测试效果,首先登陆获得token
在接下来的验证接口,配置Header 设置Authorization的Key,并将登陆获得的token设置值
格式:Bearer +登陆获得的Token
至此我们就完成了最简单的 JWT+SpringSecurity 整合
5. 基于RBAC角色模型的升级
回忆一下我们之前第五章节中,RBAC角色模型的表设计:需要通过用户ID查询到用户分配的角色+角色所配置的菜单资源
这里我们就基于 最新Spring Security实战教程(五)基于数据库的动态用户认证传统RBAC角色模型实战开发 复用代码,集合JWT动态从数据库获取用户信息认证、授权
❶ 复用第五章节RBAC角色模型代码
相关RBAC角色模型的知识,请小伙伴自行查阅第五章节内容,篇幅有限这里就简单贴出代码即可
实体类
// SysMenu实体类 @Data @TableName("sys_menu") public class SysMenu { @TableId(type = IdType.AUTO) private Long menuId; private String menuName; private String perms; } // SysRole实体类 @Data @TableName("sys_role") public class SysRole { @TableId(type = IdType.AUTO) private Long roleId; private String roleName; private String roleKey; @TableField(exist = false) private List menus; } // SysUser实体类 // 博主为了方便,直接使用数据库映射的SysUser对象直接实现UserDetails,大家在开发过程中建议单独构建实现对象! @Data @TableName("sys_user") public class SysUser implements UserDetails { @TableId(type = IdType.AUTO) private Long userId; @TableField("login_name") private String username; // Spring Security认证使用的字段 private String password; private String status; // 状态(0正常 1锁定) private String delFlag; // 删除标志(0代表存在 1代表删除) @TableField(exist = false) private List roles; // 实现UserDetails接口 @Override public Collection authorities; }
❺ 调整JwtSecurityConfig配置
@Configuration //开启方法级的安全控制 @EnableMethodSecurity @RequiredArgsConstructor public class JwtSecurityConfig { private final JwtAuthFilter jwtAuthFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 禁用 CSRF,因为使用 JWT 方式无需 Session .csrf(csrf -> csrf.disable()) // 设置无状态 Session 管理 .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login").permitAll() .requestMatchers("/api/auth/loginByMysql").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling(ex -> ex .authenticationEntryPoint(jwtAuthenticationEntryPoint()) ); return http.build(); } //统一认证凭证处理 @Bean public AuthenticationEntryPoint jwtAuthenticationEntryPoint() { return (request, response, authException) -> { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\": 401,\"msg\": \"无效的认证凭证\"}"); }; } // 自定义认证中使用 AuthenticationManager @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } //定义密码加密方式 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
❺ 定义接口统一返回Result + 全局异常处理
之前简单的整合过程中,小伙伴或许发现了接口返回的非JSON数据,且token过期等没有正确的错误提示,这里我们继续完善一下
@Data @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) public class AjaxResult { private static final long serialVersionUID = 1L; /** * 状态码 */ private int code; /** * 返回内容 */ private String msg; /** * 数据对象 */ private T data; /** * 初始化一个新创建的 AjaxResult 对象 * * @param code 状态码 * @param msg 返回内容 */ public AjaxResult(int code, String msg) { this.code = code; this.msg = msg; } /** * 返回成功消息 * * @return 成功消息 */ public static AjaxResult success() { return AjaxResult.success("操作成功"); } /** * 返回成功数据 * * @return 成功消息 */ public static AjaxResult success(T data) { return AjaxResult.success("操作成功", data); } /** * 返回成功消息 * * @param msg 返回内容 * @return 成功消息 */ public static AjaxResult success(String msg) { return AjaxResult.success(msg, null); } /** * 返回成功消息 * * @param msg 返回内容 * @param data 数据对象 * @return 成功消息 */ public static AjaxResult success(String msg, T data) { return new AjaxResult(200, msg, data); } /** * 返回错误消息 * * @return */ public static AjaxResult error() { return AjaxResult.error("操作失败"); } /** * 返回错误消息 * * @param msg 返回内容 * @return 警告消息 */ public static AjaxResult error(String msg) { return AjaxResult.error(msg, null); } /** * 返回错误消息 * * @param msg 返回内容 * @param data 数据对象 * @return 警告消息 */ public static AjaxResult error(String msg, T data) { return new AjaxResult(500, msg, data); } /** * 返回错误消息 * * @param code 状态码 * @param msg 返回内容 * @return 警告消息 */ public static AjaxResult error(int code, String msg) { return new AjaxResult(code, msg, null); } }
定义全局异常处理类
@RestControllerAdvice public class GlobalExceptionHandler { /** * 全局异常 */ @ExceptionHandler(Exception.class) public AjaxResult handleException(Exception e) { return AjaxResult.error(e.getMessage()); } }
❺ controller中登陆、测试方法
//通过数据库用户数据登陆 @PostMapping("/loginByMysql") public AjaxResult loginByMysql(@RequestBody LoginRequest request) { return AjaxResult.success(authService.authenticate(request)); } //验证权限 @PreAuthorize("hasAuthority('admin:menu:add')") @GetMapping("/add") public AjaxResult add() { return AjaxResult.success("方法的授权admin:menu:add,访问ok"); }
❻ 运行测试
访问 /loginByMysql 接口查看返回的用户数据,前端可以根据登陆接口返回决定资源权限的配置
最后切换用户测试 /add 仅配置了 admin:menu:add 允许访问
最后修改token、或者当token过期,继续请求查看是否被 AuthenticationEntryPoint 统一处理
6. 结语
至此本章节内容也就结束了,我们通过 Spring Security 与 JWT 实现无状态认证。从 JWT 的基本原理出发,逐步构建了登录接口、JWT 生成工具、拦截过滤器及安全配置的初步整合方案,最后再结合之前章节RBAC角色模型升级,完整实现了动态数据库用户的认证授权等。通过完整的代码样例,相信小伙伴们都可以快速搭建出一个高效、灵活的认证系统。
如果本文对你构建前后端分离应用提供了切实可行的解决方案,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!
-