SpringBoot3集成SpringSecurity+JWT的實(shí)現(xiàn)
準(zhǔn)備工作
概述: 在本文中,我們將一步步學(xué)習(xí)如何使用 Spring Boot 3 和 Spring Security 來(lái)保護(hù)我們的應(yīng)用程序。我們將從簡(jiǎn)單的入門開始,然后逐漸引入數(shù)據(jù)庫(kù),并最終使用 JWT 實(shí)現(xiàn)前后端分離。
引入依賴
這里主要用到了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工具類庫(kù) 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過濾器時(shí),打開swagger會(huì)報(bào)錯(cuò):jakarta.servlet.ServletException: Handler dispatch failed: java.lang.NoSuchMethodError: 'void org.springframework.web.method.ControllerAdviceBean.<init>(java.lang.Object) ,說(shuō)是版本兼容問題。暫時(shí)沒有找到很好的解決方案,所以給Spring boot版本降至3.3.5。
設(shè)計(jì)表結(jié)構(gòu)
關(guān)于表結(jié)構(gòu)內(nèi)容我這里不詳細(xì)的說(shuō)了,各個(gè)表字段內(nèi)容,可以拉一下代碼,獲取表結(jié)構(gòu)sql腳本。關(guān)注公眾號(hào):“Harry技術(shù)”,回復(fù)“jwt”,即可獲取到整個(gè)項(xiàng)目源碼以及表結(jié)構(gòu)。
sys_config 系統(tǒng)配置表 sys_dept 部門表 sys_dict 字典表 sys_dict_data 字典數(shù)據(jù)表 sys_menu 菜單表 sys_role 角色表 sys_role_menu 角色菜單關(guān)系表 sys_user 用戶表 sys_user_role 用戶角色關(guān)系表
生成基本代碼
白名單配置
因?yàn)槲覀冞@里引入knife4j ,關(guān)于knife4j 的相關(guān)配置可以參考《Spring Boot 3 整合Knife4j(OpenAPI3規(guī)范)》,我們需要將以下接口加入到白名單
# 白名單列表 ignore-urls: - /v3/api-docs/** - /doc.html - /swagger-resources/** - /webjars/** - /swagger-ui/** - /swagger-ui.html
JWT配置
JWT(JSON Web Token)相關(guān)資料網(wǎng)絡(luò)上非常多,可以自行搜索,簡(jiǎn)單點(diǎn)說(shuō)JWT就是一種網(wǎng)絡(luò)身份認(rèn)證和信息交換格式。
Header
頭部信息,主要聲明了JWT的簽名算法等信息Payload
載荷信息,主要承載了各種聲明并傳遞明文數(shù)據(jù)Signature
簽名,擁有該部分的JWT被稱為JWS,也就是簽了名的JWT,用于校驗(yàn)數(shù)據(jù)
整體結(jié)構(gòu)是:
header.payload.signature
配置參數(shù)jwt密碼、過期時(shí)間等
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
創(chuàng)建SecurityProperties
/** * Security Properties * * @author harry * @公眾號(hào) Harry技術(shù) */ @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 過期時(shí)間 */ private Long ttl; } }
自定義未授權(quán)和未登錄結(jié)果返回
在之前的案例中沒有自定義未授權(quán)和未登錄,直接在頁(yè)面上顯示錯(cuò)誤信息,這樣對(duì)于前端來(lái)說(shuō)不是很好處理,我們將所有接口按照一定的格式返回,會(huì)方便前端交互處理。
未登錄
/** * 當(dāng)未登錄或者token失效訪問接口時(shí),自定義的返回結(jié)果 * * @author harry * @公眾號(hào) Harry技術(shù) */ @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(); } }
未授權(quán)
/** * 當(dāng)訪問接口沒有權(quán)限時(shí),自定義的返回結(jié)果 * * @author harry * @公眾號(hào) Harry技術(shù) */ @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(); } }
創(chuàng)建JWT過濾器
這里直接使用了Hutool-jwt提供的JWTUtil工具類,主要包括:JWT創(chuàng)建、JWT解析、JWT驗(yàn)證。
/** * JWT登錄授權(quán)過濾器 * * @author harry * @公眾號(hào) Harry技術(shù) */ @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 { // 獲取請(qǐng)求token String token = request.getHeader(HttpHeaders.AUTHORIZATION); try { // 如果請(qǐng)求頭中沒有Authorization信息,或者Authorization以Bearer開頭,則認(rèn)為是匿名用戶 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 是否有效(驗(yàn)簽 + 是否過期) 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有效或無(wú)Token時(shí)繼續(xù)執(zhí)行過濾鏈 chain.doFilter(request, response); } }
改寫SecurityConfig
關(guān)于Spring Boot 3 集成 Spring Security相關(guān)的知識(shí)點(diǎn),可以參考文章:《Spring Boot 3 集成 Spring Security(1)認(rèn)證》、《Spring Boot 3 集成 Spring Security(2)授權(quán)》、《Spring Boot 3 集成 Spring Security(3)數(shù)據(jù)管理》。
/** * Spring Security 權(quán)限配置 * * @author harry * @公眾號(hào) Harry技術(shù) */ @Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) // 開啟方法級(jí)別的權(quán)限控制 @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)); // 添加自定義未授權(quán)和未登錄結(jié)果返回 http.exceptionHandling(customizer -> customizer // 處理未授權(quán) .accessDeniedHandler(restfulAccessDeniedHandler) // 處理未登錄 .authenticationEntryPoint(restAuthenticationEntryPoint)); // JWT 校驗(yàn)過濾器 http.addFilterBefore(new JwtValidationFilter(userDetailsService, securityProperties.getJwt().getKey()), UsernamePasswordAuthenticationFilter.class); return http.build(); } /** * AuthenticationManager 手動(dòng)注入 * * @param authenticationConfiguration 認(rèn)證配置 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } /** * 強(qiáng)散列哈希加密實(shí)現(xiàn) */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
這里主要做了以下幾點(diǎn)配置:
- 將不需要認(rèn)證鑒權(quán)的接口加入白名單
- 由于使用的是JWT,我們這里不需要csrf、禁用session
- 添加自定義未授權(quán)和未登錄結(jié)果返回
- 配置 JWT 校驗(yàn)過濾器
我們根據(jù)數(shù)據(jù)庫(kù)中的用戶信息加載用戶,并將角色轉(zhuǎn)換為 Spring Security 能識(shí)別的格式。我們寫一個(gè)SysUserDetails
類來(lái)實(shí)現(xiàn)自定義Spring Security 用戶對(duì)象。
/** * 用戶詳情服務(wù) * * @author harry * @公眾號(hào) Harry技術(shù) */ @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); // 獲取數(shù)據(jù)范圍標(biāo)識(shí) Integer dataScope = sysUserRoleMapper.getMaximumDataScope(roles); Set<String> permissions = new HashSet<>(); // 如果 roles 包含 root 則擁有所有權(quán)限 if (roles.contains(CommonConstant.SUPER_ADMIN_ROOT)) { permissions.add(CommonConstant.ALL_PERMISSION); } else { // 獲取菜單權(quán)限標(biāo)識(shí) permissions = sysMenuMapper.getMenuPermission(userId); // 過濾空字符串 permissions.remove(""); } return new SysUserDetails(user, permissions, roles, username, dataScope); } }
這里使用了@Cacheable
結(jié)合redis做的緩存處理,關(guān)于緩存相關(guān)配置,可以參考文章《Spring Boot 3 整合Redis(1) 基礎(chǔ)功能》、《Spring Boot 3 整合Redis(2)注解驅(qū)動(dòng)緩存》。
登錄驗(yàn)證
寫一個(gè)登錄接口/auth/login,返回 token、tokenType等信息
/** * 登錄相關(guān) * * @author harry * @公眾號(hào) Harry技術(shù) */ @Slf4j @RestController @RequiredArgsConstructor @Tag(name = "認(rèn)證中心") @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 獲取當(dāng)前用戶信息") @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) { // 需要 將當(dāng)前用戶token 設(shè)置無(wú)效 SecurityContextHolder.clearContext(); return R.success(); } }
LoginResult 對(duì)象
/** * * @author harry * @公眾號(hào) Harry技術(shù) */ @Data public class LoginResult { @Schema(description = "token") private String token; @Schema(description = "token 類型", example = "Bearer") private String tokenType; @Schema(description = "過期時(shí)間(單位:秒)", example = "604800") private Long expiration; @Schema(description = "刷新token") private String refreshToken; }
啟動(dòng)查看接口
訪問http://localhost:8080/swagger-ui/index.html
或者http://localhost:8080/doc.html
未登錄
當(dāng)我們處于未登錄狀態(tài)時(shí)訪問/auth/info
接口,直接返回了我們自定義的異常信息
登錄
這里我們登錄用戶 harry/123456
,設(shè)定用戶角色TEST
,菜單權(quán)限不給字典相關(guān)的操作。
看到接口成功返回token等信息,我們將token信息填寫到 Authorize,作為全局配置。
這時(shí),我們?cè)L問/auth/info
,可以看到當(dāng)前登錄的用戶信息
我們?cè)L問字典相關(guān)的接口,如:/sys_dict/page
,返回了沒有相關(guān)權(quán)限的信息
訪問其他接口,如:/sys_dept/page
,可以看到數(shù)據(jù)正常返回。
總結(jié)
到這里,我們已經(jīng)掌握了Spring Boot 3 整合 Security 的全過程。我們將從簡(jiǎn)單的入門開始,然后學(xué)習(xí)如何整合數(shù)據(jù)庫(kù),并最終使用 JWT 實(shí)現(xiàn)前后端分離。這些知識(shí)將幫助我們構(gòu)建更安全、更可靠的應(yīng)用程序。后續(xù)我們會(huì)深入了解在項(xiàng)目中用到的一些其他框架、工具。讓我們一起開始吧!
到此這篇關(guān)于SpringBoot3集成SpringSecurity+JWT的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot3集成SpringSecurity+JWT內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot+Spring Security+JWT實(shí)現(xiàn)RESTful Api權(quán)限控制的方法
- SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法
- Springboot集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的步驟詳解
- SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗(yàn)
- SpringBoot+SpringSecurity+JWT實(shí)現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
- SpringBoot集成Spring security JWT實(shí)現(xiàn)接口權(quán)限認(rèn)證
- SpringBoot3.x接入Security6.x實(shí)現(xiàn)JWT認(rèn)證的完整步驟
- springboot+springsecurity+mybatis+JWT+Redis?實(shí)現(xiàn)前后端離實(shí)戰(zhàn)教程
相關(guān)文章
Java AOP動(dòng)態(tài)代理詳細(xì)介紹
AOP是一種設(shè)計(jì)思想,是軟件設(shè)計(jì)領(lǐng)域中的面向切面編程,它是面向?qū)ο缶幊痰囊环N補(bǔ)充和完善。本文將用Java實(shí)現(xiàn)AOP代理的三種方式,需要的可以參考一下2022-08-08Eureka注冊(cè)不上或注冊(cè)后IP不對(duì)(多網(wǎng)卡的坑及解決)
這篇文章主要介紹了Eureka注冊(cè)不上或注冊(cè)后IP不對(duì)(多網(wǎng)卡的坑及解決),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11Spring如何使用@Indexed加快啟動(dòng)速度
這篇文章主要介紹了Spring如何使用@Indexed加快啟動(dòng)速度,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11據(jù)說(shuō)這個(gè)是可以擼到2089年的idea2020.2(推薦)
這篇文章主要介紹了據(jù)說(shuō)這個(gè)是可以擼到2089年的idea2020.2,本教程給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09SpringBoot使用Graylog日志收集的實(shí)現(xiàn)示例
Graylog是一個(gè)生產(chǎn)級(jí)別的日志收集系統(tǒng),集成Mongo和Elasticsearch進(jìn)行日志收集,這篇文章主要介紹了SpringBoot使用Graylog日志收集的實(shí)現(xiàn)示例,感興趣的小伙伴們可以參考一下2019-04-04