Spring Boot 3 集成 Spring Security + JWT

简介: 本文详细介绍了如何使用Spring Boot 3和Spring Security集成JWT,实现前后端分离的安全认证概述了从入门到引入数据库,再到使用JWT的完整流程。列举了项目中用到的关键依赖,如MyBatis-Plus、Hutool等。简要提及了系统配置表、部门表、字典表等表结构。使用Hutool-jwt工具类进行JWT校验。配置忽略路径、禁用CSRF、添加JWT校验过滤器等。实现登录接口,返回token等信息。

Spring Boot 3 集成 Spring Security + JWT

准备工作

概述: 在本文中,我们将一步步学习如何使用 Spring Boot 3 和 Spring Security 来保护我们的应用程序。我们将从简单的入门开始,然后逐渐引入数据库,并最终使用 JWT 实现前后端分离。

引入依赖

这里主要用到了Mybatis-plus、hutool 、knife4j ,其他依赖可以直接勾选

<properties>

       <java.version>17</java.version>

       <mybatisplus.version>3.5.9</mybatisplus.version>

       <knife4j.version>4.5.0</knife4j.version>

       <hutool.version>5.8.26</hutool.version>

   </properties>

   <dependencies>

       <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-cache</artifactId>

       </dependency>

       <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-data-redis</artifactId>

       </dependency>

       <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-security</artifactId>

       </dependency>

       <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-web</artifactId>

       </dependency>

       <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-devtools</artifactId>

           <scope>runtime</scope>

           <optional>true</optional>

       </dependency>

       <dependency>

           <groupId>com.mysql</groupId>

           <artifactId>mysql-connector-j</artifactId>

           <scope>runtime</scope>

       </dependency>

       <dependency>

           <groupId>org.projectlombok</groupId>

           <artifactId>lombok</artifactId>

           <optional>true</optional>

       </dependency>

       <dependency>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-test</artifactId>

           <scope>test</scope>

       </dependency>

       <dependency>

           <groupId>org.springframework.security</groupId>

           <artifactId>spring-security-test</artifactId>

           <scope>test</scope>

       </dependency>

       <!-- MyBatis-Plus https://baomidou.com-->

       <dependency>

           <groupId>com.baomidou</groupId>

           <artifactId>mybatis-plus-spring-boot3-starter</artifactId>

       </dependency>

       <dependency>

           <groupId>com.baomidou</groupId>

           <artifactId>mybatis-plus-jsqlparser</artifactId>

       </dependency>

       <!--Knife4j https://doc.xiaominfo.com/-->

       <dependency>

           <groupId>com.github.xiaoymin</groupId>

           <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>

           <version>${knife4j.version}</version>

       </dependency>

       <!-- Java工具类库 https://doc.hutool.cn -->

       <dependency>

           <groupId>cn.hutool</groupId>

           <artifactId>hutool-all</artifactId>

           <version>${hutool.version}</version>

       </dependency>

   </dependencies>

   <dependencyManagement>

       <dependencies>

           <dependency>

               <groupId>com.baomidou</groupId>

               <artifactId>mybatis-plus-bom</artifactId>

               <version>${mybatisplus.version}</version>

               <type>pom</type>

               <scope>import</scope>

           </dependency>

       </dependencies>

   </dependencyManagement>

我这里使用的Spring boot版本为3.3.5 ,使用3.4.0整合JWT过滤器时,打开swagger会报错:jakarta.servlet.ServletException: Handler dispatch failed: java.lang.NoSuchMethodError: 'void org.springframework.web.method.ControllerAdviceBean.<init>(java.lang.Object) ,说是版本兼容问题。暂时没有找到很好的解决方案,所以给Spring boot版本降至3.3.5

设计表结构

关于表结构内容我这里不详细的说了,各个表字段内容,可以拉一下代码,获取表结构sql脚本。关注公众号:“Harry技术”,回复“jwt”,即可获取到整个项目源码以及表结构。

sys_config 系统配置表

sys_dept 部门表

sys_dict 字典表

sys_dict_data 字典数据表

sys_menu 菜单表

sys_role 角色表

