- 前言
- 核心组件
- SecurityFilterChain
- RegisteredClientRepository
- OAuth2AuthorizationService OAuth2AuthorizationConsentService
- JWKSource
- ProviderSettings
- OAuth2TokenCustomizer
- 内置 Filter
- OAuth2AuthorizationEndpointFilter
- OAuth2TokenEndpointFilter
- NimbusJwkSetEndpointFilter
- 其他
- 总结
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 路径外都需要认证
@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")
// 建议注册一个 openid 的 scope,可以不提供 userinfo endpoint
.scope(OidcScopes.OPENID)
.scope("message.read")
.scope("message.write")
// 设置 Client 需要页面审核授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
// 设置 Token 的有效期为 30min
.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 有效期等,可见示例
@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 组件可以不注册
@Bean
public JWKSource 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 算法加签
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://localhost:9000").build();
}
- 这是 spring-security-oauth2-authorization-server 比较新颖的一个机制,可以通过 provider 机制将 认证中心 的元数据信息比如 AuthorizationEndpoint TokenEndipoint 等暴露出去
- 对应的,Client 就可以基于此机制获取 认证中心 元数据信息,从而避免了大量繁琐的配置
@Bean
public OAuth2TokenCustomizer oAuth2TokenCustomizer() {
return context -> {
// 客户端 scope
Set scopes = context.getAuthorizedScopes();
// 认证用户的权限
Set authorities = context.getPrincipal()
.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
// 权限集合
HashSet authorizedScopes = new HashSet<>();
authorizedScopes.addAll(scopes);
authorizedScopes.addAll(authorities);
// 覆盖 JWT scope 信息
context.getClaims()
.claim(OAuth2ParameterNames.SCOPE, authorizedScopes);
};
}
- 该组件非必需,主要负责 JWT 的自定义处理
- 比如示例中,将认证用户的权限也添加到 JWT 的 scope 属性中,这主要是针对 Client oauth2login 的场景,则对应的 Resource Server 就可以针对 认证用户 的权限进行控制了
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 匹配 /oauth2/authorize 路径
if (!this.authorizationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
// 将 request 处理为 OAuth2AuthorizationCodeRequestAuthenticationToken
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationConverter.convert(request);
// 由对应的 AuthorizationProvider 处理
// 比如生成授权码、处理 Consent 信息等
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationManager.authenticate(authorizationCodeRequestAuthentication);
// 如果用户还未认证,则继续执行下去,可能会被登录页面等拦截器处理
if (!authorizationCodeRequestAuthenticationResult.isAuthenticated()) {
filterChain.doFilter(request, response);
return;
}
// 如果需要审核则发送 Consent 页面
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 页面或者 重定向 客户端回调地址
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 处理 /oauth2/token 路径
if (!this.tokenEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
// ...
// 由 OAuth2AuthorizationCodeAuthenticationProvider 处理
// 包括生成 Token RefreshToken 等
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
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 处理 /oauth2/jwks 路径
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
// 基于我们声明的 JWKSource 组件
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 示例



