SpringBoot+SpringSecurity+JWT實現(xiàn)系統(tǒng)認證與授權(quán)示例
1. Spring Security簡介
Spring Security是Spring的一個核心項目,它是一個功能強大且高度可定制的認證和訪問控制框架。它提供了認證和授權(quán)功能以及抵御常見的攻擊,它已經(jīng)成為保護基于spring的應(yīng)用程序的事實標(biāo)準。
Spring Boot提供了自動配置,引入starter依賴即可使用。
Spring Security特性總結(jié):
- 使用簡單,提供Spring Boot starter依賴,極易與Spring Boot項目整合。
- 專業(yè),提供CSRF防護、點擊劫持防護、XSS防護等,并提供各種安全頭整合(X-XSS-Protection,X-Frame-Options等)。
- 密碼加密存儲,支持多種加密算法
- 擴展性和可定制性極強
- OAuth2 JWT認證支持
- … …
2. JWT簡介
JWT(Json web token),是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(RFC 7519).該token被設(shè)計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息(例如,權(quán)限信息)。一旦用戶被授予token,用戶即可通過該token訪問服務(wù)器上的資源。
https://jwt.io/,該網(wǎng)站提供了一個debuggr,便于初學(xué)者學(xué)習(xí)理解JWT。
3. Spring Boot整合Spring Security
注意本篇文章演示使用JDK和Spring Boot的版本如下:
Spring Boot:2.7.2
JDK:11
不同的Spring Boot版本配置不同,但是原理相同。
在Spring Boot項目的pom.xml文件中加入下面的依賴:
<!-- Spring Security的Spring boot starter,引入后將自動啟動Spring Security的自動配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 下面的依賴包含了OAuth2 JWT認證實現(xiàn) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency>
以上兩個依賴即可。
4. 配置Spring Security使用JWT認證
注意: 不同的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對JWT做簽名,所以這里需要一對秘鑰。 //秘鑰文件的路徑在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)的登錄接口,所以需要匿名可訪問 http.authorizeRequests().antMatchers("/login").anonymous(); //其他請求都需認證后才能訪問 http.authorizeRequests().anyRequest().authenticated() .and() //采用JWT認證無需session保持,所以禁用掉session管理器 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() //login接口可能來自其他站點,所以對login不做csrf防護 .csrf((csrf) -> csrf.ignoringAntMatchers("/login")) //配置認證方式為JWT,并且配置了一個JWT認證裝換器,用于去掉解析權(quán)限時的SCOOP_前綴 .oauth2ResourceServer().jwt().jwtAuthenticationConverter( JwtAuthenticationConverter() ); //配置認證失敗或者無權(quán)限時的處理器 http.exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()) .accessDeniedHandler(new BearerTokenAccessDeniedHandler()) ); //根據(jù)配置生成SecurityFilterChain對象 return http.build(); } /** * JWT解碼器,用于認證時的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認證解碼時,去掉Spring Security對權(quá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項目的Resource目錄下生成一對RSA秘鑰。
可以使用下面的網(wǎng)站進行生成:http://tools.jb51.net/password/rsa_encode/,注意: 密鑰格式使用 PKCS#8,私鑰密碼為空。
還有一點需要說明,我在代碼中使用了Spring Boot的值注入:
@Value("${jwt.public.key}") private RSAPublicKey key; @Value("${jwt.private.key}") private RSAPrivateKey priv;
有沒有很好奇Spring Boot是如何將yaml文件中的字符串對應(yīng)的文件轉(zhuǎn)換為RSAPublicKey和RSAPrivateKey ?
其實是Spring Security幫我們做了處理,在Spring Security中幫我們實現(xiàn)了一個轉(zhuǎn)換器ResourceKeyConverterAdapter,具體可以閱讀相關(guān)源碼來更深入的了解。
至此我們的項目已經(jīng)支持JWT認證了。
但是用戶需要在請求頭Authorization中攜帶合法的JWT才能通過認證,進而訪問服務(wù)器資源,那么如何給用戶頒發(fā)一個合法的JWT呢?
很簡單,可以提供一個登錄接口,讓用戶輸入用戶名和密碼,匹配成功后頒發(fā)令牌即可。
其實并不是必須這樣做,還有其他方式,比如我們調(diào)用第三方接口,我們經(jīng)常的做法是先去第三方申請,申請通過后我們就可以得到一個令牌。這個過程和上面的登錄通過后頒發(fā)一個令牌是一樣的,都是通過合法的途徑獲得一個令牌!
5. 實現(xiàn)登錄接口
登錄接口只有一個目的,就是給合法用戶頒發(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); } }
登錄邏輯實現(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) { //從用戶信息存儲庫中獲取用戶信息 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)拋出該異常后會將http響應(yīng)狀態(tài)碼設(shè)置為401 unauthorized throw new BadCredentialsException("密碼錯誤!"); } private String generateToken(UserDetails userDetails) { Instant now = Instant.now(); //JWT過期時間為36000秒,也就是600分鐘,10小時 long expiry = 36000L; String scope = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(" ")); //將用戶權(quán)限信息使用空格分割拼為字符串,放到JWT的payload的scope字段中,注意不要改變scope這個屬性,這是Spring Security OAuth2 JWT默認處理方式,在JWT解碼時需要讀取該字段,轉(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(); } }
其他非核心代碼這里就不貼出來了,我將代碼放到github上了,具體可以轉(zhuǎn)到https://github.com/cloudgyb/spring-security-study-jwt。
6. 測試
使用postman測試一下:
使用錯誤的密碼,會返回401 Unauthorized的狀態(tài)碼,表示我們認證失敗!
使用正確的用戶名和密碼:
返回了JWT令牌。
此時客戶端拿到了合法的令牌,接下來就可以訪問服務(wù)器上有權(quán)訪問的資源了。
我寫了一個測試接口:
@RestController public class HelloController { @GetMapping("/") @PreAuthorize("hasAuthority('test')") public String hello(Authentication authentication) { return "Hello, " + authentication.getName() + "!"; } }
該接口需要用戶擁有"test"的權(quán)限,但是登錄用戶沒有該權(quán)限(只有一個app的權(quán)限),此時調(diào)用該接口:
首先將上一步登錄獲得的令牌粘貼到token中:
我們發(fā)送請求得到了403 Forbidden的響應(yīng),意思就是我們沒有訪問權(quán)限,此時我們將接口權(quán)限改為“app”:
@RestController public class HelloController { @GetMapping("/") @PreAuthorize("hasAuthority('app')") public String hello(Authentication authentication) { return "Hello, " + authentication.getName() + "!"; } }
重啟項目。再次發(fā)起請求:
我們已經(jīng)可以正常訪問了!
Spring Security專業(yè)性很強,有些術(shù)語對于初學(xué)者可能有點難度,但是一旦掌握這些概念,你會喜歡上Spring Security的!
7. 源碼
這兒有一個可直接運行的demo供參考:https://github.com/cloudgyb/spring-security-study-jwt。
到此這篇關(guān)于SpringBoot+SpringSecurity+JWT實現(xiàn)系統(tǒng)認證與授權(quán)示例的文章就介紹到這了,更多相關(guān)SpringBoot SpringSecurity JWT認證授權(quán)內(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集成Spring security JWT實現(xiàn)接口權(quán)限認證
- SpringBoot3.x接入Security6.x實現(xiàn)JWT認證的完整步驟
- springboot+springsecurity+mybatis+JWT+Redis?實現(xiàn)前后端離實戰(zhàn)教程
- SpringBoot3集成SpringSecurity+JWT的實現(xiàn)
相關(guān)文章
Java命令設(shè)計模式優(yōu)雅解耦命令和執(zhí)行提高代碼可維護性
本文介紹了Java命令設(shè)計模式,它將命令請求封裝成對象,以達到解耦命令請求和執(zhí)行者的目的,從而提高代碼可維護性。本文詳細闡述了該模式的設(shè)計原則、實現(xiàn)方法和優(yōu)缺點,并提供了實際應(yīng)用場景和代碼示例,幫助讀者深入理解和應(yīng)用該模式2023-04-04關(guān)于@Scheduled參數(shù)及cron表達式解釋
這篇文章主要介紹了關(guān)于@Scheduled參數(shù)及cron表達式解釋,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12項目總結(jié)之HttpURLConnection的disconnect的問題
這篇文章主要介紹了項目總結(jié)之HttpURLConnection的disconnect的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06詳解Kotlin中如何實現(xiàn)類似Java或C#中的靜態(tài)方法
Kotlin中如何實現(xiàn)類似Java或C#中的靜態(tài)方法,本文總結(jié)了幾種方法,分別是:包級函數(shù)、伴生對象、擴展函數(shù)和對象聲明。這需要大家根據(jù)不同的情況進行選擇。2017-05-05教你如何將Springboot項目成功部署到linux服務(wù)器
這篇文章主要介紹了如何將Springboot項目成功部署到linux服務(wù)器上,本文分步驟給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12