sys_role_menu 角色菜单关系表

sys_user 用户表

sys_user_role 用户角色关系表

生成基本代码

白名单配置

因为我们这里引入knife4j ,关于knife4j 的相关配置可以参考《Spring Boot 3 整合Knife4j(OpenAPI3规范)》,我们需要将以下接口加入到白名单

 # 白名单列表

 ignore-urls:

   - /v3/api-docs/**

   - /doc.html

   - /swagger-resources/**

   - /webjars/**

   - /swagger-ui/**

   - /swagger-ui.html

JWT配置

JWT(JSON Web Token)相关资料网络上非常多,可以自行搜索,简单点说JWT就是一种网络身份认证和信息交换格式。

  • Header 头部信息,主要声明了JWT的签名算法等信息
  • Payload 载荷信息,主要承载了各种声明并传递明文数据
  • Signature 签名,拥有该部分的JWT被称为JWS,也就是签了名的JWT,用于校验数据

整体结构是:

header.payload.signature

配置参数jwt密码、过期时间等

  • yml 配置

# 安全配置

security:

 jwt:

   # JWT 秘钥

   key: www.tech-harry.cn

   # JWT 有效期(单位:秒)

   ttl: 7200

 # 白名单列表

 ignore-urls:

   - /v3/api-docs/**

   - /doc.html

   - /swagger-resources/**

   - /webjars/**

   - /swagger-ui/**

   - /swagger-ui.html

   - /auth/login

  • 创建SecurityProperties

/**

* Security Properties

*

* @author harry

* @公众号 Harry技术

*/

@Data

@ConfigurationProperties(prefix = "security")

public class SecurityProperties {

   /**

    * 白名单 URL 集合

    */

   private List<String> ignoreUrls;

   /**

    * JWT 配置

    */

   private JwtProperty jwt;

   /**

    * JWT 配置

    */

   @Data

   public static class JwtProperty {

       /**

        * JWT 密钥

        */

       private String key;

       /**

        * JWT 过期时间

        */

       private Long ttl;

   }

}

自定义未授权和未登录结果返回

在之前的案例中没有自定义未授权和未登录,直接在页面上显示错误信息,这样对于前端来说不是很好处理,我们将所有接口按照一定的格式返回,会方便前端交互处理。

  • 未登录

/**

* 当未登录或者token失效访问接口时,自定义的返回结果

*

* @author harry

* @公众号 Harry技术

*/

@Component

public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

   @Override

   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {

       response.setCharacterEncoding("UTF-8");

       response.setContentType("application/json");

       response.getWriter().println(JSONUtil.toJsonStr(R.unauthorized(authException.getMessage())));

       response.getWriter().flush();

   }

}

  • 未授权

/**

* 当访问接口没有权限时,自定义的返回结果

*

* @author harry

* @公众号 Harry技术

*/

@Component

public class RestfulAccessDeniedHandler implements AccessDeniedHandler {

   @Override

   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {

       response.setCharacterEncoding("UTF-8");

       response.setContentType("application/json");

       response.getWriter().println(JSONUtil.toJsonStr(R.forbidden(e.getMessage())));

       response.getWriter().flush();

   }

}

创建JWT过滤器

这里直接使用了Hutool-jwt提供的JWTUtil工具类,主要包括:JWT创建、JWT解析、JWT验证。

/**

* JWT登录授权过滤器

*

* @author harry

* @公众号 Harry技术

*/

@Slf4j

public class JwtValidationFilter extends OncePerRequestFilter {


   private final UserDetailsService userDetailsService;


   // 密钥

   private final byte[] secretKey;


   public JwtValidationFilter(UserDetailsService userDetailsService, String secretKey) {

       this.userDetailsService = userDetailsService;

       this.secretKey = secretKey.getBytes();

   }


   @Override

