SpringBoot+SpringSecurity+JWT實(shí)現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
1. Spring Security簡(jiǎn)介
Spring Security是Spring的一個(gè)核心項(xiàng)目,它是一個(gè)功能強(qiáng)大且高度可定制的認(rèn)證和訪問(wèn)控制框架。它提供了認(rèn)證和授權(quán)功能以及抵御常見(jiàn)的攻擊,它已經(jīng)成為保護(hù)基于spring的應(yīng)用程序的事實(shí)標(biāo)準(zhǔn)。
Spring Boot提供了自動(dòng)配置,引入starter依賴即可使用。
Spring Security特性總結(jié):
- 使用簡(jiǎn)單,提供Spring Boot starter依賴,極易與Spring Boot項(xiàng)目整合。
- 專業(yè),提供CSRF防護(hù)、點(diǎn)擊劫持防護(hù)、XSS防護(hù)等,并提供各種安全頭整合(X-XSS-Protection,X-Frame-Options等)。
- 密碼加密存儲(chǔ),支持多種加密算法
- 擴(kuò)展性和可定制性極強(qiáng)
- OAuth2 JWT認(rèn)證支持
- … …
2. JWT簡(jiǎn)介
JWT(Json web token),是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開(kāi)放標(biāo)準(zhǔn)(RFC 7519).該token被設(shè)計(jì)為緊湊且安全的,特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景。JWT的聲明一般被用來(lái)在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息(例如,權(quán)限信息)。一旦用戶被授予token,用戶即可通過(guò)該token訪問(wèn)服務(wù)器上的資源。
https://jwt.io/,該網(wǎng)站提供了一個(gè)debuggr,便于初學(xué)者學(xué)習(xí)理解JWT。
3. Spring Boot整合Spring Security
注意本篇文章演示使用JDK和Spring Boot的版本如下:
Spring Boot:2.7.2
JDK:11
不同的Spring Boot版本配置不同,但是原理相同。
在Spring Boot項(xiàng)目的pom.xml文件中加入下面的依賴:
<!-- Spring Security的Spring boot starter,引入后將自動(dòng)啟動(dòng)Spring Security的自動(dòng)配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 下面的依賴包含了OAuth2 JWT認(rèn)證實(shí)現(xiàn) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
以上兩個(gè)依賴即可。
4. 配置Spring Security使用JWT認(rèn)證
注意: 不同的Spring Boot版本配置不同,但是原理相同,本文使用的是Spring Boot:2.7.2。
主要是配置HttpSecurity Bean生成SecurityFilterBean,配置如下:
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
/**
* Spring Security 配置
*
* @author cloudgyb
* @since 2022/7/30 18:31
*/
@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
public class WebSecurityConfigurer {
//使用RSA對(duì)JWT做簽名,所以這里需要一對(duì)秘鑰。
//秘鑰文件的路徑在application.yml文件中做了配置(具體配置在下面)。
@Value("${jwt.public.key}")
private RSAPublicKey key;
@Value("${jwt.private.key}")
private RSAPrivateKey priv;
/**
* 構(gòu)建SecurityFilterChain bean
*/
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
//"/login"是系統(tǒng)的登錄接口,所以需要匿名可訪問(wèn)
http.authorizeRequests().antMatchers("/login").anonymous();
//其他請(qǐng)求都需認(rèn)證后才能訪問(wèn)
http.authorizeRequests().anyRequest().authenticated()
.and()
//采用JWT認(rèn)證無(wú)需session保持,所以禁用掉session管理器
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
//login接口可能來(lái)自其他站點(diǎn),所以對(duì)login不做csrf防護(hù)
.csrf((csrf) -> csrf.ignoringAntMatchers("/login"))
//配置認(rèn)證方式為JWT,并且配置了一個(gè)JWT認(rèn)證裝換器,用于去掉解析權(quán)限時(shí)的SCOOP_前綴
.oauth2ResourceServer().jwt().jwtAuthenticationConverter(
JwtAuthenticationConverter()
);
//配置認(rèn)證失敗或者無(wú)權(quán)限時(shí)的處理器
http.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
);
//根據(jù)配置生成SecurityFilterChain對(duì)象
return http.build();
}
/**
* JWT解碼器,用于認(rèn)證時(shí)的JWT解碼
*/
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
/**
* JWT編碼器,生成JWT
*/
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
/**
* JWT認(rèn)證解碼時(shí),去掉Spring Security對(duì)權(quán)限附帶的默認(rèn)前綴SCOOP_
*/
@Bean
JwtAuthenticationConverter JwtAuthenticationConverter() {
final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
application.yml
jwt: private.key: classpath:app.key public.key: classpath:app.pub
上邊的配置需要在Spring Boot項(xiàng)目的Resource目錄下生成一對(duì)RSA秘鑰。
可以使用下面的網(wǎng)站進(jìn)行生成:http://tools.jb51.net/password/rsa_encode/,注意: 密鑰格式使用 PKCS#8,私鑰密碼為空。
還有一點(diǎn)需要說(shuō)明,我在代碼中使用了Spring Boot的值注入:
@Value("${jwt.public.key}")
private RSAPublicKey key;
@Value("${jwt.private.key}")
private RSAPrivateKey priv;
有沒(méi)有很好奇Spring Boot是如何將yaml文件中的字符串對(duì)應(yīng)的文件轉(zhuǎn)換為RSAPublicKey和RSAPrivateKey ?
其實(shí)是Spring Security幫我們做了處理,在Spring Security中幫我們實(shí)現(xiàn)了一個(gè)轉(zhuǎn)換器ResourceKeyConverterAdapter,具體可以閱讀相關(guān)源碼來(lái)更深入的了解。
至此我們的項(xiàng)目已經(jīng)支持JWT認(rèn)證了。
但是用戶需要在請(qǐng)求頭Authorization中攜帶合法的JWT才能通過(guò)認(rèn)證,進(jìn)而訪問(wèn)服務(wù)器資源,那么如何給用戶頒發(fā)一個(gè)合法的JWT呢?
很簡(jiǎn)單,可以提供一個(gè)登錄接口,讓用戶輸入用戶名和密碼,匹配成功后頒發(fā)令牌即可。
其實(shí)并不是必須這樣做,還有其他方式,比如我們調(diào)用第三方接口,我們經(jīng)常的做法是先去第三方申請(qǐng),申請(qǐng)通過(guò)后我們就可以得到一個(gè)令牌。這個(gè)過(guò)程和上面的登錄通過(guò)后頒發(fā)一個(gè)令牌是一樣的,都是通過(guò)合法的途徑獲得一個(gè)令牌!
5. 實(shí)現(xiàn)登錄接口
登錄接口只有一個(gè)目的,就是給合法用戶頒發(fā)令牌!
登錄API接口:
@RestController
public class SysLoginController {
private final SysLoginService sysLoginService;
public SysLoginController(SysLoginService sysLoginService) {
this.sysLoginService = sysLoginService;
}
@PostMapping("/login")
public String login(@RequestBody LoginInfo loginInfo) {
return sysLoginService.login(loginInfo);
}
}
登錄邏輯實(shí)現(xiàn):
@Service
public class SysLoginService {
private final JwtEncoder jwtEncoder;
private final SpringSecurityUserDetailsService springSecurityUserDetailsService;
public SysLoginService(JwtEncoder jwtEncoder, SpringSecurityUserDetailsService springSecurityUserDetailsService) {
this.jwtEncoder = jwtEncoder;
this.springSecurityUserDetailsService = springSecurityUserDetailsService;
}
public String login(LoginInfo loginInfo) {
//從用戶信息存儲(chǔ)庫(kù)中獲取用戶信息
final UserDetails userDetails = springSecurityUserDetailsService.loadUserByUsername(loginInfo.getUsername());
final String password = userDetails.getPassword();
//匹配密碼,匹配成功生成JWT令牌
if (password.equals(loginInfo.getPassword())) {
return generateToken(userDetails);
}
//密碼不匹配,拋出異常,Spring Security發(fā)現(xiàn)拋出該異常后會(huì)將http響應(yīng)狀態(tài)碼設(shè)置為401 unauthorized
throw new BadCredentialsException("密碼錯(cuò)誤!");
}
private String generateToken(UserDetails userDetails) {
Instant now = Instant.now();
//JWT過(guò)期時(shí)間為36000秒,也就是600分鐘,10小時(shí)
long expiry = 36000L;
String scope = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
//將用戶權(quán)限信息使用空格分割拼為字符串,放到JWT的payload的scope字段中,注意不要改變scope這個(gè)屬性,這是Spring Security OAuth2 JWT默認(rèn)處理方式,在JWT解碼時(shí)需要讀取該字段,轉(zhuǎn)為用戶的權(quán)限信息!
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plusSeconds(expiry))
.subject(userDetails.getUsername())
.claim("scope", scope)
.build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
其他非核心代碼這里就不貼出來(lái)了,我將代碼放到github上了,具體可以轉(zhuǎn)到https://github.com/cloudgyb/spring-security-study-jwt。
6. 測(cè)試
使用postman測(cè)試一下:
使用錯(cuò)誤的密碼,會(huì)返回401 Unauthorized的狀態(tài)碼,表示我們認(rèn)證失敗!

使用正確的用戶名和密碼:

返回了JWT令牌。
此時(shí)客戶端拿到了合法的令牌,接下來(lái)就可以訪問(wèn)服務(wù)器上有權(quán)訪問(wèn)的資源了。
我寫(xiě)了一個(gè)測(cè)試接口:
@RestController
public class HelloController {
@GetMapping("/")
@PreAuthorize("hasAuthority('test')")
public String hello(Authentication authentication) {
return "Hello, " + authentication.getName() + "!";
}
}
該接口需要用戶擁有"test"的權(quán)限,但是登錄用戶沒(méi)有該權(quán)限(只有一個(gè)app的權(quán)限),此時(shí)調(diào)用該接口:
首先將上一步登錄獲得的令牌粘貼到token中:

我們發(fā)送請(qǐng)求得到了403 Forbidden的響應(yīng),意思就是我們沒(méi)有訪問(wèn)權(quán)限,此時(shí)我們將接口權(quán)限改為“app”:
@RestController
public class HelloController {
@GetMapping("/")
@PreAuthorize("hasAuthority('app')")
public String hello(Authentication authentication) {
return "Hello, " + authentication.getName() + "!";
}
}
重啟項(xiàng)目。再次發(fā)起請(qǐng)求:

我們已經(jīng)可以正常訪問(wèn)了!
Spring Security專業(yè)性很強(qiáng),有些術(shù)語(yǔ)對(duì)于初學(xué)者可能有點(diǎn)難度,但是一旦掌握這些概念,你會(huì)喜歡上Spring Security的!
7. 源碼
這兒有一個(gè)可直接運(yùn)行的demo供參考:https://github.com/cloudgyb/spring-security-study-jwt。
到此這篇關(guān)于SpringBoot+SpringSecurity+JWT實(shí)現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例的文章就介紹到這了,更多相關(guān)SpringBoot SpringSecurity JWT認(rèn)證授權(quán)內(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集成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)教程
- SpringBoot3集成SpringSecurity+JWT的實(shí)現(xiàn)
相關(guān)文章
Java命令設(shè)計(jì)模式優(yōu)雅解耦命令和執(zhí)行提高代碼可維護(hù)性
本文介紹了Java命令設(shè)計(jì)模式,它將命令請(qǐng)求封裝成對(duì)象,以達(dá)到解耦命令請(qǐng)求和執(zhí)行者的目的,從而提高代碼可維護(hù)性。本文詳細(xì)闡述了該模式的設(shè)計(jì)原則、實(shí)現(xiàn)方法和優(yōu)缺點(diǎn),并提供了實(shí)際應(yīng)用場(chǎng)景和代碼示例,幫助讀者深入理解和應(yīng)用該模式2023-04-04
關(guān)于@Scheduled參數(shù)及cron表達(dá)式解釋
這篇文章主要介紹了關(guān)于@Scheduled參數(shù)及cron表達(dá)式解釋,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
idea設(shè)置JVM運(yùn)行參數(shù)的幾種方式
對(duì)JVM運(yùn)行參數(shù)進(jìn)行修改是JVM性能調(diào)優(yōu)的重要手段,本文主要介紹了idea設(shè)置JVM運(yùn)行參數(shù)的幾種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
java實(shí)現(xiàn)注冊(cè)登錄系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)注冊(cè)登錄系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
項(xiàng)目總結(jié)之HttpURLConnection的disconnect的問(wèn)題
這篇文章主要介紹了項(xiàng)目總結(jié)之HttpURLConnection的disconnect的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
詳解Kotlin中如何實(shí)現(xiàn)類似Java或C#中的靜態(tài)方法
Kotlin中如何實(shí)現(xiàn)類似Java或C#中的靜態(tài)方法,本文總結(jié)了幾種方法,分別是:包級(jí)函數(shù)、伴生對(duì)象、擴(kuò)展函數(shù)和對(duì)象聲明。這需要大家根據(jù)不同的情況進(jìn)行選擇。2017-05-05
教你如何將Springboot項(xiàng)目成功部署到linux服務(wù)器
這篇文章主要介紹了如何將Springboot項(xiàng)目成功部署到linux服務(wù)器上,本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
Java中String的split切割字符串方法實(shí)例及擴(kuò)展
最近在項(xiàng)目中遇到一個(gè)小問(wèn)題,一個(gè)字符串分割成一個(gè)數(shù)組,下面這篇文章主要給大家介紹了關(guān)于Java中String的split切割字符串方法的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06

