@[TOC](spring security OAuth实践(未完待续))
参考文章
核心filter
spring security维护了一个FilterChainProxy
,这个类会依次调用spring security过滤器链,默认的过滤器链是11个,加上自定义的(MyusernamePasswordAuthentication
是自定义过滤器,用于代替默认的UsernamePasswordAuthentication
)一个是十二个,如图,调用顺序从0-12
SecurityContextPersistenceFilter
整个Spring Security 过滤器链的开端,它有两个作用:一是当请求到来时,检查Session中是否存在SecurityContext
,如果不存在,就创建一个新的SecurityContext
。二是请求结束时将SecurityContext
放入 session
中,并清空 SecurityContextHolder
。
UsernamePasswordAuthenticationFilter
继承自抽象类 AbstractAuthenticationProcessingFilter
,当进行表单登录时,该Filter将用户名和密码封装成一个 UsernamePasswordAuthentication
进行验证。
改filter主要校验表单参数,之后封装成一个 UsernamePasswordAuthentication
,如果需要修改spring security自动生成的表单元素(如添加一个验证码参数),需要自定义一个UsernamePasswordAuthenticationFilter
,在过滤器配置自定义filter以替换UsernamePasswordAuthenticationFilter
实现表单校验构造UsernamePasswordAuthentication
AnonymousAuthenticationFilter
匿名身份过滤器,当前面的Filter认证后依然没有用户信息时,该Filter会生成一个匿名身份——AnonymousAuthenticationToken
。一般的作用是用于匿名登录。
ExceptionTranslationFilter
异常转换过滤器,用于处理 FilterSecurityInterceptor
抛出的异常。
过滤器链经过此filter时会进行try…catch…,如果后面的filter抛出异常,会在此处捕获并处理(如后面的FilterSecurityInterceptor
投票后抛出SpringSecurityException
)
FilterSecurityInterceptor
过滤器链最后的关卡,从 SecurityContextHolder
中获取 Authentication
,比对用户拥有的权限和所访问资源需要的权限。
认证过程
认证过程之后再补充
如果需要自定义认证过程需要自定义LoginAuthenticationProvide
实现AuthenticationProvider
接口(AuthenticationProvider
默认的实现是DaoAuthenticationProvider
)
重写authenticate
和supports
方法
authenticate
方法参考DaoAuthenticationProvider
类的authenticate
方法,认证成功返回一个UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities())
对象,认证失败抛出AuthenticationException
异常,AuthenticationException
异常是一个spring security的抽象异常,它有很多子类实现,其中之一就是UsernameNotFoundException
和BadCredentialsException
读取保存在session中的认证信息
SecurityContextPersistenceFilter
当我们填写表单完毕后,点击登录按钮,请求先经过 SecurityContextPersistenceFilter
过滤器,在前面就曾提到,该Filter有两个作用,其中之一就是在请求到来时,创建 SecurityContext
安全上下文,我们来看看它内部是如何做的,部分源码如下:
public class SecurityContextPersistenceFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
/**安全上下文存储的仓库*/
private SecurityContextRepository repo;
private boolean forceEagerSessionCreation = false;
/**使用HttpSession来存储 SecurityContext*/
public SecurityContextPersistenceFilter() {
this(new HttpSessionSecurityContextRepository());
}
public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
this.repo = repo;
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
/**如果是第一次请求,request中肯定没有FILTER_APPLIED属性*/
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 设置 FILTER_APPLIED 属性为 true,
* 这样同一个请求再次访问时,就直接进入后续Filter的操作
*/
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}
/**
* 封装 requset 和 response
*/
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
/**
* 从存储安全上下文的仓库中载入 SecurityContext 安全上下文,
* 其内部是从 Session中获取上下文信息
*/
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
/**
* 安全上下文信息设置到 SecurityContextHolder 中,
* 以便在同一个线程中,后续访问 SecurityContextHolder
* 能获取到 SecuritContext*/
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();
/**
* 将安全上下文信息存储到 Session中,相当于登录态的维护
*/
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
this.forceEagerSessionCreation = forceEagerSessionCreation;
}
}
请求到来时,利用HttpSessionSecurityContextRepository
读取安全上下文。我们这里是第一次请求,读取的安全上下文中是没有 Authentication身份信息的,将安全上下文设置到 SecurityContextHolder
之后,进入下一个过滤器。
请求结束时,同样利用HttpSessionSecurityContextRepository
该存储安全上下文的仓库将认证后的SecurityContext
放入 session
中,这也是登录态维护的关键,具体调用的是 SecurityContextRepository
的saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
HttpSessionSecurityContextRepository
HttpSessionSecurityContextRepository
的save()
方法
@Override
protected void saveContext(SecurityContext context) {
/**从上下文中获取到认证信息*/
final Authentication authentication = context.getAuthentication();
HttpSession httpSession = request.getSession(false);
// See SEC-776
/**认证信息为空或者是匿名用户*/
if (authentication == null || trustResolver.isAnonymous(authentication)) {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
}
if (httpSession != null && authBeforeExecution != null) {
// SEC-1587 A non-anonymous context may still be in the session
// SEC-1735 remove if the contextBeforeExecution was not anonymous
httpSession.removeAttribute(springSecurityContextKey);
}
return;
}
if (httpSession == null) {
httpSession = createNewSessionIfAllowed(context);
}
// If HttpSession exists, store current SecurityContext but only if it has
// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
if (httpSession != null) {
// We may have a new session, so check also whether the context attribute
// is set SEC-1561
if (contextChanged(context)
|| httpSession.getAttribute(springSecurityContextKey) == null) {
/**设置上下文信息到session的attribute中,保存了认证状态*/
httpSession.setAttribute(springSecurityContextKey, context);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext '" + context
+ "' stored to HttpSession: '" + httpSession);
}
}
}
}
SecurityContext
SecurityContext
是一个接口,有两个方法
- 获取认证信息和设置认证信息
UsernamePasswordAuthenticationFilter
的父类是 AbstractAuthenticationProcessingFilter
,首先进入父类的 doFilter方法,部分源码如下:
该doFilter方法中一个核心就是调用子类 UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法,该方法进入真正的认证过程,并返回认证后的 Authentication
,该方法的源码如下:
该方法中有一个关键点就是 this.getAuthenticationManager().authenticate(authRequest)
,调用内部的AuthenticationManager
去认证,在之前的文章就介绍过AuthenticationManager
,它是身份认证的核心接口,它的实现类是 ProviderManager
,而 ProviderManager
又将请求委托给一个 AuthenticationProvider
列表,列表中的每一个 AuthenticationProvider
将会被依次查询是否需要通过其进行验证,每个 provider的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication
对象的所有属性
下面来分析一个关键的 AuthenticationProvider
,它就是 DaoAuthenticationProvider,它是框架最早的provider,也是最最常用的 provider。大多数情况下我们会依靠它来进行身份认证,它的父类是 AbstractUserDetailsAuthenticationProvider
,认证过程首先会调用父类的 authenticate
方法,核心源码如下:
从上面一大串源码中,提取几个关键的方法:
retrieveUser(…): 调用子类 DaoAuthenticationProvider
的 retrieveUser()
方法获取 UserDetails
preAuthenticationChecks.check(user)
: 对从上面获取的UserDetails进行预检查,即判断用户是否锁定,是否可用以及用户是否过期
additionalAuthenticationChecks(user,authentication)
: 对UserDetails附加的检查,对传入的Authentication与获取的UserDetails进行密码匹配
postAuthenticationChecks.check(user)
: 对UserDetails进行后检查,即检查UserDetails的密码是否过期
createSuccessAuthentication(principalToReturn, authentication, user)
: 上面所有检查成功后,利用传入的Authentication 和获取的UserDetails生成一个成功验证的Authentication
如何自定义登录security
构造一个org.springframework.security.core.userdetails.User( user.getAccountID() + "|" + user.getUserName(), user.getPassword(), authorities);
对象
构造一个UsernamePasswordAuthenticationToken
通过authenticate(usernamePasswordAuthenticationToken)
进行认证,认证之后保存在上下文
SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(authentication);
调用结束SecurityContextPersistenceFilter
会将上下文保存在session中,这样就将自定义的登录与security的认证兼容了(可以不使用security的认证了)
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, password, userDetails.getAuthorities());
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authentication);
还有一种不经过security登录认证兼容已有登录
在 WebSecurityConfigurerAdapter
的子类配置中,配置一个自定义filter,插入在
SecurityContextPersistenceFilter
前,在这个filter中兼容已有登录状态,将认证信息放在session的attribute中,这样执行到SecurityContextPersistenceFilter
时也可以从session中加载到认证信息
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
username, "", authorities);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, "", userDetails.getAuthorities());
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(usernamePasswordAuthenticationToken);
request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
http.addFilterBefore(accessTokenFilter, StopSecurityDefaultEndpointFilter.class);
自定义退出登录
退出登录源码参考
security的默认退出登录逻辑在LogoutFilter
维护了一个LogoutSuccessHandler
,这个类决定了退出登录成功后的重定向,默认是SimpleUrlLogoutSuccessHandler
,handler默认是CompositeLogoutHandler
,如果想要修改重定向的方式,可以自定义一个LogoutSuccessHandler配置 在 SecurityConfig中配置,http.logout() .logoutSuccessHandler(logoutSuccessHandler)
自定义未认证处理
实现AccessDeniedHandler
接口,重写handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
方法自定义返回值即可
自定义TokenEndPoint异常处理
ProviderManager
ProviderManager
实现WebResponseExceptionTranslator
接口,重写translate(Exception e)
方法,
获取公钥端点和校验token端点权限配置
security.tokenKeyAccess("isAuthenticated()").checkTokenAccess("isAuthenticated()")
这两个端点是跨域配置登录后访问的,如果使用的是自定义登录的方式,配置security.addTokenEndpointAuthenticationFilter(MyEndpointFilter)
MyEndpointFilter是自己实现的filter,在此filter中兼容自己的登录方式,把认证信息存储在session的attribute中
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
username, "", authorities);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, "", userDetails.getAuthorities());
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(usernamePasswordAuthenticationToken);
request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
这样,这两个端点检查认证的时候就可以识别到用户已经登录了
如果需要修改未登录范湖IDE错误,可以实现AuthenticationEntryPoint
接口,实现commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3)
方法,配置security.authenticationEntryPoint(MyauthenticationEntryPoint)
就可以了