   protected void doFilterInternal(HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull FilterChain chain) throws ServletException, IOException {

       // 获取请求token

       String token = request.getHeader(HttpHeaders.AUTHORIZATION);

       try {

           // 如果请求头中没有Authorization信息,或者Authorization以Bearer开头,则认为是匿名用户

           if (StrUtil.isBlank(token) || !token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {

               chain.doFilter(request, response);

               return;

           }


           // 去除 Bearer 前缀

           token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());

           // 解析 Token

           JWT jwt = JWTUtil.parseToken(token);


           // 检查 Token 是否有效(验签 + 是否过期)

           boolean isValidate = jwt.setKey(secretKey).validate(0);

           if (!isValidate) {

               log.error("JwtValidationFilter error: token is invalid");

               throw new ApiException(ResultCode.UNAUTHORIZED);

           }

           JSONObject payloads = jwt.getPayloads();

           String username = payloads.getStr(JWTPayload.SUBJECT);

           SysUserDetails userDetails = (SysUserDetails) this.userDetailsService.loadUserByUsername(username);


           UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

           authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

           SecurityContextHolder.getContext().setAuthentication(authentication);


       } catch (Exception e) {

           log.error("JwtValidationFilter error: {}", e.getMessage());

           SecurityContextHolder.clearContext();

           throw new ApiException(ResultCode.UNAUTHORIZED);

       }

       // Token有效或无Token时继续执行过滤链

       chain.doFilter(request, response);

   }

}

改写SecurityConfig

关于Spring Boot 3 集成 Spring Security相关的知识点,可以参考文章:《Spring Boot 3 集成 Spring Security(1)认证》、《Spring Boot 3 集成 Spring Security(2)授权》、《Spring Boot 3 集成 Spring Security(3)数据管理》。

/**

* Spring Security 权限配置

*

* @author harry

* @公众号 Harry技术

*/

@Configuration

@EnableWebSecurity

@EnableMethodSecurity(securedEnabled = true) // 开启方法级别的权限控制

@RequiredArgsConstructor

public class SecurityConfig {


   private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;

   private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;

   private final SecurityProperties securityProperties;

   private final UserDetailsService userDetailsService;


   @Bean

   protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {


       // 忽略的路径

       http.authorizeHttpRequests(requestMatcherRegistry -> requestMatcherRegistry.requestMatchers(

                       securityProperties.getIgnoreUrls().toArray(new String[0])).permitAll()

               .anyRequest().authenticated()

       );


       http

               // 由于使用的是JWT,我们这里不需要csrf

               .csrf(AbstractHttpConfigurer::disable)

               // 禁用session

               .sessionManagement(configurer ->

                       configurer

                               .sessionCreationPolicy(SessionCreationPolicy.STATELESS));


       // 添加自定义未授权和未登录结果返回

       http.exceptionHandling(customizer ->

               customizer

                       // 处理未授权

                       .accessDeniedHandler(restfulAccessDeniedHandler)

                       // 处理未登录

                       .authenticationEntryPoint(restAuthenticationEntryPoint));

       // JWT 校验过滤器

       http.addFilterBefore(new JwtValidationFilter(userDetailsService, securityProperties.getJwt().getKey()), UsernamePasswordAuthenticationFilter.class);


       return http.build();

   }


   /**

    * AuthenticationManager 手动注入

    *

    * @param authenticationConfiguration 认证配置

    */

   @Bean

   public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {

       return authenticationConfiguration.getAuthenticationManager();

   }


   /**

    * 强散列哈希加密实现

    */

   @Bean

   public PasswordEncoder passwordEncoder() {

       return new BCryptPasswordEncoder();

   }

}

这里主要做了以下几点配置:

  • 将不需要认证鉴权的接口加入白名单
  • 由于使用的是JWT,我们这里不需要csrf、禁用session
  • 添加自定义未授权和未登录结果返回
  • 配置 JWT 校验过滤器

我们根据数据库中的用户信息加载用户,并将角色转换为 Spring Security 能识别的格式。我们写一个SysUserDetails类来实现自定义Spring Security 用户对象。

/**

* 用户详情服务

*

* @author harry

* @公众号 Harry技术

*/

@Service

@RequiredArgsConstructor

public class UserDetailsServiceImpl implements UserDetailsService {


   private final SysUserMapper sysUserMapper;

   private final SysMenuMapper sysMenuMapper;

   private final SysUserRoleMapper sysUserRoleMapper;


