Spring-security概述
整体来讲,spring-security就是一个过滤器链filter-chain,由多个filter共同完成整体的登出,认证,鉴权功能。部分filter会根据配置动态的删除或增加,比如basicAuthenticationFilter.
整个过滤器链作为一个filter加入整体应用的过滤器链。
这是未加入任何配置spring-security所生成的filter-chain。
他们对应在整体过滤器链中的位置
对springsecurity定制开发的本质就是修改部分过滤器可替换组件,以及通过在spring-security过滤器链中添加自己的filter.
源码解析
springSecurity比较重和常见修改使用的过滤器为 LogoutFilter(登出控制),UsernamePasswordAuthenticationFilter(认证逻辑filter),FilterSecurityInterceptor(鉴权)。下面分别通过大致源码来梳理下他们的代码逻辑。
本文档内容所有handler都可以通过配置进行替换修改为自己的handler
LogoutFilter
登出Filter,有三个参数如下:
//path匹配,用于判断是否是登出请求
private RequestMatcher logoutRequestMatcher;
//登出处理,固定为CompositeLogoutHandler,CompositeLogoutHandler包含多个LogoutHandler
//内部LogoutHandler可自定义,执行登出逻辑时会循环执行所有LogoutHandler.logout方法。
private final LogoutHandler handler;
//登出成功处理Handler,可自定义,常见修改就是自定义登出接口。
private final LogoutSuccessHandler logoutSuccessHandler;
源代码逻辑解析:
因为这部分很简单就直接贴过来了。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//判断是否是登出请求
if (this.requiresLogout(request, response)) {
//从SecurityContextHolder中获取用户信息
Authentication auth = SecurityContextHolder中获取用户信息.getContext().getAuthentication();
//CompositeLogoutHandler处理,它会将所有LogoutHandler的logout方法执行一遍。
this.handler.logout(request, response, auth);
//执行登出成功逻辑。默认为SimpleUrlLogoutSuccessHandler,登出成功重定向到登陆页面。
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
} else {
chain.doFilter(request, response);
}
}
UsernamePasswordAuthenticationFilter
认证逻辑Filter,非常重要的Filter.
UsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter。实现了attemptAuthentication。
首先是AbstractAuthenticationProcessingFilter的整体doFilter逻辑
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//判断是否是登录请求
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
try {
//具体filter实现的attemptAuthentication
Authentication authenticationResult = this.attemptAuthentication(request, response);
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//登陆成功,默认会执行successHandler的onAuthenticationSuccess方法
this.successfulAuthentication(request, response, chain, authenticationResult);
} catch (InternalAuthenticationServiceException var5) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
this.unsuccessfulAuthentication(request, response, var5);
} catch (AuthenticationException var6) {
//这里捕获的AuthenticationException异常,所以一般在登录失败逻辑中抛出此类型异常,然后
//执行unsuccessfulAuthentication。默认为执行failureHandler.onAuthenticationFailure
this.unsuccessfulAuthentication(request, response, var6);
}
}
}
然后具体看下UsernamePasswordAuthenticationFilter的attemptAuthentication方法
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
整体流程很清晰,就是拿到username,password然后交给AuthenticationManager处理。
默认实现为ProviderManager,具体代码
//包含一个AuthenticationProvider数组
private List<AuthenticationProvider数组> providers;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
while(var9.hasNext()) {
//循环调用AuthenticationProvider,
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
//看是否支持当前类型
if (provider.supports(toTest)) {
...
try {
//这里对所有的AuthenticationException异常都先进行捕获,在所有provider执行完,再进行处理
//这里就是多个provider只要有一个对这个信息认证成功了,就是成功了。
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
...
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
...
if (result != null) {
...
return result;
} else {
// 如果不成功,对异常的处理
}
}
总结:登录整体流程 filter拦截登录请求->manager的authenticate方法–>provider的authenticate方法。
如果我要自定义一套自己的登录逻辑可以怎么做。
第一种可以只增加provider,因为只要有一个provider认证成功即为成功。
第二种就是替换自己的manger
第三种就是在UsernamePasswordAuthenticationFilter前增加自己的登录filter.目前koca是通过自己的filter实现多种登录拦截
FilterSecurityInterceptor 鉴权
鉴权虽然名字叫做Interceptor 但其实他还是实现了Filter接口
FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter
dofilter方法只是包装FilterInvocation类执行invoke方法。
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
//防止重复鉴权
if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} else {
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
//都是执行的父类方法,beforeInvocation是具体的鉴权逻辑
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
在父方法beforeInvocation调用了attemptAuthorization方法。
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, Authentication authenticated) {
try {
//使用accessDecisionManager.decide鉴权
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException var5) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object, attributes, this.accessDecisionManager));
} else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
}
this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var5));
//不成功抛出异常。
throw var5;
}
}
补充一点,校验不成功抛出的AccessDeniedException异常是谁处理的呢。
这之前的一个filter,ExceptionTranslationFilter的doFilter逻辑
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
//通过try catch去捕获后续filter的异常
chain.doFilter(request, response);
} catch (IOException var7) {
throw var7;
} catch (Exception var8) {
//只获取AuthenticationException和AccessDeniedException异常,其余的直接抛
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);
RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
//直接抛
if (securityException == null) {
this.rethrow(var8);
}
...
//根据类型处理异常,最后通过accessDeniedHandler进行处理
this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
}
}
koca安全认证功能与实现
-
登录功能
通过在UsernamePassword之前配置新的认证Filter,包含Filter,manager,provider。
包含认证成功以及失败处理handler
-
token功能
校验是添加Filter拦截器实现。
生成是通过认证成功handler处理。
-
第三方应用授权
也是通过Filter添加实现
-
鉴权
是通过替换FilterSecurityInterceptor实现自己的鉴权逻辑
-
其余功能
包含异常处理handler等,