SpringBoot3集成SpringSecurity+JWT的實現(xiàn)
準備工作
概述: 在本文中,我們將一步步學(xué)習(xí)如何使用 Spring Boot 3 和 Spring Security 來保護我們的應(yīng)用程序。我們將從簡單的入門開始,然后逐漸引入數(shù)據(jù)庫,并最終使用 JWT 實現(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過濾器時,打開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。
設(shè)計表結(jié)構(gòu)
關(guān)于表結(jié)構(gòu)內(nèi)容我這里不詳細的說了,各個表字段內(nèi)容,可以拉一下代碼,獲取表結(jié)構(gòu)sql腳本。關(guān)注公眾號:“Harry技術(shù)”,回復(fù)“jwt”,即可獲取到整個項目源碼以及表結(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)系表
生成基本代碼

白名單配置
因為我們這里引入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ò)上非常多,可以自行搜索,簡單點說JWT就是一種網(wǎng)絡(luò)身份認證和信息交換格式。
Header頭部信息,主要聲明了JWT的簽名算法等信息Payload載荷信息,主要承載了各種聲明并傳遞明文數(shù)據(jù)Signature簽名,擁有該部分的JWT被稱為JWS,也就是簽了名的JWT,用于校驗數(shù)據(jù)
整體結(jié)構(gòu)是:
header.payload.signature
配置參數(shù)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創(chuàng)建SecurityProperties
/**
* Security Properties
*
* @author harry
* @公眾號 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 過期時間
*/
private Long ttl;
}
}自定義未授權(quán)和未登錄結(jié)果返回
在之前的案例中沒有自定義未授權(quán)和未登錄,直接在頁面上顯示錯誤信息,這樣對于前端來說不是很好處理,我們將所有接口按照一定的格式返回,會方便前端交互處理。
未登錄
/**
* 當(dāng)未登錄或者token失效訪問接口時,自定義的返回結(jié)果
*
* @author harry
* @公眾號 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)限時,自定義的返回結(jié)果
*
* @author harry
* @公眾號 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驗證。
/**
* JWT登錄授權(quán)過濾器
*
* @author harry
* @公眾號 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 {
// 獲取請求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時繼續(xù)執(zhí)行過濾鏈
chain.doFilter(request, response);
}
}改寫SecurityConfig
關(guān)于Spring Boot 3 集成 Spring Security相關(guān)的知識點,可以參考文章:《Spring Boot 3 集成 Spring Security(1)認證》、《Spring Boot 3 集成 Spring Security(2)授權(quán)》、《Spring Boot 3 集成 Spring Security(3)數(shù)據(jù)管理》。
/**
* Spring Security 權(quán)限配置
*
* @author harry
* @公眾號 Harry技術(shù)
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true) // 開啟方法級別的權(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 校驗過濾器
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();
}
/**
* 強散列哈希加密實現(xiàn)
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}這里主要做了以下幾點配置:
- 將不需要認證鑒權(quán)的接口加入白名單
- 由于使用的是JWT,我們這里不需要csrf、禁用session
- 添加自定義未授權(quán)和未登錄結(jié)果返回
- 配置 JWT 校驗過濾器
我們根據(jù)數(shù)據(jù)庫中的用戶信息加載用戶,并將角色轉(zhuǎn)換為 Spring Security 能識別的格式。我們寫一個SysUserDetails類來實現(xiàn)自定義Spring Security 用戶對象。
/**
* 用戶詳情服務(wù)
*
* @author harry
* @公眾號 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)識
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)識
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ū)動緩存》。
登錄驗證
寫一個登錄接口/auth/login,返回 token、tokenType等信息
/**
* 登錄相關(guān)
*
* @author harry
* @公眾號 Harry技術(shù)
*/
@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) {
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 對象
/**
*
* @author harry
* @公眾號 Harry技術(shù)
*/
@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


未登錄
當(dāng)我們處于未登錄狀態(tài)時訪問/auth/info接口,直接返回了我們自定義的異常信息

登錄
這里我們登錄用戶 harry/123456,設(shè)定用戶角色TEST,菜單權(quán)限不給字典相關(guān)的操作。

看到接口成功返回token等信息,我們將token信息填寫到 Authorize,作為全局配置。

這時,我們訪問/auth/info,可以看到當(dāng)前登錄的用戶信息

我們訪問字典相關(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 實現(xiàn)前后端分離。這些知識將幫助我們構(gòu)建更安全、更可靠的應(yīng)用程序。后續(xù)我們會深入了解在項目中用到的一些其他框架、工具。讓我們一起開始吧!
到此這篇關(guān)于SpringBoot3集成SpringSecurity+JWT的實現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot3集成SpringSecurity+JWT內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot+Spring Security+JWT實現(xiàn)RESTful Api權(quán)限控制的方法
- SpringBoot集成Spring Security用JWT令牌實現(xiàn)登錄和鑒權(quán)的方法
- Springboot集成Spring Security實現(xiàn)JWT認證的步驟詳解
- SpringBoot3.0+SpringSecurity6.0+JWT的實現(xiàn)
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗
- SpringBoot+SpringSecurity+JWT實現(xiàn)系統(tǒng)認證與授權(quán)示例
- SpringBoot集成Spring security JWT實現(xiàn)接口權(quán)限認證
- SpringBoot3.x接入Security6.x實現(xiàn)JWT認證的完整步驟
- springboot+springsecurity+mybatis+JWT+Redis?實現(xiàn)前后端離實戰(zhàn)教程
相關(guān)文章
Eureka注冊不上或注冊后IP不對(多網(wǎng)卡的坑及解決)
這篇文章主要介紹了Eureka注冊不上或注冊后IP不對(多網(wǎng)卡的坑及解決),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11
據(jù)說這個是可以擼到2089年的idea2020.2(推薦)
這篇文章主要介紹了據(jù)說這個是可以擼到2089年的idea2020.2,本教程給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
SpringBoot使用Graylog日志收集的實現(xiàn)示例
Graylog是一個生產(chǎn)級別的日志收集系統(tǒng),集成Mongo和Elasticsearch進行日志收集,這篇文章主要介紹了SpringBoot使用Graylog日志收集的實現(xiàn)示例,感興趣的小伙伴們可以參考一下2019-04-04

