備注:本文適用于你在SpringBoot2.7以前集成過oauth2,并且項目已經正式投入使用的情況,否則,我建議你直接學習或者找資料學習最新的oauth2集成,就不要糾結于老版本的oauth2。
原因:Spring Security 5.x和Spring Security6.x,我個人認為,你可以理解為兩套不同的框架,他們僅僅只是名字差不多而已,升級難度在于,舊系統的登錄認證已經在使用了,尤其是已經介入很多子系統時,這時候需升級需要適配原來的認證,不然會導致子系統需要重新單點登錄。
Spring Security 5.x → 6.x:
棄用舊 API:spring-security-oauth2-autoconfigure 被移除,取而代之的是更模塊化的組件(如 spring-boot-starter-oauth2-client)。
新認證架構:基于 SecurityFilterChain 和 AuthenticationProvider 的聲明式配置取代了舊的 WebSecurityConfigurerAdapter。
本文部分內容參考:https://blog.csdn.net/gandilong 寫的SpringBoot+SpringSecurity OAuth2 認證服務搭建實戰 (二)
文章目錄
- 1快速demo 授權服務器
- 1.1依賴
- 1.2AuthorizationServerConfig
- 1.3SecurityConfig
- 1.4驗證
- 2老版本/oauth/token
- 3問題:認證接口變了
- 4新版本/oauth2/token
- 4.1定位/oauth2/token
1快速demo 授權服務器
1.1依賴
springBoot版本
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.4</version><relativePath/> <!-- lookup parent from repository --></parent>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-authorization-server</artifactId></dependency>
通過查看依賴,發現,SpringBoot3.4.4引用如下依賴
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>1.3.0</version><scope>compile</scope></dependency>
1.2AuthorizationServerConfig
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;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;@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfig {OAuth2TokenEndpointFilter OAuth2TokenEndpointFilter ;OAuth2AuthorizationServerConfigurer OAuth2AuthorizationServerConfigurer;OAuth2ClientAuthenticationFilter OAuth2ClientAuthenticationFilter ;@Beanpublic PasswordEncoder passwordEncoder() throws NoSuchAlgorithmException {return new BCryptPasswordEncoder(12,SecureRandom.getInstanceStrong());}/*** 提供登錄頁面用戶名密碼的認證* @return*/@Beanpublic UserDetailsService userDetailsService(PasswordEncoder passwdEncoder) {UserDetails user= User.builder().username("user")//.password(passwdEncoder.encode("123")) // 使用 {noop} 前綴表示密碼不會被編碼.password("{noop}123") // 使用 {noop} 前綴表示密碼不會被編碼.accountExpired(false).credentialsExpired(false).accountLocked(false).authorities("ROLE_USER") // 用戶的權限.build();return new InMemoryUserDetailsManager(user);}/*** 應用注冊倉庫* @return*/@Beanpublic RegisteredClientRepository registeredClientRepository(PasswordEncoder passwdEncoder) {RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("clientid").clientSecret(passwdEncoder.encode("client_secret"))如果是CLIENT_SECRET_POST才會用到.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST).clientAuthenticationMethod(ClientAuthenticationMethod.NONE).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).redirectUri("http://localhost:8080/login/oauth2/code/clientid").postLogoutRedirectUri("http://localhost:8080/").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).build();return new InMemoryRegisteredClientRepository(oidcClient);}/*** 生成jwk,用在jwt編碼和jwt解碼器上* @return*/@Beanpublic JWKSource<SecurityContext> jwkSource() {KeyPair keyPair = generateRsaKey();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();JWKSet jwkSet = new JWKSet(rsaKey);return new ImmutableJWKSet<>(jwkSet);}/*** 生成RSA256非對稱的秘鑰對:公鑰和私鑰,其中公鑰會出布出去。* @return*/private static KeyPair generateRsaKey() {KeyPair keyPair;try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);keyPair = keyPairGenerator.generateKeyPair();}catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}/*** jwt 解碼器,給資源服務器用* @param jwkSource* @return*/@Beanpublic JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}/*** jwt 編碼器,給授權服務器用* @param jwkSource* @return*/@Beanpublic JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {return new NimbusJwtEncoder(jwkSource);}/*** 默認授權服務器配置* @return*/@Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}
}
1.3SecurityConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;@Configuration
@EnableWebSecurity
public class SecurityConfig {@Bean@Order(1)public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)throws Exception {http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()).oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults()));return http.build();}}
1.4驗證
需要注意,這里使用post + x-www-form-urlencoded來請求,這個是區別于老版本(大概應該是boot2.7以前)
如果你在springBoot2.7以前用過oauth2,對于上述的方式肯定會很蒙蔽,接下來我們來說明為啥會這樣。
2老版本/oauth/token
如下為例,下面是我們一個老版本的oauth2的登錄認證接口,這里我用密碼模式來舉例
http://127.0.0.1:8080/oauth/token?client_id=client_hutao&client_secret=secret_hutao&username=hutao&password=123456&grant_type=password
我們使用postMan調用,如下所示
兩者一對比就能發現,老版在url里面傳參數,新版在請求體x-www-form-urlencoded傳參數。
老版oauth2使用我們比較熟悉的Spring框架的接口開發方式來實現,如下代碼所示
@RequestMapping(value = "/oauth/token", method=RequestMethod.GET)@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)@RequestParam Map<String, String> parameters
因此我們想要去閱讀源碼或者調試信息時,只需要對org.springframework.security.oauth2.provider.endpoint.TokenEndpoint打debug打斷點就可以了,該代碼閱讀起來難度也比較小,很貼近我們比較常見的restful接口的開發。
3問題:認證接口變了
我們先拋開我們怎么搭建授權服務器的問題,以及我現在為什么要從springBoot2升級到springBoot3,先解決一個問題,那就是,我們做授權服務器來實現單點登錄這些功能,為啥要這樣做?因為我們有很多系統都會調用這個單點登錄來實現登錄。也就是說,我們的這個登錄認證接口是被很系統正在使用的,現在如果直接就升級,會導致我們很多子系統要跟著改,這影響很大,因此,我們從springBoot2升級到springBoot3(沒有商量,必須升級)以后,oauth2也得跟著升級,我們得保證oauth2跟著升級以后,我們在新版本的oauth2下,得適配以前其他系統對接的接口。
4新版本/oauth2/token
通過上面的案例,我們發現接口地址變了,以前是/oauth/token,現在是/oauth2/token,并且
現在TokenEndpoint它不見了,不是說換個名字,是正兒八經的被刪除了,通過追蹤發現,/oauth2/token的工作原理機制和/oauth/token完全不一樣。
特性 | 舊版 (Spring Security OAuth2) | 新版 (Spring Authorization Server) |
---|---|---|
依賴 | spring-security-oauth2(已廢棄) | spring-boot-starter-oauth2-authorization-server |
端點類 | TokenEndpoint(顯式 @FrameworkEndpoint) | 無集中式端點類,改為分散的 Filter + AuthenticationProvider OAuth2TokenEndpointFilter |
請求方法 | 支持 GET/POST /oauth/token | 僅支持 POST /oauth2/token |
代碼入口 | 直接由 TokenEndpoint 處理 | 通過過濾器鏈和認證提供者協作處理 |
4.1定位/oauth2/token
如下所示,當我們請求這個接口時,實際上是下面這個OAuth2TokenEndpointFilter處理,而不是以前的那種,TokenEndpoint中的@RequestMapping(value = “/oauth/token”)處理
如下所示,你就可以盡情的debug,查看請求參數或者報錯信息,來幫助你定位問題了