目录
- 准备工作
- 引入依赖
- 设计表结构
- 生成基本代码
- 白名单配置
- JWT配置
- 配置参数jwt密码、过期时间等
- 自定义未授权和未登录结果返回
- 创建JWT过滤器
- 改写SecurityConfig
- 登录验证
- 启动查看接口
- 未登录
- 登录
- 总结
准备工作
概述: 在本文中,我们将一步步学习如何使用 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/**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
未登录
当我们处于未登录状态时访问/auth/info
接口,直接返回了我们自定义的异常信息
登录
这里我们登录用户 harry/123456
,设定用户角色TEST
,菜单权限不给字典相关的操作。
看到接口成功返回token等信息,我们将token信息填写到 Authorize,作为全局配置。
这时,我们访问/auth/info
,可以看到当前登录的用户信息
我们访问字典相关的接口,如:/sys_dict/page
,返回了没有相关权限的信息
访问其他接口,如:/sys_dept/page
,可以看到数据正常返回。
总结
到这里,我们已经掌握了Spring Boot 3 整合 Security 的全过程。我们将从简单的入门开始,然后学习如何整合数据库,并最终使用 JWT 实现前后端分离。这些知识将帮助我们构建更安全、更可靠的应用程序。后续我们会深入了解在项目中用到的一些其他框架、工具。让我们一起开始吧!
到此这篇关于SpringBoot3集成SpringSecurity+JWT的实现的文章就介绍到这了,更多相关SpringBoot3集成SpringSecurity+JWT内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论