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>17java.version>
        <mybatisplus.version>3.5.9mybatisplus.version>
        <knife4j.version>4.5.0knife4j.version>
        <hutool.version>5.8.26hutool.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-cacheartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>com.mysqlgroupId>
            <artifactId>mysql-connector-jartifactId>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-testartifactId>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-spring-boot3-starterartifactId>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-jsqlparserartifactId>
        dependency>
        
        <dependency>
            <groupId>com.github.xiaoymingroupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starterartifactId>
            <version>${knife4j.version}version>
        dependency>
        
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>${hutool.version}version>
        dependency>
    dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-bomartifactId>
                <version>${mybatisplus.version}version>
                <type>pomtype>
                <scope>importscope>
            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.(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 roles = sysUserRoleMapper.listRoleKeyByUserId(userId);
        // 获取数据范围标识
        Integer dataScope = sysUserRoleMapper.getMaximumDataScope(roles);
        Set 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 login(@RequestBody SysUserLoginParam sysUserLoginParam) {
        return R.success(sysUserService.login(sysUserLoginParam.getUsername(), sysUserLoginParam.getPassword()));
    }
    @Operation(summary = "info 获取当前用户信息")
    @GetMapping(value = "/info")
    public R 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 获取源码地址。

目录
相关文章
|
11天前
|
人工智能 Java 机器人
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
Spring AI Alibaba集成Ollama,基于Java构建本地大模型应用,支持流式对话、knife4j接口可视化,实现高隐私、免API密钥的离线AI服务。
267 1
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
存储 JSON Java
188 0
|
18天前
|
数据可视化 Java BI
将 Spring 微服务与 BI 工具集成:最佳实践
本文探讨了 Spring 微服务与商业智能(BI)工具集成的潜力与实践。随着微服务架构和数据分析需求的增长,Spring Boot 和 Spring Cloud 提供了构建可扩展、弹性服务的框架,而 BI 工具则增强了数据可视化与实时分析能力。文章介绍了 Spring 微服务的核心概念、BI 工具在企业中的作用,并深入分析了两者集成带来的优势,如实时数据处理、个性化报告、数据聚合与安全保障。同时,文中还总结了集成过程中的最佳实践,包括事件驱动架构、集中配置管理、数据安全控制、模块化设计与持续优化策略,旨在帮助企业构建高效、智能的数据驱动系统。
将 Spring 微服务与 BI 工具集成:最佳实践
|
20天前
|
监控 Cloud Native Java
Spring Integration 企业集成模式技术详解与实践指南
本文档全面介绍 Spring Integration 框架的核心概念、架构设计和实际应用。作为 Spring 生态系统中的企业集成解决方案,Spring Integration 基于著名的 Enterprise Integration Patterns(EIP)提供了轻量级的消息驱动架构。本文将深入探讨其消息通道、端点、过滤器、转换器等核心组件,以及如何构建可靠的企业集成解决方案。
80 0
|
1月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
367 3
|
2月前
|
XML 人工智能 Java
Spring Boot集成Aviator实现参数校验
Aviator是一个高性能、轻量级的Java表达式求值引擎,适用于动态表达式计算。其特点包括支持多种运算符、函数调用、正则匹配、自动类型转换及嵌套变量访问,性能优异且依赖小。适用于规则引擎、公式计算和动态脚本控制等场景。本文介绍了如何结合Aviator与AOP实现参数校验,并附有代码示例和仓库链接。
163 0
|
2月前
|
Java 关系型数据库 数据库连接
Spring Boot项目集成MyBatis Plus操作PostgreSQL全解析
集成 Spring Boot、PostgreSQL 和 MyBatis Plus 的步骤与 MyBatis 类似,只不过在 MyBatis Plus 中提供了更多的便利功能,如自动生成 SQL、分页查询、Wrapper 查询等。
267 3