这几天一直在研究oauth2协议,打算按照oauth2协议做一个认证服务,使用了spring security oauth2作为工具搭建了认证服务器和资源服务器。这篇博客还不会告知如何搭建这两个服务器,我们先来简单地了解一下oauth2的认证流程,当前主要会侧重讲授权码模式。关心Spring security核心Filter创建和工作原理的,可以查看关于spring security中Filter的创建和工作原理
1.基本认证流程
2.spring security oauth2是如何实现整个认证流程的
这个问题确实有点难度,从网上查资料说是过滤器实现的,好吧,既然是过滤器实现的,那咱们开始从过滤器找线索。
首先小伙伴要知道Filter并不属于spring,而是属于tomcat,咱们可以从ApplicationFilterChain这个类入手,启动认证服务器,启动资源服务器,对资源服务器中ApplicationFilterChain的doFilter方法打上断点,开始通过客户端发送请求访问资源服务器,请求会进资源服务器,重点内容在下面这个方法里面:
internalDoFilter(request,response);
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();
上面的方法就是在调用过滤器链,通过debug可以观察到有以下几个过滤器:
仔细看一下,有两个很明显的过滤器,一个是OAuth2ClientContextFilter,springSecurityFilterChain,而且springSecurityFilterChain还是一个代理对象,这两个过滤器肯定和spring security oauth2有关系。
OAuth2ClientContextFilter
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 记录当前地址(currentUri)到HttpServletRequest
request.setAttribute(CURRENT_URI, calculateCurrentUri(request));
try {
// 调用下一个过滤器
chain.doFilter(servletRequest, servletResponse);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// 捕获异常,根据对应的异常发起重定向请求
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
.getFirstThrowableOfType(
UserRedirectRequiredException.class, causeChain);
if (redirect != null) {
// 这个重定向会让客户端去请求认证服务器,
//也就是认证流程示意图中第2步重定向的操作
//会重定向到认证服务器的/oauth/authorize
redirectUser(redirect, request, response);
} else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new NestedServletException("Unhandled exception", ex);
}
}
}
根据上面的注释,发现其实这个过滤器做的事情,就是重定向到认证服务器。
springSecurityFilterChain
因为这个对象是一个代理对象,但是我们可以找到它的被代理类,从ApplicationFilterChain中debug,咱们可以找到FilterChainProxy,代码一直在执行内部类VirtualFilterChain的doFilter方法:
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);
}
}
重点放在additionalFilters这个对象上面:
上图就是spring security相关的Filters,也可以加入自定义Filter。
WebAsyncManagerIntegrationFilter
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 从请求中封装一个WebAsyncManager
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
// 检查是否存在一个SecurityContextCallableProcessingInterceptor
SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
//不存在的话,就创建一个设置进去
if (securityProcessingInterceptor == null) {
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
new SecurityContextCallableProcessingInterceptor());
}
// 调用下一个过滤器
filterChain.doFilter(request, response);
}
这个过滤器的功能就是注册一个SecurityContextCallableProcessingInterceptor,暂时不深究这个拦截器。
SecurityContextPersistenceFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
final boolean debug = logger.isDebugEnabled();
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
// 将request和response封装
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
// 从session中取出SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 将SecurityContext放到SecurityContextHolder中,方便后续过滤器使用
SecurityContextHolder.setContext(contextBeforeChainExecution);
// 执行后续过滤器
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
// 执行完后续过滤器后,取出SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// 清除SecurityContextHolder
SecurityContextHolder.clearContext();
// 将SecurityContextHolder重新设置回session中,
// 因为在执行后续过滤器的时候,有可能发生了变化
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
上述注释可以说明,其实SecurityContextPersistenceFilter就是做了SecurityContext的更新操作。
HeaderWriterFilter
/**
* Filter implementation to add headers to the current response. Can be useful to add
* certain headers which enable browser protection. Like X-Frame-Options, X-XSS-Protection
* and X-Content-Type-Options.
*
* @author Marten Deinum
* @author Josh Cummings
* @since 3.2
*
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request,
response, this.headerWriters);
HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request,
headerWriterResponse);
try {
filterChain.doFilter(headerWriterRequest, headerWriterResponse);
}
finally {
headerWriterResponse.writeHeaders();
}
}
看上面注释
Filter implementation to add headers to the current response
给当前响应添加headers,起到保护浏览器访问的作用。
CsrfFilter
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
}
CsrfFilter 主要是通过验证 CSRF Token 来验证,判断是否受到了跨站点攻击;处理流程简单描述如下,
每当用户登录系统某个页面的时候,通过系统后台随机生成一个 CSRF Token,通过 response 返回给客户端;客户端在发送 POST 表单提交的时候,需要将该 CSRF Token 作为隐藏字段(一般将该表单字段命名为 _csrf)提交到系统后台进行处理;系统后台会在当前的 session 中一直保存该 CSRF Token,这样,当后台收到前端所提交的 CSRF Token 以后,将会与当前 session 中缓存的 CSRF Token 进行比对,若两者相同,则验证通过,若两者不相等,则验证失败,拒绝访问;Spring Security 正式通过这样的逻辑来避免 CSRF 攻击的
LogoutFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 判断是否需要登出
if (requiresLogout(request, response)) {
// 获取Authentication
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth
+ "' and transferring to logout destination");
}
// 处理Authentication,一般就是直接清除
this.handler.logout(request, response, auth);
// 调用登出成功处理器,一般是页面跳转,也可自定义
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
// 执行后续过滤器
chain.doFilter(request, response);
}
上述注释表明,这个过滤器专门处理登出请求的。
OAuth2ClientAuthenticationProcessingFilter
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
// 向认证服务器发送请求获取token
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
try {
//通过token获取Authentication(这是一个解析token的过程)
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
// 发布成功获取Authentication事件
publish(new AuthenticationSuccessEvent(result));
return result;
}
catch (InvalidTokenException e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
}
OAuth2ClientAuthenticationProcessingFilter是专门处理认证流程第6步的一个实现,通过code向认证服务器获取token
RequestCacheAwareFilter
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
这个filter的用途官方解释是
用于用户登录成功后,重新恢复因为登录被打断的请求
这个解释也有几点需要说明
被打算的请求:简单点说就是出现了AuthenticationException、AccessDeniedException两类异常
重新恢复:既然能够恢复,那肯定请求信息被保存到cache中了
SecurityContextHolderAwareRequestFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
(HttpServletResponse) res), res);
}
一行代码,就是对请求做了一个包装。
AnonymousAuthenticationFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 当前面的过滤器都没有设置Authentication的时候,这里会给一个匿名的Authentication
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
// 继续执行后面的过滤器
chain.doFilter(req, res);
}
这个就是用来兜底的过滤器,反正只要没有Authentication,最终都会给一个Authentication。
SessionManagementFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 判断当前session中是否有SPRING_SECURITY_CONTEXT属性
if (!securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
// 判断authentication是否是一个匿名的authentication
if (authentication != null && !trustResolver.isAnonymous(authentication)) {
// 说明用户已经认证成功来,需要保存authentication到session中
try {
sessionAuthenticationStrategy.onAuthentication(authentication,
request, response);
}
catch (SessionAuthenticationException e) {
// The session strategy can reject the authentication
logger.debug(
"SessionAuthenticationStrategy rejected the authentication object",
e);
// 出异常就清除,并调用失败处理器
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, e);
return;
}
//把SecurityContext设置到当前session中
securityContextRepository.saveContext(SecurityContextHolder.getContext(),
request, response);
}
else {
// No security context or authentication present. Check for a session
// timeout
if (request.getRequestedSessionId() != null
&& !request.isRequestedSessionIdValid()) {
if (logger.isDebugEnabled()) {
logger.debug("Requested session ID "
+ request.getRequestedSessionId() + " is invalid.");
}
if (invalidSessionStrategy != null) {
invalidSessionStrategy
.onInvalidSessionDetected(request, response);
return;
}
}
}
}
chain.doFilter(request, response);
}
这个过滤器看名字就知道是管理session的了。
ExceptionTranslationFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// 获取后续过滤器抛出的异常
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
// 看看能不能拿到AuthenticationException
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
// 如果不是AuthenticationException,就看看是不是AccessDeniedException
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
// 真正处理AuthenticationException或AccessDeniedException
// 如果捕获到的异常是AuthenticationException,就重新执行认证
// 如果捕获到的异常是AccessDeniedException,再进一步执行下面的判断
// 如果当前的认证形式是Anonymous或者RememberMe,则重新执行认证
// 否则就是当前认证用户没有权限访问被请求资源,调用accessDeniedHandler.handle方法
handleSpringSecurityException(request, response, chain, ase);
}
// 非以上两种异常,都抛出
else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
}
else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
好吧,这个也好理解,就是专门处理异常的过滤器。
FilterSecurityInterceptor
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// OncePerRequestFilter子类会执行这里
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 执行父类beforeInvocation,类似于aop中的before
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 执行过滤器链
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
// 这方法就是获取ConfigAttribute和Authentication
// 通过ConfigAttribute和Authentication判断当前请求是否允许正常通过
// 不允许的话,就抛出对应的异常
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
这个过滤器就是根据配置和权限决定请求是否正常通过,这是一个最终的决断器。
认证过程
1.当请求/client/user进资源服务器,最终请求到达AnonymousAuthenticationFilter,就拿到一个匿名Authentication,通过FilterSecurityInterceptor决断后抛出AccessDeniedException,ExceptionTranslationFilter根据异常会重定向到资源服务器登录页面
2.当请求资源服务器登录页面请求进来后,到达OAuth2ClientAuthenticationProcessingFilter,
AuthorizationCodeAccessTokenProvider
if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
throw getRedirectForAuthorization(resource, request);
}
当没有从登录请求中拿到code和state参数时,抛出UserRedirectRequiredException,该异常会向上抛出被OAuth2ClientContextFilter捕获,OAuth2ClientContextFilter针对该异常准备好URL和请求参数,并告知客户端向认证服务器重定向。
3.当认证服务器接收到/oauth/auhorize请求的时候,最终还是给了一个AnonymousAuthentication,领到了一个AccessDeniedException,被告知要重定向到认证服务器的登录页面。
4.然后就乖乖地请求认证服务器的登录页面啦,被DefaultLoginPageGeneratingFilter截获,用户看到了登录页面。用户输入用户名密码,提交表单给认证服务器,被UsernamePasswordAuthenticationFilter截获,用户登录成功后认证服务器携带code告知客户端重定向到资源服务器登录请求。
5.客户端重定向到指定URL后,资源服务器通过OAuth2ClientAuthenticationProcessingFilter获取code。
6.资源服务器通过OAuth2ClientAuthenticationProcessingFilter获取code后,再发送请求到认证服务器获取token。认证服务器接收到/oauth/token请求,最终在TokenEndpoint生成token。
7.资源服务器拿到token后,说明用户认证通过,资源服务器告知用户重定向到第一次请求。
8.客户端根据指示重定向到第一次发起的请求,资源服务器此时已经持有用户认证信息,就能正常提供服务。
以上就是我对于spring security oauth2认证过程的一个简单分析,大部分分析内容都是针对资源服务器,认证服务器略有不同,小伙伴可以利用这个方法针对认证服务器做分析。