springboot 2.7 oauth server配置源码走读
入口:
上述截图中的方法签名和OAuth2AuthorizationServerConfiguration类中的一个方法一样,只不过我们自己的配置类优先级比spring中的配置类低,算是配置覆盖,看下图所示:
它们都提到了 OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
所以我们分析它:
一. new出来的OAuth2AuthorizationServerConfigurer:
官方声称它是:An AbstractHttpConfigurer for OAuth 2.0 Authorization Server support.(一个用于OAuth 2.0 授权服务器的抽象配置类)。
从源码中可以看到它“管理”着以下具体配置类:
先给出结论:这些配置类endpoint访问默认值对应如下(如何推导请耐心往下看):
我们具体看下上述第77行:getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) 的实现:
我们可以看到第一条绿色下划线:tokenEndpoint的来源,等下我们再去探究它:
所以我们去AntPathRequestMatcher类 查看实现:
可以看到用到了策略模式:根据pattern (/**表示MATCH_ALL)不同,初始化的matcher也不同:
接下来我们可以看上述第一条绿色下划线:tokenEndpoint的来源,现在去探究它:
ProviderSettings:它是一个配置类,继承于AbstractSettings(接下来我们自己配置中用到的TokenSettings+ClientSettings类也继承于它),如下所求,我们可以指定token endpoint(当然还有授权,introspection等等),
如果不指定,则默认配置如下:注意 /oauth2/jwks, 后面resource-server就可以指定它来验证token的有效性。
那ProvideSettings如何初始化呢?又是和配置类关联起来?
1.构造方法中传递:
2.在我们的配置类中流入bean(官方示例也是这么做的):
然后看源码:
其中providerSettings.getJwkSetEndpoint()我们上面看过,如果没有指定配置,则默认值为:
/oauth2/jwks
二. 我们配置类中的其它配置:
上述2个配置方法在OAuth2AuthorizationServerConfiguration类中也可以找到类似的方法,如下所示:
三.最后把完整的oauth2 server配置如下:
package com.jel.tech.auth.config;
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.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
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.OAuth2AuthorizationServerConfiguration;
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.factory.PasswordEncoderFactories;
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.NimbusJwtDecoder;
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.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
/**
* @author: jelex.xu
* @Date: 2024/1/2 18:31
* @desc:
**/
@Configuration
public class OAuth2AuthorizeSecurityConfig {
/**
* 重写:org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
* #authorizationServerSecurityFilterChain(HttpSecurity)
* @param http
* @return
* @throws Exception
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
@Bean
@Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
// @formatter:on
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient loginClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("login-client")
.clientSecret("{noop}openid-connect")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/login-client")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("message:read")
.scope("message:write")
// 指定token有效期:token:30分(默认5分钟),refresh_token:1天
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30))
.refreshTokenTimeToLive(Duration.ofDays(1))
.build())
// .id("xxx")
.build();
return new InMemoryRegisteredClientRepository(loginClient, registeredClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource(KeyPair keyPair) {
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);
}
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
}
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://127.0.0.1:9000").build();
}
@Bean
public UserDetailsService userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
// outputs {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
// remember the password that is printed out and use in the next step
System.out.println(encoder.encode("password"));
UserDetails userDetails = User.withUsername("user")
.username("user")
.password("{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
return keyPair;
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
}
}