项目场景:
既然前面有说到spring security 是如何验证当前用户以及获取到当前用户信息,哪么spring security auth2.0又是如何验证当前用户信息的呢?
技术详解:
spring security auth2.0验证用户信息其实更加简单,具体的逻辑就在OAuth2AuthenticationProcessingFilter,我们先一起看看OAuth2AuthenticationProcessingFilter的源码吧.
OAuth2AuthenticationProcessingFilter.java
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
try {
Authentication authentication = tokenExtractor.extract(request);
if (authentication == null) {
if (stateless && isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
Authentication authResult = authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
catch (OAuth2Exception failed) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + failed);
}
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));
return;
}
chain.doFilter(request, response);
}
}
内部的逻辑可以细分为以下几个步骤:
第一步:获取token的信息
第二步:如果获取token结果为null,哪么就清空security context
第三步:根据token信息获取到对应的用户信息
第四步:如果执行异常,哪么就抛出异常
我们现在来每一步进行分析,首先是获取token信息。
获取token信息主要使用tokenExtractor,而这里我们使用BearerTokenExtractor即可。我们先看看BearerTokenExtractor内部是如何操作的吧。
BearerTokenExtractor.java
public class BearerTokenExtractor implements TokenExtractor {
private final static Log logger = LogFactory.getLog(BearerTokenExtractor.class);
@Override
public Authentication extract(HttpServletRequest request) {
String tokenValue = extractToken(request);
if (tokenValue != null) {
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
return authentication;
}
return null;
}
protected String extractToken(HttpServletRequest request) {
// first check the header...
String token = extractHeaderToken(request);
// bearer type allows a request parameter as well
if (token == null) {
logger.debug("Token not found in headers. Trying request parameters.");
token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
if (token == null) {
logger.debug("Token not found in request parameters. Not an OAuth2 request.");
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
}
}
return token;
}
/**
* Extract the OAuth bearer token from a header.
*
* @param request The request.
* @return The token, or null if no OAuth authorization header was supplied.
*/
protected String extractHeaderToken(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders("Authorization");
while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
String value = headers.nextElement();
if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
// Add this here for the auth details later. Would be better to change the signature of this method.
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
int commaIndex = authHeaderValue.indexOf(',');
if (commaIndex > 0) {
authHeaderValue = authHeaderValue.substring(0, commaIndex);
}
return authHeaderValue;
}
}
return null;
}
}
BearerTokenExtractor获取token的逻辑其实很简单,首先根据请求头header里面的Authorization,然后再根据Bearer的编码方式进行解码,获取到token信息,如果没有获取到哪么再根据请求地址上是否存在access_token这个参数。因此如果请求的时候可以有两种方式携带token,一种是请求头Authorization,另外一种是请求url携带access_token。
第二步:将token信息封装成PreAuthenticatedAuthenticationToken
第三步:认证PreAuthenticatedAuthenticationToken
认证的过程中,就需要使用authenticationManager,而这里使用的并不是ProviderManager而是OAuth2AuthenticationManager,我们现在看看OAuth2AuthenticationManager里面到底做了什么吧
public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
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;
}
}
可以从源码里面看到,正在根据token信息获取用户信息主要是ResourceServerTokenServices,而ResourceServerTokenServices也是一个接口,主要有多个实现类,分别是
DefaultTokenServices:默认实现,主要是从TokenStore获取到对应的用户信息
RemoteTokenServices:主要是资源服使用,资源服务通过HTTP请求授权服,然后根据授权服务获取到对应的用户信息
UserInfoTokenServices:作用和 RemoteTokenServices差不多,只不过请求的地址是userInfoEndpointUrl
SpringSocialTokenServices:老实说,这个我不知道是干嘛的....囧
然后在OAuth2AuthenticationManager使用的是DefaultTokenServices,我们这回就看看DefaultTokenServices是做了什么吧。
DefaultTokenServices.java
public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
ConsumerTokenServices, InitializingBean {
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
InvalidTokenException {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
if (accessToken == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
else if (accessToken.isExpired()) {
tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
}
OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
if (result == null) {
// in case of race condition
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
}
if (clientDetailsService != null) {
String clientId = result.getOAuth2Request().getClientId();
try {
clientDetailsService.loadClientByClientId(clientId);
}
catch (ClientRegistrationException e) {
throw new InvalidTokenException("Client not valid: " + clientId, e);
}
}
return result;
}
}
从代码里面就可以看出来,DefaultTokenServices主要使用的也是TokenStore,而TokenStore也是一个接口,这里包含了多个实现类,具体如下:
InMemoryTokenStore:基于内存存储,默认是基于内存存储
JdbcTokenStore:基于数据库存储
JwtTokenStore:jwt,主要是通过加密解密的方式来解析token信息
RedisTokenStore:基于redis存储
这里就不一一描述了,感兴趣的可以自己去看看。好的,言归正传我们假设已经根据token信息获取到了用户信息,然后再根据源码我们就知道如果获取了用户信息
第四步:将内容放到SecurityContext
代码如下SecurityContextHolder.getContext().setAuthentication(authResult);
第五步:捕获异常OAuth2AuthenticationEntryPoint
这里也可以自定义捕获异常,然后再根据异常信息返回对应的结果信息。
后记:
综上所述,spring security oauth校验token信息是否有效,核心点就是OAuth2AuthenticationProcessingFilter,如果感兴趣的话可以仔细去看看。