   @Override

   @Cacheable(value = CacheConstants.USER_DETAILS, key = "#username", unless = "#result == null ")

   public SysUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


       // 获取登录用户信息

       SysUser user = sysUserMapper.selectByUsername(username);


       // 用户不存在

       if (BeanUtil.isEmpty(user)) {

           throw new ApiException(SysExceptionEnum.USER_NOT_EXIST);

       }

       Long userId = user.getUserId();


       // 用户停用

       if (StatusEnums.DISABLE.getKey().equals(user.getStatus())) {

           throw new ApiException(SysExceptionEnum.USER_DISABLED);

       }


       // 获取角色

       Set<String> roles = sysUserRoleMapper.listRoleKeyByUserId(userId);


       // 获取数据范围标识

       Integer dataScope = sysUserRoleMapper.getMaximumDataScope(roles);


       Set<String> permissions = new HashSet<>();

       // 如果 roles 包含 root 则拥有所有权限

       if (roles.contains(CommonConstant.SUPER_ADMIN_ROOT)) {

           permissions.add(CommonConstant.ALL_PERMISSION);

       } else {

           // 获取菜单权限标识

           permissions = sysMenuMapper.getMenuPermission(userId);

           // 过滤空字符串

           permissions.remove("");

       }


       return new SysUserDetails(user, permissions, roles, username, dataScope);

   }


}

这里使用了@Cacheable结合redis做的缓存处理,关于缓存相关配置,可以参考文章《Spring Boot 3 整合Redis(1) 基础功能》、《Spring Boot 3 整合Redis(2)注解驱动缓存》。

登录验证

  • 写一个登录接口/auth/login,返回 token、tokenType等信息

/**

* 登录相关

*

* @author harry

* @公众号 Harry技术

*/

@Slf4j

@RestController

@RequiredArgsConstructor

@Tag(name = "认证中心")

@RequestMapping("/auth")

public class LoginController {


   private final SysUserService sysUserService;


   @Operation(summary = "login 登录")

   @PostMapping(value = "/login")

   public R<LoginResult> login(@RequestBody SysUserLoginParam sysUserLoginParam) {


       return R.success(sysUserService.login(sysUserLoginParam.getUsername(), sysUserLoginParam.getPassword()));

   }


   @Operation(summary = "info 获取当前用户信息")

   @GetMapping(value = "/info")

   public R<UserInfoResult> getInfo() {

       UserInfoResult result = sysUserService.getInfo();

       return R.success(result);

   }


   @Operation(summary = "logout 注销")

   @PostMapping(value = "/logout")

   public R logout(HttpServletRequest request) {

       // 需要 将当前用户token 设置无效

       SecurityContextHolder.clearContext();

       return R.success();

   }


}

  • LoginResult 对象

 /**

  *

  * @author harry

  * @公众号 Harry技术

  */

 @Data

 public class LoginResult {

 

     @Schema(description = "token")

     private String token;

 

     @Schema(description = "token 类型", example = "Bearer")

     private String tokenType;

 

     @Schema(description = "过期时间(单位:秒)", example = "604800")

     private Long expiration;

 

     @Schema(description = "刷新token")

     private String refreshToken;

 

 }


启动查看接口

访问http://localhost:8080/swagger-ui/index.html或者http://localhost:8080/doc.html

未登录

当我们处于未登录状态时访问/auth/info接口,直接返回了我们自定义的异常信息

登录

这里我们登录用户 harry/123456,设定用户角色TEST,菜单权限不给字典相关的操作。

看到接口成功返回token等信息,我们将token信息填写到 Authorize,作为全局配置。

这时,我们访问/auth/info,可以看到当前登录的用户信息

我们访问字典相关的接口,如:/sys_dict/page,返回了没有相关权限的信息

访问其他接口,如:/sys_dept/page,可以看到数据正常返回。

总结

到这里,我们已经掌握了Spring Boot 3 整合 Security 的全过程。我们将从简单的入门开始,然后学习如何整合数据库,并最终使用 JWT 实现前后端分离。这些知识将帮助我们构建更安全、更可靠的应用程序。后续我们会深入了解在项目中用到的一些其他框架、工具。让我们一起开始吧!

