最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合

06-01 1114阅读

最新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

            完整流程图如下:

            最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合


            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

            最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合

            在接下来的验证接口,配置Header 设置Authorization的Key,并将登陆获得的token设置值

            格式:Bearer +登陆获得的Token

            最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合

            至此我们就完成了最简单的 JWT+SpringSecurity 整合


            5. 基于RBAC角色模型的升级

            回忆一下我们之前第五章节中,RBAC角色模型的表设计:需要通过用户ID查询到用户分配的角色+角色所配置的菜单资源

            最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合

            这里我们就基于 最新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 接口查看返回的用户数据,前端可以根据登陆接口返回决定资源权限的配置

            最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合

            最后切换用户测试 /add 仅配置了 admin:menu:add 允许访问

            最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合

            最后修改token、或者当token过期,继续请求查看是否被 AuthenticationEntryPoint 统一处理

            最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合


            6. 结语

            至此本章节内容也就结束了,我们通过 Spring Security 与 JWT 实现无状态认证。从 JWT 的基本原理出发,逐步构建了登录接口、JWT 生成工具、拦截过滤器及安全配置的初步整合方案,最后再结合之前章节RBAC角色模型升级,完整实现了动态数据库用户的认证授权等。通过完整的代码样例,相信小伙伴们都可以快速搭建出一个高效、灵活的认证系统。

            如果本文对你构建前后端分离应用提供了切实可行的解决方案,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!


            最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码