springcloud这样的分布式架构体系下,目前大部分采用的方案都是认证服务器和资源服务器分离的模式以达到统一认证的目的.
首先搭建一个包含注册中心,网关,认证服务器和一个资源服务器,如何搭建不多做赘述了,网上有很多详细的教程.搭建完毕后,从网关访问数据保证搭建的是否正常执行.
这时候我们绕过网关,直接访问对应启动的资源服务节点,发现依然有相关的权限验证.为了验证权限是在资源服务器验证还是认证服务器执行的,我们注释掉资源服务器的配置
再次去访问资源服务器的接口,发现不管带不带正确的token都是无法通过认证的.所以可以确定统一认证也是需要资源服务器本身进行相关的权限验证的.
我们把注释的放开,重新访问接口.oauth2通常执行链通常由additionalFilters和originalChain 组成. 而我们定义的additionalFilters 执行链如下
我们关注OAuth2AuthenticationProcessingFilter以及最后FilterSecurityInterceptor其实就可以了.
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);
}
这是一个过滤器,所以我们可以看到,在
这里解析了request请求,里面遍历了头信息去获取token.假设没有token就清除上下文,然后过滤器放行.
假设携带了token就要去解析token 的有效性,
这里调用了认证管理器去解析授权信息.我们主要就分析这里面的解析和认证.
具体来说,是使用他的子类 OAuth2AuthenticationManager 的authenticate方法.
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;
}
这段代码上面的注释比较清晰的说明了他的功能
/**
* Expects the incoming authentication request to have a principal value that is an access token value (e.g. from an
* authorization header). Loads an authentication from the {@link ResourceServerTokenServices} and checks that the
* resource id is contained in the {@link AuthorizationRequest} (if one is specified). Also copies authentication
* details over from the input to the output (e.g. typically so that the access token value and request details can
* be reported later).
*
* @param authentication an authentication request containing an access token value as the principal
* @return an {@link OAuth2Authentication}
*
* @see org.springframework.security.authentication.AuthenticationManager#authenticate(org.springframework.security.core.Authentication)
*/
这里主要是调用了ResourceServerTokenServices的子类UserInfoTokenServices的loadAuthentication方法
此时的这个userInfoEndpointUrl就是我们在我们的资源配置文件中配置的地址
然后在这个getMap方法中
我们可以看到他创建了一个restTemplate,经过层层调用后走进了restTenplate的子类OAuth2RestTemplate的
在里面又继续调用了父类restTemplate的doExecute方法
此时他创建了ClientHttpRequest去请求我们配置的地址,然后根据响应的response进行处理
在处理响应回来的response时,
判断里面是否包含error,如果有就开始处理这个error.这个处理就不详细说了,大概就是递归方法调用,然后只处理状态码是400也就是权限相关的错误,类似500的就不处理.
此时再回头看restTeplate的doExecute方法,最后根据处理结果决定返回的是null还是responseExtractor.extractData(response).
这个responseExtractor.extractData(response)基本上包含了所有的授权信息.
此时返回到OAuth2AuthenticationManager中,可以看到
判断了我们返回的授权结果,是null即该次token的授权未通过,就抛出异常.我们经常看到的
其实就是由这里抛出来的.如果不是null那么他会继续校验授权信息.后面还有关于clientId的一些处理包括是JDBC(先从redis取)还是inmemory,有兴趣的可以继续研究.
所以即使是统一认证也是分两部分的,也是需要资源服务器的执行链先去处理,在当中把token传递到配置的路径中,去校验获取返回信息.
最后就是该次过滤器放行,继续后续的处理. 往后有时间会继续尝试研究下后面的处理.