SpringBoot3集成SpringSecurity+JWT的實(shí)現(xiàn)
準(zhǔn)備工作
概述: 在本文中,我們將一步步學(xué)習(xí)如何使用 Spring Boot 3 和 Spring Security 來保護(hù)我們的應(yīng)用程序。我們將從簡單的入門開始,然后逐漸引入數(shù)據(jù)庫,并最終使用 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工具類庫 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) ,說是版本兼容問題。暫時(shí)沒有找到很好的解決方案,所以給Spring boot版本降至3.3.5。
設(shè)計(jì)表結(jié)構(gòu)
關(guān)于表結(jié)構(gòu)內(nèi)容我這里不詳細(xì)的說了,各個(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.htmlJWT配置
JWT(JSON Web Token)相關(guān)資料網(wǎng)絡(luò)上非常多,可以自行搜索,簡單點(diǎn)說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)和未登錄,直接在頁面上顯示錯(cuò)誤信息,這樣對(duì)于前端來說不是很好處理,我們將所有接口按照一定的格式返回,會(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有效或無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ù)庫中的用戶信息加載用戶,并將角色轉(zhuǎn)換為 Spring Security 能識(shí)別的格式。我們寫一個(gè)SysUserDetails類來實(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è)置無效
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 的全過程。我們將從簡單的入門開始,然后學(xué)習(xí)如何整合數(shù)據(jù)庫,并最終使用 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-08
Eureka注冊(cè)不上或注冊(cè)后IP不對(duì)(多網(wǎng)卡的坑及解決)
這篇文章主要介紹了Eureka注冊(cè)不上或注冊(cè)后IP不對(duì)(多網(wǎng)卡的坑及解決),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
Spring如何使用@Indexed加快啟動(dòng)速度
這篇文章主要介紹了Spring如何使用@Indexed加快啟動(dòng)速度,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
據(jù)說這個(gè)是可以擼到2089年的idea2020.2(推薦)
這篇文章主要介紹了據(jù)說這個(gè)是可以擼到2089年的idea2020.2,本教程給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
SpringBoot使用Graylog日志收集的實(shí)現(xiàn)示例
Graylog是一個(gè)生產(chǎn)級(jí)別的日志收集系統(tǒng),集成Mongo和Elasticsearch進(jìn)行日志收集,這篇文章主要介紹了SpringBoot使用Graylog日志收集的實(shí)現(xiàn)示例,感興趣的小伙伴們可以參考一下2019-04-04

