spring-security-oauth2客户端@EnableOauth2Sso注解过滤器整理

[img]http://dl2.iteye.com/upload/attachment/0127/7118/4005789e-9308-3fe7-b1a6-50c0601416f3.jpg[/img]

目前使用 @EnableOauth2Sso注解以及 securityConfiguration 配置 security 拦截器有13道
分别是
[list]
[size=large][b]WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
CSRFHeaderFilter(自定义的过滤器,添加在CsrfFilter后)
LogoutFilter
OAuth2ClientAuthenticationProcessingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor[/b][/size]
[/list]
[size=large][b]WebAsyncManagerIntegrationFilter[/b][/size]

public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
if (securityProcessingInterceptor == null) {
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
new SecurityContextCallableProcessingInterceptor());
}

filterChain.doFilter(request, response);
}
}

提供了对securityContext和WebAsyncManager的集成。方式是通过SecurityContextCallableProcessingInterceptor的beforeConcurrentHandling(NativeWebRequest, Callable)方法来讲SecurityContext设置到Callable上。
SecurityContextCallableProcessingInterceptor:支持spring mvc Callable集成。当SecurityContextCallableProcessingInterceptor执行preProcess(NativeWebRequest, Callable)方法时将注入的SecurityContext传递给SecurityContextHolder。

[size=large][b]SecurityContextPersistenceFilter[/b][/size]

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());
}
}

HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

try {
SecurityContextHolder.setContext(contextBeforeChainExecution);

chain.doFilter(holder.getRequest(), holder.getResponse());

}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);

if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}

SecurityContextPersistenceFilter主要是在SecurityContextRepository中保存更新一个securityContext,并将securityContext给以后的过滤器使用

SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

是通过httpSessionSecurityContextRepository.loadContext()方法 从session中获取securityContext,如果获取不到,则创建一个空的,存到SecurityContextHolder中,提供给后面的过滤器使用。
Finally 方法 将securityContext储存到httpSessionSecurityContextRepository中,此时securityContext是包含后面过滤器产生的authentication数据

[size=large][b]HeaderWriterFilter[/b][/size]

securityConfiguration配置一些header信息,在这里进行追加到request的header中

