Spring Authorization Server
一、Spring Authorization Server
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 的新政策支持的正式生产版本。
官网地址
1、 功能列表![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/6ca229e8869f1d98652c569c5d4826e0.png)
2、 依赖坐标
-我现在练习的是一下版本,最新的是0.3.1
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.2</version>
</dependency>
3、环境要求
虽然目前官方并没有明确说明,但是从源代码很容易分析出来Spring Authorization Server的环境要求。
- Java 8及以上。
- Spring Boot 2.5.9及以上。
- Spring Security 5.5.4及以上。
- Servlet Web环境,将来不排除对Reactive Web的支持。
二、Spring Authorization Server初体验
分支: springauthserver
1、环境依赖:
像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>
2、授权服务器过滤器链
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();
}
上面是一个基本的配置,关键的步骤为:
- 配置拦截授权服务器相关的请求端点。
- 由于是接口调用,同时关闭相关端点的CSRF功能。
- 将配置类加入HttpSecurity激活配置。
3、客户端的注册和持久化管理
按照OAuth2协议,所有的OAuth2客户端都应该在授权服务器中进行信息注册。你去申请接入第三方开放平台,都要提交一些信息,第三方平台审核通过后会把一些OAuth2客户端信息发给你,这些信息你不会陌生,大部分都包含在OAuth2客户端类库的OAuth2ClientProperties.Registration
中,对应Spring Authorization Serve
r授权服务器的实体为RegisteredClient
:
这些属性多数在前面的章节中已经介绍了,redirect_uri变成了复数以适应多个OAuth2客户端,另外redirect_uri还有一些隐含规则和操作
,相关源码:
这里简单总结一个要点:
- redirect_uri不能有锚点(fragment),比如微信DEMO中携带了锚点#wechat_redirect,这种事实上是不符合OAuth2规范的。
- redirect_uri的host不能为null或者localhost,这一点非常重要。
- 如果redirect_uri的host不是环回地址,必须注册到授权服务器,精确匹配到URI字符串。
- 如果redirect_uri的host是环回地址,可以在调用时切换端口port。
- 如果OAuth2授权服务器是Spring Authorization Server,目前必须严格按照这个规则配置redirect_uri。
4、 ClientSettings
该OAuth2客户端的一些规则配置,包括:
- REQUIRE_PROOF_KEY 授权码授权流程中是否需要对密钥进行质询和验证,默认false。当为true时,开启授权码PKCE支持 RFC7636。
- REQUIRE_AUTHORIZATION_CONSENT 客户端请求授权时是否添加同意授权选项。
- JWK_SET_URL 这个参见Spring Security中的JOSE类库中相关的描述。
- TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM 为private_key_jwt和client_secret_jwt声明JWS签名算法。只能用于令牌端点对客户端进行身份验证环节。
private_key_jwt和client_secret_jwt参见ClientAuthenticationMethod。
5、TokenSettings
注册OAuth2客户端时对该客户端令牌的通用规则配置,包含了:
- ACCESS_TOKEN_TIME_TO_LIVE 访问令牌生存时间,默认5分钟。
- REUSE_REFRESH_TOKENS 是否可以复用刷新令牌,默认true。
- ID_TOKEN_SIGNATURE_ALGORITHM OIDC ID Token使用的签名算法,默认RS256
你可以通过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;
}
6、授权状态信息持久化
资源拥有者的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客户端注册到授权服务器的注册信息中配置了授权确认功能才有用。
7、JWK源配置
授权服务器公私钥都需要,参考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)。
附数据库DDL脚本
Spring Authorization Server的类库内置了数据库DDL脚本,在org/springframework/security/oauth2/server/authorization下,分别是
- oauth2-authorization-schema.sql
- oauth2-authorization-consent-schema.sql
- oauth2-registered-client-schema.sql
你可以手动或者借助于spring.sql.init系列命令进行初始化。
三、Spring Authorization Server客户端
Spring Authorization Server的服务器已经在上面中搭建好了,并注册了一个OAuth2客户端,本篇将利用这个注册的客户端实现HttpSecurity.oauth2Client功能。
1、OAuth2客户端配置
1.1、配置文件
先配置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();
}
1.2、对应的yaml配置:
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类。
1.3、 redirect-uri
/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()的配置即可。
1.4、测试
这里要先启动OAuth2授权服务器,后启动OAuth2客户端,因为客户端需要调用issuer-uri初始化OAuth2授权服务器的信息。
- 浏览器打开http://127.0.0.1:8082/test/bar,注意不能用localhost。
- 浏览器会跳转授权服务器的登录页面http://localhost:9000/login,依次输入用户名test和密码123456
- 然后会重定向到http://localhost:9000/oauth2/authorize,这里增加了一个让用户二次确认的选项,这个功能是我们在授权服务器中特地开启的,你可以勾选同意授权或者直接拒绝授权
- 当勾选scope同意授权后,又重定向到/test/bar完成请求拿到了JSON信息,可以看出来确实是一个匿名用户。
三、授权服务器处理客户端授权请求流程
客户端通过/oauth2/authorize向授权服务器发起了授权请求,这期间发生了什么?通过日志我们来看一个究竟。这里分为两个阶段:
1、用户登录之前
下面是授权服务器收到授权请求处理并跳转到登录/login前的日志:
在这里插入图片描述
日志中标记了四个关键点:
- 授权服务器接收到了授权请求并经过过滤器链处理。
- OAuth2AuthorizationEndpointFilter拦截到了授权请求,然后由授权服务器接收到了授权请求并经过过滤器链处理。
- HttpSession中对授权请求进行了缓存,然后发现本次授权请求是匿名访问。
- 匿名访问被投票拒绝,跳转到登录页。
2、用户登录之后
跳转到登录页后,用户输入用户名和密码登录成功后,会跳转到授权确认页。这里从日志发现重复的还是上面步骤②,说明授权确认页的逻辑还是在OAuth2AuthorizationEndpointFilter中。
3、授权确认后
经过点选确认授权后授权服务器执行了下列逻辑:
-
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方式后面后面也会做一些专门的测试样例。
4、授权服务器发放Token
经过日志分析发现/oauth2/token
是被授权服务器的OAuth2TokenEndpointFilter
拦截处理的。具体交给了OAuth2AuthorizationCodeAuthenticationProvider
来处理,最终返回包含Access Token
的OAuth2AccessTokenAuthenticationToken
。
上面对Spring Authorization Server的案例流程进行了日志分析,发现了几个关键的过滤器。可以预见到Spring Authorization Server的配置就是围绕这几个过滤器
展开的。