示例源码:关注公众号“Harry技术”,回复 jwt 获取源码地址。

目录
相关文章
|
5天前
|
Java 关系型数据库 MySQL
SpringBoot 通过集成 Flink CDC 来实时追踪 MySql 数据变动
通过详细的步骤和示例代码,您可以在 SpringBoot 项目中成功集成 Flink CDC,并实时追踪 MySQL 数据库的变动。
79 43
|
7天前
|
监控 前端开发 Java
SpringBoot集成Tomcat、DispatcherServlet
通过这些配置,您可以充分利用 Spring Boot 内置的功能,快速构建和优化您的 Web 应用。
43 21
|
14天前
|
监控 Java 应用服务中间件
SpringBoot是如何简化Spring开发的,以及SpringBoot的特性以及源码分析
Spring Boot 通过简化配置、自动配置和嵌入式服务器等特性,大大简化了 Spring 应用的开发过程。它通过提供一系列 `starter` 依赖和开箱即用的默认配置,使开发者能够更专注于业务逻辑而非繁琐的配置。Spring Boot 的自动配置机制和强大的 Actuator 功能进一步提升了开发效率和应用的可维护性。通过对其源码的分析,可以更深入地理解其内部工作机制,从而更好地利用其特性进行开发。
34 6
|
25天前
|
监控 Java Nacos
使用Spring Boot集成Nacos
通过上述步骤,Spring Boot应用可以成功集成Nacos,利用Nacos的服务发现和配置管理功能来提升微服务架构的灵活性和可维护性。通过这种集成,开发者可以更高效地管理和部署微服务。
159 17
|
25天前
|
XML JavaScript Java
SpringBoot集成Shiro权限+Jwt认证
本文主要描述如何快速基于SpringBoot 2.5.X版本集成Shiro+JWT框架,让大家快速实现无状态登陆和接口权限认证主体框架,具体业务细节未实现,大家按照实际项目补充。
68 11
|
Java 数据库连接 数据库
|
20天前
|
XML Java 应用服务中间件
Spring Boot 两种部署到服务器的方式
本文介绍了Spring Boot项目的两种部署方式:jar包和war包。Jar包方式使用内置Tomcat,只需配置JDK 1.8及以上环境,通过`nohup java -jar`命令后台运行,并开放服务器端口即可访问。War包则需将项目打包后放入外部Tomcat的webapps目录,修改启动类继承`SpringBootServletInitializer`并调整pom.xml中的打包类型为war,最后启动Tomcat访问应用。两者各有优劣,jar包更简单便捷,而war包适合传统部署场景。需要注意的是,war包部署时,内置Tomcat的端口配置不会生效。
155 17
Spring Boot 两种部署到服务器的方式
|
20天前
|
Dart 前端开发 JavaScript
springboot自动配置原理
Spring Boot 自动配置原理:通过 `@EnableAutoConfiguration` 开启自动配置,扫描 `META-INF/spring.factories` 下的配置类,省去手动编写配置文件。使用 `@ConditionalXXX` 注解判断配置类是否生效,导入对应的 starter 后自动配置生效。通过 `@EnableConfigurationProperties` 加载配置属性,默认值与配置文件中的值结合使用。总结来说,Spring Boot 通过这些机制简化了开发配置流程,提升了开发效率。
55 17
springboot自动配置原理
|
2月前
|
Java 数据库连接 Maven
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
|
1月前
|
Java 测试技术 应用服务中间件
Spring Boot 如何测试打包部署
本文介绍了 Spring Boot 项目的开发、调试、打包及投产上线的全流程。主要内容包括: 1. **单元测试**:通过添加 `spring-boot-starter-test` 包,使用 `@RunWith(SpringRunner.class)` 和 `@SpringBootTest` 注解进行测试类开发。 2. **集成测试**:支持热部署,通过添加 `spring-boot-devtools` 实现代码修改后自动重启。 3. **投产上线**:提供两种部署方案,一是打包成 jar 包直接运行,二是打包成 war 包部署到 Tomcat 服务器。
47 10