public class HeaderWriterFilter extends OncePerRequestFilter {

/**
* Collection of {@link HeaderWriter} instances to write out the headers to the
* response .
*/
private final List<HeaderWriter> headerWriters;

/**

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

for (HeaderWriter factory : headerWriters) {
factory.writeHeaders(request, response);
}
filterChain.doFilter(request, response);
}

}

例如 实现writeHeaders接口


[size=x-large][b]CsrfFilter[/b][/size]

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
CsrfToken generatedToken = tokenRepository.generateToken(request);
csrfToken = new SaveOnAccessCsrfToken(tokenRepository, request, response,
generatedToken);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);

if (!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 (logger.isDebugEnabled()) {
logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}

filterChain.doFilter(request, response);
}


在跨域的场景下,客户端访问服务端会首先发起get请求,这个get请求在到达服务端的时候,服务端的Spring security会有一个过滤 器 CsrfFilter去检查这个请求,如果这个request请求的http header里面的X-CSRF-COOKIE的token值为空的时候,服务端就好自动生成一个 token值放进这个X-CSRF-COOKIE值里面,客户端在get请求的header里面获取到这个值,如果客户端有表单提交的post请求,则要求客户端要 携带这个token值给服务端,在post请求的header里面设置_csrf属性的token值,提交的方式可以是ajax也可以是放在form里面设置hidden 属性的标签里面提交给服务端,服务端就会根据post请求里面携带的token值进行校验,如果跟服务端发送给合法客户端的token值是一样的,那么 这个post请求就可以受理和处理,如果不一样或者为空,就会被拦截。由于恶意第三方可以劫持session id,而很难获取token值,所以起到了 安全的防护作用。

[size=large][b]CSRFHeaderFilter[/b][/size]
自定义的定义,如果cookie中没有XSRF-TOKEN,则追加

[img]http://dl2.iteye.com/upload/attachment/0127/7121/34ad0427-ae99-3a2f-8a6c-7e888186d406.png[/img]


[size=large][b]LogoutFilter[/b][/size]

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 auth = SecurityContextHolder.getContext().getAuthentication();

if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth
+ "' and transferring to logout destination");
}

for (LogoutHandler handler : handlers) {
handler.logout(request, response, auth);
}

logoutSuccessHandler.onLogoutSuccess(request, response, auth);

return;
}

chain.doFilter(request, response);
}


handler.logout(request, response, auth);

调用securityContextLogoutHandler.logout,清除session,清除SecurityContext

public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
logger.debug("Invalidating session: " + session.getId());
session.invalidate();
}
}

if (clearAuthentication) {
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
}

SecurityContextHolder.clearContext();
}



[size=large][b]OAuth2ClientAuthenticationProcessingFilter[/b][/size]

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {

OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
try {
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));
}
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;
}

}

private void publish(ApplicationEvent event) {
if (eventPublisher!=null) {
eventPublisher.publishEvent(event);
}
}



父类AbstractAuthenticationProcessingFilter 中doFilter方法调用attemptAuthentication()


代码
accessToken = restTemplate.getAccessToken(); 中,如果用户没有通过认证的code,或者使用code去oauth服务换取token时,code已经失效,会在
AuthorizationCodeAccessTokenProvider的getRedirectForAuthorization中抛出UserRedirectRequiredException e


public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {

AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;

if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
throw getRedirectForAuthorization(resource, request);
}
obtainAuthorizationCode(resource, request);
}
return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
getHeadersForTokenRequest(request));

}



[size=large][b]RequestCacheAwareFilter[/b][/size]

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);
}

优先使用缓存的请求
[size=large][b]SecurityContextHolderAwareRequestFilter[/b][/size]
SecurityContextHolderAwareRequestWrapper类对request包装的目的主要是实现servlet api的一些接口方法isUserInRole、getRemoteUser

[size=large][b]AnonymousAuthenticationFilter[/b][/size]
[/code]
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

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);
}

如果SecurityContextHolder中securityContext没有认证,则创建一个anonymousAuthenticationToken

[size=large][b]SessionManagementFilter[/b][/size]
当请求所在的session里没有设置"SPRING_SECURITY_CONTEXT"属性的时候, 才会被这个过滤器拦截. 否则跳到下一个过滤器. 但这个"SPRING_SECURITY_CONTEXT"属性是在用户登录验证完成后就放进session里的(具体参考AbstractAuthenticationProcessingFilter.successfulAuthentication()方法), 所以登录成功后的每一个请求, 它的session里都会有这个属性, 所以就不会被这个过滤器拦截.
有两种情况, 用户发出的请求会被这个过滤器拦截:
1. 当用户重启浏览器, 然后使用remember-me授权成功后, 再直接访问授权后的url. 这时, session里没有了"SPRING_SECURITY_CONTEXT"属性. 那么这个请求会被此过滤器拦截. 这时得到的authentication是RemembermeAuthentication的实例.)
2. 当session因过期而被容器自动销毁时, 若用户继续发出请求, 这个请求会放在一个新的session里, 新session里没有设置"SPRING_SECURITY_CONTEXT"属性, 那么这个请求也会被这个过滤器拦截. 这时得到的authentication是AnonymousAuthentication的实例.


securityContextRepository 是HttpSessionSecurityContextRepository 的实例,第一种的情况的时候,将securityContext固化到redis(目前项目统一使用spring-session整合spring-data-redis)
[code="java"]
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);

if (!securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();

if (authentication != null && !trustResolver.isAnonymous(authentication)) {
// The user has been authenticated during the current request, so call the
// session strategy
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;
}
// Eagerly save the security context to make it available for any possible
// re-entrant
// requests which may occur before the current request completes.
// SEC-1396.
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);
}

[size=large][b]ExceptionTranslationFilter[/b][/size]
统一处理之后的过滤抛出异常的过滤器

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) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);

if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}

if (ase != null) {
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);
}
}

在这个过滤器的 try 部分中会继续执行过滤器链中剩余的过滤器,后面会执行的一个过滤器是 FilterSecurityInterceptor,这个过滤器是用来处理授权的,就是验证用户是否有权访问某个资源,如果没有权限,就会抛出 AccessDeniedException,此时就会进入 handleSpringSecurityException 方法执行。
此时会访问 /login ,在OAuth2ClientAuthenticationProcessingFilter过滤器中obtainAccessToken
取不到code 会抛出 UserRedirectRequiredException e,再由OAuth2ClientContextFilter
捕获 UserRedirectRequiredException重定向到oauth服务的
https://oauth.rubikstack.com/oauth/authorize?client_id=xxx&redirect_uri=https://......

public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
request.setAttribute(CURRENT_URI, calculateCurrentUri(request));

try {
chain.doFilter(servletRequest, servletResponse);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
.getFirstThrowableOfType(
UserRedirectRequiredException.class, causeChain);
if (redirect != null) {
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);
}
}
}


[size=large][b]FilterSecurityInterceptor[/b][/size]
简化授权和访问控制决定,委托一个AccessDecisionManager完成授权的判断
这个filter是filterchain中比较复杂,也是比较核心的过滤器,主要负责授权的工作


public void doFilter(ServletRequest request, ServletResponse response, FilterChin chain)
throws IOException, ServletException {
//封装request, response, chain,方便参数传递、增加代码阅读性
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}

主要逻辑
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));

throw accessDeniedException;
}
[color=red]AffirmativeBased:只要有一个voter投同意票,就授权成功
ConsensusBased:只要投同意票的大于投反对票的,就授权成功
UnanimousBased:需要一致通过才授权成功[/color]
这里用 [b]AffirmativeBased决策管理器[/b]
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;

for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);

if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}

switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;

case AccessDecisionVoter.ACCESS_DENIED:
deny++;

break;

default:
break;
}
}

if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}

// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值