Spring Authorization Server 的一些核心组件和内置 Filter
前言
Spring Security 将 认证中心 的实现剥离了出去,由另一个工程 spring-security-oauth2-authorization-server 提供,这是一个由 Spring Security 团队牵头、社区主导开发的新项目,到写这篇文章时才发布到正式版本 0.2.3
本文基于自己的使用体验,简单的聊一下 Spring Authorization Server 的一些核心组件以及内置的 Filter
核心组件
SecurityFilterChain
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationSFC(HttpSecurity httpSecurity) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(httpSecurity);
return httpSecurity
.formLogin(Customizer.withDefaults())
.build();
}
@Bean
public SecurityFilterChain defaultSFC(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.authorizeHttpRequests()
.mvcMatchers(
"/test"
).permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin(Customizer.withDefaults())
.build();
}
- 准确地说,
SecurityFilterChain 是 Spring Security 的组件,当然 spring-security-oauth2-authorization-server 是依赖 Spring Security 搭建的 SecurityFilterChain 的作用跟在 Spring Security 中一样,就是提供基于 RequestMatcher 的 Filter 声明,以 Spring Security 风格的流式 API 声明针对请求路径的 Filter 组OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(httpSecurity) 这是 spring-security-oauth2-authorization-server 提供的方法,可以理解为针对 Spring Authorization Server 最佳实践配置,主要是针对默认的路径比如 /oauth2/authorize /oauth2/token 等配置默认规则- 上述示例中,
authorizationSFC 组件优先级最高,主要是处理 Spring Authorization Server 的指定路径, defaultSFC 组件处理应用的其他路,效果为:除了 /test 路径外都需要认证
RegisteredClientRepository
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-id")
.redirectUri("http://127.0.0.1:8080/authorized")
.redirectUri("http://127.0.0.1:8080/callback")
.scope(OidcScopes.OPENID)
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofMinutes(30)).build())
.build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
RegisteredClientRepository ,该组件主要负责注册的 Client 信息- 示例中我们注册的是一个
JdbcRegisteredClientRepository ,它基于数据库持久化 Client 信息 - 不难猜测,
Spring Authorization Server 还提供一个默认实现 InMemoryRegisteredClientRepository ,基于内存保存 Client 信息,除了测试一般不用 - 在实际开发中,如果不是一开始就基于
Spring Security Oauth2 的认证搭建的系统,则 Client 信息一般都基于其他形式管理,则我们需要提供自己的 RegisteredClientRepository 实现,好在这并不复杂,一共就三个方法 save findById findByClientId - 其中
RegisteredClient 的构造也提供了方便的 Builder API ,包括了对 clientSettings 的构造比如 Token 有效期等,可见示例
OAuth2AuthorizationService OAuth2AuthorizationConsentService
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
- 这两个组件分别是对
OAuth2Authorization 和 OAuth2AuthorizationConsent 管理抽象,前者代表客户端在认证中心的授权信息,后者代表客户端授权的审核信息 - 这两个组件一般就是用
Jdbc 的实现了,不需要自己拓展,当然如果注册的 Client 不需要 Consent 时,OAuth2AuthorizationConsentService 组件可以不注册
JWKSource
@Bean
public JWKSource<SecurityContext> jwkSource() throws JOSEException {
RSAKey rsaKey = new RSAKeyGenerator(2048).generate();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
spring-security-oauth2-authorization-server 默认发放的令牌是 JWT ,具体的加签算法基于 JWKSource 获取JWKSource 是 Nimbus 的类,持有 JWKSelector 和 SecurityContext 上下文信息来解析 JWK - 示例中默认返回
RSAKey ,即 RSA256 算法加签
ProviderSettings
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://localhost:9000").build();
}
- 这是
spring-security-oauth2-authorization-server 比较新颖的一个机制,可以通过 provider 机制将 认证中心 的元数据信息比如 AuthorizationEndpoint TokenEndipoint 等暴露出去 - 对应的,
Client 就可以基于此机制获取 认证中心 元数据信息,从而避免了大量繁琐的配置
OAuth2TokenCustomizer
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer() {
return context -> {
Set<String> scopes = context.getAuthorizedScopes();
Set<String> authorities = context.getPrincipal()
.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
HashSet<String> authorizedScopes = new HashSet<>();
authorizedScopes.addAll(scopes);
authorizedScopes.addAll(authorities);
context.getClaims()
.claim(OAuth2ParameterNames.SCOPE, authorizedScopes);
};
}
- 该组件非必需,主要负责
JWT 的自定义处理 - 比如示例中,将认证用户的权限也添加到
JWT 的 scope 属性中,这主要是针对 Client oauth2login 的场景,则对应的 Resource Server 就可以针对 认证用户 的权限进行控制了
内置 Filter
OAuth2AuthorizationEndpointFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.authorizationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationConverter.convert(request);
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationManager.authenticate(authorizationCodeRequestAuthentication);
if (!authorizationCodeRequestAuthenticationResult.isAuthenticated()) {
filterChain.doFilter(request, response);
return;
}
if (authorizationCodeRequestAuthenticationResult.isConsentRequired()) {
sendAuthorizationConsent(request, response, authorizationCodeRequestAuthentication, authorizationCodeRequestAuthenticationResult);
return;
}
this.authenticationSuccessHandler.onAuthenticationSuccess(
request, response, authorizationCodeRequestAuthenticationResult);
} catch (OAuth2AuthenticationException ex) {
}
}
- 该拦截器主要处理默认
/oauth2/authorize 路径,主要场景是 oauth2login 或自行请求 授权码 - 它会将
Request 转换成对应的 OAuth2AuthorizationCodeRequestAuthenticationToken 交给 AuthenticationManager 处理 AuthenticationManager 的实现类 ProviderManager 是一种 组合 的设计模式,其下有一组 AuthenticationProvider ,对应的 AuthenticationProvider 会处理,比如此处由 OAuth2AuthorizationCodeRequestAuthenticationProvider 处理- 如果用户未认证,则拦截器继续执行下去以继续
登录认证 等操作 - 认证过后则会选择返回
Consent 页面或者 重定向 客户端回调地址
OAuth2TokenEndpointFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.tokenEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);
} catch (OAuth2AuthenticationException ex) {
}
}
- 该过滤器主要处理
/oauth2/token 路径,主要场景为 oauth2login 或自行获取 Token - 这里主要负责处理的
AuthenticationProvider 为 OAuth2AuthorizationCodeAuthenticationProvider ,负责 Token (RefreshToken ) 的生成 Token 的生成由 OAuth2TokenGenerator 接口负责,仍旧采用 组合 设计模式,由 DelegatingOAuth2TokenGenerator 依次组合 JwtGenerator OAuth2AccessTokenGenerator 等实例,即默认生成 JWT
NimbusJwkSetEndpointFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
JWKSet jwkSet;
try {
jwkSet = new JWKSet(this.jwkSource.get(this.jwkSelector, null));
}
catch (Exception ex) {
}
}
- 该过滤器处理
/oauth2/jwks 路径,主要场景是 Resource Server 验签 JWT 时获取 JWK 公钥 - 基于我们声明的
JWKSource 的组件返回 - 对应的,加签
JWT 的私钥也是基于该组件生成的
其他
还有一些拦截器不再一一列举,比如:
OAuth2AuthorizationServerMetadataEndpointFilter :提供 认证中心 的 Provider 元数据信息OAuth2ClientAuthenticationFilter :负责 Client 的认证- 等等
总结
因为官方暂时未提供成熟的 Spring Boot 自动装配,因此这些组件需要自行注册
当然示例中给出的 个人最佳实践 也是基本可以应付工作中的大多数场景
完整 demo 示例
|