Spring Authorization Server 起初是一个社区驱动的项目,在 Spring 的实验项目中启动,由 Spring Security 团队领导,其目的主要是为 Spring 社区提供 OAuth 2.0 授权服务器支持,并最终取代 Spring Security OAuth 。该项目使用 ZenHub 来确定功能路线图的优先次序,并帮助组织项目计划。
自 2020 年 4 月发布 Spring Authorization Server 以来,其已实现的功能已经足够为大部分 OAuth 2.1 授权框架提供支持。
Spring Authorization Server 是一个框架,提供了OAuth 2.1和OpenID Connect 1.0规范以及其他相关规范的实现。它建立在Spring Security之上,为构建 OpenID Connect 1.0 Identity Providers 和 OAuth2 Authorization Server 产品提供安全、轻量级和可定制的基础。
此外,在已经发布到 Spring Authorization Server 0.3.1 版本,这也是由 Spring 的新政策支持的正式生产版本。
官网地址
-我现在练习的是一下版本,最新的是0.3.1
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.2</version>
</dependency>
虽然目前官方并没有明确说明,但是从源代码很容易分析出来Spring Authorization Server的环境要求。
分支: springauthserver
像OAuth2 Client、Resource Server一样,Spring Authorization Server也是以插件的形式接入Spring Security的体系中。下面列举了目前必备的环境依赖:
<dependencies>
<!-- actuator 指标监控 非必须 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- spring security starter 必须 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<!-- orm -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- spring mvc servlet web 必须 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 插件 非必须 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
OAuth2授权服务器专门处理OAuth2客户端的授权请求流程,授权端点、Token端点、用户信息端点等等都需要对应的过滤器支持,这些过滤器由Spring Authorization Server中的OAuth2AuthorizationServerConfigurer负责初始化和配置。我们只需要定义一个优先级最高的过滤器链,把授权服务器配置类初始化并激活即可。
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
// TODO 你可以根据需求对authorizationServerConfigurer进行一些个性化配置
RequestMatcher authorizationServerEndpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
// ①
http.requestMatcher(authorizationServerEndpointsMatcher)
.authorizeRequests().anyRequest().authenticated()
.and()
// ②忽略掉相关端点的csrf
.csrf(csrf -> csrf
.ignoringRequestMatchers(authorizationServerEndpointsMatcher))
// 开启form登录
.formLogin()
.and()
// ③应用 授权服务器的配置
.apply(authorizationServerConfigurer);
return http.build();
}
上面是一个基本的配置,关键的步骤为:
按照OAuth2协议,所有的OAuth2客户端都应该在授权服务器中进行信息注册。你去申请接入第三方开放平台,都要提交一些信息,第三方平台审核通过后会把一些OAuth2客户端信息发给你,这些信息你不会陌生,大部分都包含在OAuth2客户端类库的OAuth2ClientProperties.Registration
中,对应Spring Authorization Serve
r授权服务器的实体为RegisteredClient
:
这些属性多数在前面的章节中已经介绍了,redirect_uri变成了复数以适应多个OAuth2客户端,另外redirect_uri还有一些隐含规则和操作
,相关源码:
这里简单总结一个要点:
该OAuth2客户端的一些规则配置,包括:
private_key_jwt和client_secret_jwt参见ClientAuthenticationMethod。
注册OAuth2客户端时对该客户端令牌的通用规则配置,包含了:
你可以通过TokenSettings.withSettings添加额外的自定义属性或者覆盖已有的属性。
我们来初始化一个OAuth2客户端,这里我们使用的客户端授权方法ClientAuthenticationMethod是client_secret_basic,因为之前对应的basic已经不建议使用了:
private RegisteredClient createRegisteredClient(final String id) {
return RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端ID和密码
.clientId("felord")
// 此处为了避免频繁启动重复写入仓库
.id(id)
// client_secret_basic 客户端需要存明文 服务器存密文
.clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder()
.encode("secret"))
// 名称 可不定义
.clientName("felord")
// 授权方法
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 授权类型
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 回调地址名单,不在此列将被拒绝 而且只能使用IP或者域名 不能使用 localhost
.redirectUri("http://127.0.0.1:8082/login/oauth2/code/test-client-oidc")
.redirectUri("http://127.0.0.1:8082/authorized")
.redirectUri("http://127.0.0.1:8082/login/oauth2/code/test")
.redirectUri("http://127.0.0.1:8082/test/bar")
.redirectUri("https://baidu.com")
// OIDC支持
.scope(OidcScopes.OPENID)
// 其它Scope
.scope("message.read")
.scope("userinfo")
.scope("message.write")
// JWT的配置项 包括TTL 是否复用refreshToken等等
.tokenSettings(TokenSettings.builder().build())
// 配置客户端相关的配置项,包括验证密钥或者 是否需要授权页面
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true).build())
.build();
}
上面注册的OAuth2客户端信息需要持久化到数据库,RegisteredClientRepository接口抽象了对RegisteredClient的持久化操作,这里我们直接启用内置的JDBC实现以代替默认的内存实现:
@SneakyThrows
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// 每次都会初始化 生产的话 只初始化JdbcRegisteredClientRepository
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
// TODO 生产上 注册客户端需要使用接口 不应该采用下面的方式
// only@test begin
final String id = "10000";
RegisteredClient registeredClient = registeredClientRepository.findById(id);
if (registeredClient == null) {
registeredClient = this.createRegisteredClient(id);
//这里为了测试,我们在初始化JdbcRegisteredClientRepository的时候保存了一个OAuth2客户端信息。
registeredClientRepository.save(registeredClient);
}
// only@test end
return registeredClientRepository;
}
资源拥有者的OAuth2授权状态信息OAuth2Authorization也需要持久化管理,Spring Authorization Server提供了OAuth2AuthorizationService来负责这个工作,我们同样需要启用内置的JDBC实现以代替默认的内存实现:
@Bean
public OAuth2AuthorizationService authorizationService(
JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate,
registeredClientRepository);
}
授权确认状态持久化
如果该客户端配置ClientSettings开启了授权确认REQUIRE_AUTHORIZATION_CONSENT ,授权确认的信息也要持久化管理,需要启用内置的JDBC实现以代替默认的内存实现:
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(
JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate,
registeredClientRepository);
}
OAuth2客户端注册到授权服务器的注册信息中配置了授权确认功能才有用。
授权服务器公私钥都需要,参考Spring Security中的JOSE类库中的方法,结合Spring Authorization Server提供的方案,我们只需要定义一个JWKSource类型的Spring Bean即可:
/**
* 加载JWK资源
*
* @return the jwk source
*/
@SneakyThrows
@Bean
public JWKSource<SecurityContext> jwkSource() {
//TODO 这里优化到配置
// jks classpath路径
String path = "jose.jks";
// key alias
String alias = "jose";
// password
String pass = "test.cn";
ClassPathResource resource = new ClassPathResource(path);
KeyStore jks = KeyStore.getInstance("jks");
char[] pin = pass.toCharArray();
jks.load(resource.getInputStream(), pin);
RSAKey rsaKey = RSAKey.load(jks, alias, pin);
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
到这里就配置完了。启动项目,访问下面的issue端点:
http://localhost:9000/.well-known/oauth-authorization-server
将返回授权服务器的元信息:
{
"issuer": "http://localhost:9000",
"authorization_endpoint": "http://localhost:9000/oauth2/authorize",
"token_endpoint": "http://localhost:9000/oauth2/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"jwks_uri": "http://localhost:9000/oauth2/jwks",
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"client_credentials",
"refresh_token"
],
"revocation_endpoint": "http://localhost:9000/oauth2/revoke",
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"introspection_endpoint": "http://localhost:9000/oauth2/introspect",
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt"
],
"code_challenge_methods_supported": [
"plain",
"S256"
]
}
这些配置是提供给OAuth2客户端的,里面也有不少的端点,比如jwks_uri你可以访问一下,看看能否获取公钥JWK。
OAuth2客户端请求授权跳转到授权服务器,需要一个授权服务器用户登录认证并同意授权。我们在Spring Authorization Server授权服务器中临时指定一个测试用户test,密码为123456:
@Bean
UserDetailsService users() {
UserDetails user = User.builder()
.username("test")
.password("123456")
.passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode)
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
这个用户就是OAuth2中的资源拥有者(Resource Owner)。
Spring Authorization Server的类库内置了数据库DDL脚本,在org/springframework/security/oauth2/server/authorization下,分别是
Spring Authorization Server的服务器已经在上面中搭建好了,并注册了一个OAuth2客户端,本篇将利用这个注册的客户端实现HttpSecurity.oauth2Client功能。
先配置OAuth2客户端的配置文件,这里要对照着Spring Authorization Server中注册的那个OAuth2客户端。这里抄过来对照:
private RegisteredClient createRegisteredClient(final String id) {
return RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端ID
.clientId("felord")
// 此处为了避免频繁启动重复写入仓库
.id(id)
// client_secret_basic 模式下的密码 在客户端需要存明文 在授权服务器存密文
.clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder()
.encode("secret"))
// 名称可不定义
.clientName("felord")
// 授权方法
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 支持的授权类型
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 回调地址名单,不在此列将被拒绝 而且只能使用IP或者域名 不能使用 localhost
.redirectUri("http://127.0.0.1:8082/login/oauth2/code/test-client-oidc")
.redirectUri("http://127.0.0.1:8082/authorized")
.redirectUri("http://127.0.0.1:8082/login/oauth2/code/felord")
.redirectUri("http://127.0.0.1:8082/test/bar")
.redirectUri("https://baidu.com")
// OIDC支持
.scope(OidcScopes.OPENID)
// 其它Scope
.scope("message.read")
.scope("userinfo")
.scope("message.write")
// JWT的配置项 包括TTL 是否复用refreshToken等等
.tokenSettings(TokenSettings.builder().build())
// 配置客户端相关的配置项,包括验证密钥或者 是否需要授权页面
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true).build())
.build();
}
spring:
security:
oauth2:
client:
registration:
test:
client-id: felord
client-secret: secret
redirect-uri: 'http://127.0.0.1:8082/test/bar'
authorization-grant-type: authorization_code
client-authentication-method: client_secret_basic
scope: message.read,message.write
provider:
test:
#todo provider 尽量用域名 不要用localhost或者IP 而且要和well-known接口中保持一致
issuer-uri: http://localhost:9000
/.well-known/oauth-authorization-server端点能替代很多provider的配置项,如果同时存在issuer-uri和其它端点,issuer-uri的优先级要低一些,相关的解析逻辑请参考ClientRegistrations类。
/test/bar是一个测试接口:
/**
* 测试Spring Authorization Server
*
* @see HttpSecurity#oauth2Client()
* @param client the client
* @return the map
*/
@GetMapping("/test/bar")
public Map<String,Object> bar(@RegisteredOAuth2AuthorizedClient("test") OAuth2AuthorizedClient client){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Map<String, Object> map = new HashMap<>();
map.put("authentication",authentication);
// OAuth2AuthorizedClient 为敏感信息不应该返回前端
map.put("oAuth2AuthorizedClient",client);
return map;
}
根据OAuth2ClientConfigurer一文中的讲解,/test/bar需要配置匿名访问,伪代码:
http.authorizeRequests((requests) -> requests
.antMatchers("/test/bar")
.hasAnyAuthority("ROLE_ANONYMOUS","SCOPE_userinfo")
.anyRequest().authenticated())
其它的复用前面几个客户端HttpSecurity.oauth2Client()的配置即可。
这里要先启动OAuth2授权服务器,后启动OAuth2客户端,因为客户端需要调用issuer-uri初始化OAuth2授权服务器的信息。
客户端通过/oauth2/authorize向授权服务器发起了授权请求,这期间发生了什么?通过日志我们来看一个究竟。这里分为两个阶段:
下面是授权服务器收到授权请求处理并跳转到登录/login前的日志:
在这里插入图片描述
日志中标记了四个关键点:
跳转到登录页后,用户输入用户名和密码登录成功后,会跳转到授权确认页。这里从日志发现重复的还是上面步骤②,说明授权确认页的逻辑还是在OAuth2AuthorizationEndpointFilter中。
经过点选确认授权后授权服务器执行了下列逻辑:
POST 请求/oauth2/authorize,再次被OAuth2AuthorizationEndpointFilter拦截处理,
由于用户是认证的,通知OAuth2客户端重定向到最开始的redirect_uri地址http://127.0.0.1:8082/test/bar?code=CODE&state=STATE
OAuth2客户端OAuth2AuthorizationCodeGrantFilter
拦截到携带了code和state的redirect_uri
后向授权服务器发起/oauth2/token
请求获取token,具体的流程参考前面的相关文章。
这里需要提及一个重要的知识点,这里由于我们采用的客户端认证方式(ClientAuthenticationMethod)是client_secret_basic,所以获取token的请求是这样的:
POST /oauth2/token HTTP/1.1
Accept: application/json;charset=UTF-8
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Authorization: Basic ZmVsb3JkOnNlY3JldA==
Host: localhost:9000
该请求通过HTTP BASIC
的方式来认证客户端。 它使用客户端的client-id和client-secret
作为凭证信息,并且使用 BASE64算法进行编码。 而且授权服务器保存得客户端密码是经过摘要的密文。这和前面gitee和wechat采用的机制完全不同,更加安全。那授权服务器如何处理/oauth2/token呢?
其它的ClientAuthenticationMethod方式后面后面也会做一些专门的测试样例。
经过日志分析发现/oauth2/token
是被授权服务器的OAuth2TokenEndpointFilter
拦截处理的。具体交给了OAuth2AuthorizationCodeAuthenticationProvider
来处理,最终返回包含Access Token
的OAuth2AccessTokenAuthenticationToken
。
上面对Spring Authorization Server的案例流程进行了日志分析,发现了几个关键的过滤器。可以预见到Spring Authorization Server的配置就是围绕这几个过滤器
展开的。