在客户端获取到令牌后,访问资源时,可以设置如下请求头或者请求参数中加上 access_token。
Authorization: Bearer access_token
在前两篇博客中《access_token的生成》《如何生成jwt》,我们经过源码分析,已经知道 access_token 产生的大体流程,并且知道如何自定义 token 的格式,其中,授权服务器配置中的 AuthorizationServerEndpointsConfigurer 可谓极为重要;而关于access_token 的验证流程,我们就从对应的资源服务器的配置入手。
与生成access_token 有固定接口 /oauth/token 不同,我们肯定不能通过业务资源的接口地址去找到验证 token 源码的入口。SpringSecurity原理是责任链模式,绝大数功能都是过滤器实现的,这里也不例外。资源服务器的配置如下:
这里重点关注一下ResourceServerSecurityConfigurer。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId("message")//设置资源id
.tokenStore(new JwtTokenStore(jwtAccessTokenConverter()))
.tokenServices(new RemoteTokenServices())
.tokenExtractor(new BearerTokenExtractor())
;
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
在 ResourceServerSecurityConfigurer 中我们看到鉴权所需要的两个重要元素,Filter 和 AuthenticationManager。看到下面的代码,一些比较敏感的小伙伴可能会推测,所谓的 access_token 验证应该就是用这里配置的 Filter 和 AuthenticationManager进行鉴权了。
public final class ResourceServerSecurityConfigurer extends
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
......
private OAuth2AuthenticationProcessingFilter resourcesServerFilter;
private AuthenticationManager authenticationManager;
......
@Override
public void configure(HttpSecurity http) throws Exception {
AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
//N 指定过滤器
resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
//N 设置 AuthenticationManager
resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
if (eventPublisher != null) {
resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
}
if (tokenExtractor != null) {
resourcesServerFilter.setTokenExtractor(tokenExtractor);
}
resourcesServerFilter = postProcess(resourcesServerFilter);
resourcesServerFilter.setStateless(stateless);
// @formatter:off
http
.authorizeRequests().expressionHandler(expressionHandler)
.and()
//N 将过滤器放到过滤器链中
.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
// @formatter:on
}
private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
if (this.authenticationManager != null) {
//如果自己配置了 authenticationManager
if (authenticationManager instanceof OAuth2AuthenticationManager) {
oauthAuthenticationManager = (OAuth2AuthenticationManager) authenticationManager;
} else {
return authenticationManager;
}
}
oauthAuthenticationManager.setResourceId(resourceId);
oauthAuthenticationManager.setTokenServices(resourceTokenServices(http));
oauthAuthenticationManager.setClientDetailsService(clientDetails());
return oauthAuthenticationManager;
}
......
}
通过上面的代码.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)我们可以知道,用于处理令牌验证的过滤器应该就是 OAuth2AuthenticationProcessingFilter 了,并且默认AuthenticationManager是OAuth2AuthenticationManager。
过滤器肯定是要看 doFilter 了,核心逻辑如下:
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
......
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setTokenExtractor(TokenExtractor tokenExtractor) {
this.tokenExtractor = tokenExtractor;
}
......
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
boolean debug = logger.isDebugEnabled();
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
//N 从 request 中提取access_token
Authentication authentication = this.tokenExtractor.extract(request);
if (authentication == null) {
if (this.stateless && this.isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
} else {
//N 把access_token 放到request属性中 {"OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE":"access_token"}
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
//request中的 remoteAddress、sessionId、tokenValue、tokenType 等信息放到details中
needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//N 鉴权
Authentication authResult = this.authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
this.eventPublisher.publishAuthenticationSuccess(authResult);
//N 设置安全上下文
SecurityContextHolder.getContext().setAuthentication(authResult);
}
} catch (OAuth2Exception e) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + e);
}
this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(e.getMessage(), e), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(e.getMessage(), e));
return;
}
chain.doFilter(request, response);
}
......
}
关于 BearerTokenExtractor,作用就是从request 的请求头或者请求参数中提取access_token,并设置 tokenType(例如bearer类型),逻辑相对简单,这里就不展开了。当然我们也可以定义自己的 token 提取器,只要在配置资源服务器时设置给 ResourceServerSecurityConfigurer 对象即可。
AuthenticationManager 的作用就是鉴权,这里的逻辑也很简单,就是验证token是否是真的由授权服务器产生,如果是,继续校验resourceId 和scope。资源服务器默认的resourceId 在 ResourceServerSecurityConfigurer 类中,是 “oauth2-resource”。
这里如果令牌对应的 resourceIds 是空的,就不校验resourceId了,换种说法就是如果我们数据库接入端表中没配置resourceId,就拥有所有资源服务器的访问权限,总感觉不爽。
public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean {
private ResourceServerTokenServices tokenServices;
......
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
//N 提取 access_token
String token = (String) authentication.getPrincipal();
//N 验证 access_token 是否真的由授权服务器产生
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
//N 校验该令牌是否拥有访问资源服务器的权限
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
//N 校验令牌和client 的scope是否相符
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
//clientDetailsService 可能为null,后面我们单独说明。
private void checkClientDetails(OAuth2Authentication auth) {
if (clientDetailsService != null) {
//N 进入这里说明要么我们自己设置了clientDetailsService,要么资源服务与授权服务是同一个服务
ClientDetails client;
try {
client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
}
catch (ClientRegistrationException e) {
throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
}
//数据库查询到的客户端配置的scope
Set<String> allowed = client.getScope();
//N 校验令牌的scope
for (String scope : auth.getOAuth2Request().getScope()) {
if (!allowed.contains(scope)) {
throw new OAuth2AccessDeniedException(
"Invalid token contains disallowed scope (" + scope + ") for this client");
}
}
}
}
}
接下来我们继续探索 tokenServices.loadAuthentication(token);方法都做了什么。
ResourceServerTokenServices 的默认实现是 DefaultTokenServices, 这段代码可以在ResourceServerSecurityConfigurer 中看到,当然,如果我们配置了 resourceTokenServices,在 if 中则会直接返回配置的实现。
public final class ResourceServerSecurityConfigurer extends
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
......
private ResourceServerTokenServices tokenServices(HttpSecurity http) {
if (resourceTokenServices != null) {
return resourceTokenServices;
}
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(clientDetails());
this.resourceTokenServices = tokenServices;
return tokenServices;
}
private ClientDetailsService clientDetails() {
//获取授权服务设置的ClientDetailsService
return getBuilder().getSharedObject(ClientDetailsService.class);
}
......
}
Security 为我们提供了两个 ResourceServerTokenServices 子类。
通过 tokenStore 可以推测出,这个必须要和授权服务器能够访问相同的存储才行。(JwtTokenStore比较特殊,它并没有存储token,因为验证jwt只需要对称密钥或者公钥就可以,感兴趣的小伙伴可以看 JwtTokenStore 的源码)
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
//N 从存储中读取access_token,这里 JwtTokenStore 实现的比较特殊,是返回接收到的jwt
//N 显然这要求授权服务器和资源服务器能否访问相同的存储,例如DB 或者redis
OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
} else if (accessToken.isExpired()) {
//令牌过期处理
this.tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
} else {
//N 验证token ,如果是 JwtTokenStore ,验证jwt签名
OAuth2Authentication result = this.tokenStore.readAuthentication(accessToken);
if (result == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
} else {
//这里如果我们自己没有配置clientDetailsService,默认共享授权服务的,可能是null
if (this.clientDetailsService != null) {
String clientId = result.getOAuth2Request().getClientId();
try {
this.clientDetailsService.loadClientByClientId(clientId);
} catch (ClientRegistrationException var6) {
throw new InvalidTokenException("Client not valid: " + clientId, var6);
}
}
return result;
}
}
}
这里说明一下 clientDetailsService,上面我们提到 OAuth2AuthenticationManager 和 DefaultTokenServices 中的this.clientDetailsService 都可能是 null 的问题。这是因为他们的取值都是 ResourceServerSecurityConfigurer 中的如下方法,getSharedObject获取的是授权服务配置的 ClientDetailsService。当授权服务与资源服务不是同一个服务的时候,getSharedObject 就会取不到值。
private ClientDetailsService clientDetails() {
//获取授权服务设置的ClientDetailsService
return getBuilder().getSharedObject(ClientDetailsService.class);
}
在前面的博文,我们分析令牌生成时,授权服务只是提到了 AuthorizationServerEndpointsConfigurer 的配置,这里我们补充一下另外两个配置。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private DataSource dataSource;
/**
* 对auth2 提供的接口做访问规则配置和添加自定义过滤器
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//允许表单认证,对于接口/oauth/token,如果开启此配置,并且url中有client_id和client_secret会触发ClientCredentialsTokenEndpointFilter用于校验客户端是否有权限
.allowFormAuthenticationForClients()
//设置接口/oauth/check_token 访问权限,默认denyAll(),资源服务器可以调用这个验证token,如果是jwt,资源服务器也可以自己通过密钥验证
.checkTokenAccess("isAuthenticated()")
//提供jwt的公钥接口 /oauth/token_key
.tokenKeyAccess("permitAll()")
//N 添加自定义的过滤器
//通过AbstractAuthenticationProcessingFilter断点的additionalFilter可以看到该过滤器再链中的位置
//.addTokenEndpointAuthenticationFilter(new MyFilterFive())
//.passwordEncoder()
;
}
/**
* 接入端管理配置,对应的是 oauth_client_details 表,实体是 BaseClientDetails
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
/*clients
.inMemory()
//client_1是客户端授权方式
.withClient("client_1")//接入端id
.secret(new BCryptPasswordEncoder().encode("123456"))//接入端密钥
//.resourceIds(DEMO_RESOURCE_ID)//资源id
.authorizedGrantTypes("client_credentials", "refresh_token")//授权方式
.scopes("select")//访问域
.authorities("client");//权限*/
clients.jdbc(dataSource);
//clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}
/**
* SpringSecurity-OAuth2 提供端口
* /oauth/authorize:授权端口
* /oauth/token:令牌端口
* /oauth/confirm_access:用户确认授权提交端口
* /oauth/error:授权服务错误信息端口
* /oauth/check_token:用于资源服务器访问的令牌解析端口
* /oauth/token_key:提供公有秘钥端口,如果使用的是 JWT 令牌的话
* pathMapping 可以映射成其他地址
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.pathMapping("/oauth/token","/cloneli/token")
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)//允许的请求方式
//tokenStore默认内存存储,重启服务token就会失效
.tokenStore(new InMemoryTokenStore())
.reuseRefreshTokens(true)
//.tokenEnhancer()
.accessTokenConverter(jwtAccessTokenConverter())
//用于配置密码式的授权方式,如果不设置,密码模式请求token是,token为null,TokenEndpoint会提示不支持password授权模式,这里配置就是parent AuthenticationManager
//.authenticationManager(authenticationManager())
/*.tokenGranter(new TokenGranter() {
@Override
public OAuth2AccessToken grant(String s, TokenRequest tokenRequest) {
return null;
}
})*/
;
}
}
在上面AuthorizationServerSecurityConfigurer 的配置中,可以看到 我们设置了 “/oauth/check_token” 接口.checkTokenAccess(“isAuthenticated()”),即需要授权才能访问该接口。
而权限的配置就在下面 AuthorizationServerSecurityConfiguration 的方法中,同时有包括setSharedObject 的ClientDetailsService,因此授权服务和资源服务不是同一个服务,也就没有@EnableAuthorizationServer注解,就不会执行setSharedObject 的操作,这就是为什么getSharedObject(ClientDetailsService.class)可能是null的原因。
@Configuration
@Order(0)
//ClientDetailsServiceConfiguration 提供clientDetailsService的bean
@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class })
public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
......
@Autowired
private ClientDetailsService clientDetailsService;
......
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
http.apply(configurer);
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}
// @formatter:off
http
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
//设置接口的访问权限
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
// @formatter:on
//setSharedObject clientDetailsService
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
......
}
//没有@EnableAuthorizationServer注解,就不会执行AuthorizationServerSecurityConfiguration的方法
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
}
见名知意,这个需要调用其他服务验证 access_token,显然能提供这个服务的应该就是授权服务了。授权服务提供了 CheckTokenEndpoint 用于验证 access_token 的接口如下,而其底层实现也是 resourceServerTokenServices 。
/oauth/check_token
调用授权服务器的 “/oauth/check_token” 接口,我们上面提到需要授权服务器设置访问权限,这里用的是客户端模式。
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
//N 设置请求头,客户端凭证,访问授权服务器需要认证
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
//N 设置请求地址,并发送请求获取验证结果
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
if (map.containsKey("error")) {
logger.debug("check_token returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
//N 构造 OAuth2Authentication 对象
return tokenConverter.extractAuthentication(map);
}
当然如果上面两种校验 access_token 的实现不能满足项目需求,也可以自定义自己的ResourceServerTokenServices 实现类,重写验证access_token的逻辑。
综上,整个令牌验证的流程就是 ResourceServerSecurityConfigurer指定了OAuth2AuthenticationProcessingFilter过滤器,过滤器调用 AuthenticationManager, AuthenticationManager 又调用 ResourceServerTokenServices 实现令牌验证,其中具体实现我们可以通过资源服务配置进行修改。
到这里,令牌验证通过,Authentication的信息就会放到线程变量SecurityContext中,然后过滤器就会放行请求。
不过验证令牌后得到的权限信息还没有用到。这个我们后续的文章再继续研究
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
......
}