开发者

SpringBoot3集成SpringSecurity+JWT的实现

开发者 https://www.devze.com 2025-07-27 11:26 出处:网络 作者: Harry技术
目录准备工作引入依赖设计表结构生成基本代码白名单配置JWT配置配置参数jwt密码、过期时间等自定义未授权和未登录结果返回创建JWT过滤器改写SecurityConfig登录验证启动查看接口未登录登录总结准备工作
目录
  • 准备工作
  • 引入依赖
    • 设计表结构
    • 生成基本代码
    • 白名单配置
  • JWT配置
    • 配置参数jwt密码、过期时间等
    • 自定义未授权和未登录结果返回
    • 创建JWT过滤器
  • 改写SecurityConfig
    • 登录验证
      • 启动查看接口
      • 未登录
      • 登录
    • 总结

      准备工作

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

      引入依赖

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

      SpringBoot3集成SpringSecurity+JWT的实现

      <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 用户角色关系表

      生成基本代码

      SpringBoot3集成SpringSecurity+JWT的实现

      白名单配置

      因为我们这里引入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/**rnRKGPuNJk
          - /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 JphpwtProperty {
      
              /**
               * 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 sysUserwww.devze.comRoleMapper;
      
          @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) {
      
        rnRKGPuNJk      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

      SpringBoot3集成SpringSecurity+JWT的实现

      SpringBoot3集成SpringSecurity+JWT的实现

      未登录

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

      SpringBoot3集成SpringSecurity+JWT的实现

      登录

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

      SpringBoot3集成SpringSecurity+JWT的实现

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

      SpringBoot3集成SpringSecurity+JWT的实现

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

      SpringBoot3集成SpringSecurity+JWT的实现

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

      SpringBoot3集成SpringSecurity+JWT的实现

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

      SpringBoot3集成SpringSecurity+JWT的实现

      总结

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

      到此这篇关于SpringBoot3集成SpringSecurity+JWT的实现的文章就介绍到这了,更多相关SpringBoot3集成SpringSecurity+JWT内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

      0

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      关注公众号