Spring Authorization Server 体验
jdk17 最低要求
介绍
最新版现在支持
- 授权码模式(authorization_code)
- 客户端模式(client_credentials)
- 密码模式(resource owner password credentials)已经被废弃了
引入依赖
创建 spring boot项目,引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
配置AuthorizationServerConfig
@Configuration
public class AuthorizationServerConfig {
@Bean
@Order(1)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http.exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)).oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
/**
* 默认发放令牌
* @return
*/
@Bean
public 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);
}
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;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource){
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
@Bean
public RegisteredClientRepository registeredClientRepository(){
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("clientId")
.clientSecret("{noop}clientSecret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://www.baidu.com")
.postLogoutRedirectUri("http://127.0.0.1:8080/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.scope("all")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
}
DefaultSecurityConfig
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/oauth2/**")
.permitAll()
.anyRequest().authenticated()
)
.cors(Customizer.withDefaults())
.csrf((csrf) -> csrf.disable())
.formLogin(Customizer.withDefaults()) ;
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("test")
.password("test")
.roles("user").build();
return new InMemoryUserDetailsManager(userDetails);
}
}
授权token /oauth2/token
GET
请求 http://localhost:9000/oauth2/authorize?response_type=code&client_id=clientId&client_secret=clientSecret&scope=message.read openid&redirect_uri=http://www.baidu.com
, 如果有多个 scope ,请用空格隔开
- response_type:这个意思是相应的方式为code码
- client_id:即客户端的id
- scope:请求授权范围
- redirect_uri:重定向回来的地址
请求后会要求登陆,即我们配置的 UserDetails
, 这里是 test/test
,登陆后会跳转到授权页面
所授权的就是我们请求scope 所填的权限,只要同意,就会重定向到我们写的重定向地址,并且拼接了code,即https://www.baidu.com/?code=MwFmihKPXBi
获取token
使用 POST 方式请求 /oauth2/token
, 在填写client_id 时,spring 提供了几种不同的方式
client_secret_basic
:客户端的 client_id 和 client_secret,在header中增加Authorization
头,Basic +client_id:client_secret
client_secret_post
:客户端的 client_id 和 client_secret,直接在地址上拼接请求参数如?client_id=xx&client_secret=xxx
client_secret_jwt
:利用 JWT 进行认证private_key_jwt
:方式就是利用 JWT 进行认证。请求方拥有自己的公私钥(密钥对)
我们以第二种方式 client_secret_post
访求,http://localhost:9000/oauth2/token?grant_type=authorization_code&code=ZR4lloWwLsea6f7gs3agbNQ1G3299EVpNJSFL7VgUXwgGFtch75MWKm0M1KOCvnpoapBiyi681ruK52r9ThRPxGaw73TnVdoQn9AY3jCIFlCTOxrtSKF5Q111e5HIHDC&redirect_uri=http://www.baidu.com&client_id=clientId&client_secret=clientSecret
,code 就是上一步授权重定向页面所带回来的code :
curl --location --request POST 'http://localhost:9000/oauth2/token?grant_type=authorization_code&code=ZR4lloWwLsea6f7gs3agbNQ1G3299EVpNJSFL7VgUXwgGFtch75MWKm0M1KOCvnpoapBiyi681ruK52r9ThRPxGaw73TnVdoQn9AY3jCIFlCTOxrtSKF5Q111e5HIHDC&redirect_uri=http%3A%2F%2Fwww.baidu.com&client_id=clientId&client_secret=clientSecret' \
--header 'Cookie: JSESSIONID=755740EAE86CD70E8605A263396F21F7'
返回:
{
"access_token": "eyJraWQiOiIwZTVhMmUzMy1hOT5ZEAYqLTdtLmvTgLTXQ",
"refresh_token": "9tM8L7Ch56Tsm5cMlDCW9geRoaRei9cJe7Szw9s2n4G8_Y3W7nDp",
"scope": "openid message.read",
"id_token": "eyJraWQiOiIwZTVhMmUzMyEZsW7ic5ju67Ag",
"token_type": "Bearer",
"expires_in": 299
}
请求资源
http://127.0.0.1:9000/userinfo
:
curl --location 'http://127.0.0.1:9000/userinfo' \
--header 'Authorization: Bearer eyJraWQiOiI0ODhiZThlNy1jZWYnFVg' \ # access_token
--header 'Cookie: JSESSIONID=F9C28DB81035EDFC4ADB7B7E50893FB9'
返回:
{
"sub": "test"
}