Spring Security 认证
Spring Security 是一个基于过滤器链来提供认证和授权功能的框架,本文主要分析其中的认证过程
过滤器链
过滤器链如下图所示,过滤器链中主要是第二部分用于进行认证。本文分析 UsernamePasswordAuthenticationFilter,它用于处理表单提交的登录请求。SecurityContextPersistenceFilter 主要用于从请求读取或向响应装入认证信息 securityContext
认证处理流程
认证用户名和密码
UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法处理认证
@Override
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());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 向token中添加一些详细信息
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
- 当用户在登录页面输入用户名和密码后,进入 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 类的 attemptAuthentication 方法。这个方法会读取请求中的用户名和密码,并生成 UsernamePasswordAuthenticationToken 这个用于认证的 token
- 调用 setDetails 方法,向 token 中添加一些详细信息,具体就是请求的 ip、session 的信息
- 调用 ProviderManager 中的 authenticate 进行认证工作
ProviderManager 的 authenticate 方法进行认证工作
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
// 遍历所有的providers
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
// 如果provider不支持认证此请求,跳过
continue;
}
try {
// 此provider进行认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
// catches
}
if (result == null && this.parent != null) {
// 这个manager没有认证信息,那就让父manager尝试认证
try {
// 父manager认证并获取结果
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
// catches
}
// 认证成功
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// 如果eraseCredentialsAfterAuthentication为true,在认证完成后抹除credential
((CredentialsContainer) result).eraseCredentials();
}
// 如果父manager认证成功,它会发布认证成功事件,因此子manager就不要再发布了
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// 父manager没能成功认证,抛出ProviderNotFoundException
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// 父manager认证失败,会发布认证失败事件,子manager就不要重复发布了
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
- 遍历所有的 providers(不同 provider 可以认为是不同登录方式的认证器),直到有一个 provider 可以对此认证请求进行认证。
- 如果有 provider 可以认证
- 调用这个 provider 的 authenticate 方法进行认证,得到认证结果 result
- 如果 result 不为 null,将认证后的结果 copy 到传进来的 authentication 当中,结束认证
- 如果这个 providerManager 认证结果为空,但有父 manager,那么就让父 manager 尝试认证,得到 parentResult,并把 parent Result 赋给 result
- 如果上两步得到的 result 不为 null,表示认证成功。否则,认证失败,抛出异常 ProviderNotFoundException
在这里,ProviderManager 只有一个 provider:AnonymousAuthenticationProvider,它不能对本次的认证请求进行认证,因此把认证请求委托给父 manager。父 manager 的也只有一个 provider:DaoAuthenticationProvider。DaoAuthenticationProvider 可以对本次认证请求进行认证
AbstractUserDetailsAuthenticationProvider 的 authenticate 方法
DaoAuthenticationProvider 用于认证 UsernamePasswordAuthenticationToken 信息。它继承了 AbstractUserDetailsAuthenticationProvider 类,authenticate 方法实际上调用的是 AbstractUserDetailsAuthenticationProvider 的方法。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
// 根据用户名从缓存获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) { // 缓存中没有用户信息
cacheWasUsed = false;
try {
// 获取用户信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
// exception handling
}
try {
// 预检查
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 处理异常
// 后置检查
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
// 将用户信息放入缓存中
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
-
根据 authentication 中的用户名,从用户缓存中获取用户信息
-
如果缓存中没有用户信息,调用 retrieveUser 方法来获取用户信息
DaoAuthenticationProvider 的 retrieveUser 方法来获取用户信息源码如下
@Override protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { // 通过userDetailsService得到user UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } // 处理异常 }
获取 userDetailService 组件(在这里是配置类注入的 InMemoryUserDetailsManager),调用其 loadUserByUsername 方法,得到 loadUser。如果不为空,返回即可,否则抛出异常
-
得到用户信息后,第 15 行语句调用 AbstractUserDetailsAuthenticationProvider 的 子类 DefaultPreAuthenticationChecks 的 check 方法,对用户信息进行认证。
@Override public void check(UserDetails user) { // 账号被锁... if (!user.isAccountNonLocked()) {} // 用户未被启用 if (!user.isEnabled()) {} // 用户过期 if (!user.isAccountNonExpired()) {} }
从源码来看,主要判断账号是否被锁、未被启用或者已经过期
-
进行到第 19 行,在预检查后还需要进行后置认证检查,调用的是 AbstractUserDetailsAuthenticationProvider 子类 DefaultPostAuthenticationChecks 的 check 方法,用于检查 credentials 是否过期
-
将用户信息放入缓存中
-
调用 createSuccessAuthentication 方法生成成功的认证对象,这个对象所属的类是 UsernamePasswordAuthenticationToken,包括 principals, credentials, user 的 authorities 和 details
AbstractAuthenticationProcessingFilter 的 successfulAuthentication 方法
UsernamePasswordAuthenticationFilter 的 attempAuthentication 方法认证成功后,会进入 AbstractAuthenticationProcessingFilter 的 successfulAuthentication 方法
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
// ...
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
- 创建一个空的 context
- 将认证结果放进 context 中
- 将 context 放进 SecurityContextHolder 中
- 调用 handler 进行认证成功的处理
SavedRequestAwareAuthenticationSuccessHandler 的 onAuthenticationSuccess 方法
上一步中的最后一个的 handler 是 SavedRequestAwareAuthenticationSuccessHandler,它的 onAuthenticationSuccess 方法的源码如下
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
// 从缓存中得到认证之前的请求
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
// 缓存中没有对应的请求,调用父类的方法,跳转到设置好或者默认的url
super.onAuthenticationSuccess(request, response, authentication);
return;
}
// 得到跳转目标url参数
String targetUrlParameter = getTargetUrlParameter();
// 如果设置了认证成功后总是跳转到某个url
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
这个 handler 主要用于在认证成功后进行页面的跳转。如果在认证前尝试访问某个资源,那么这个请求就会被缓存,在认证成功后从缓存中读出这个请求。如果配置了认证成功后总是跳转到某个 url,那么就跳转到那个 url。如果没有,则跳转到啊之前缓存的那个 url。如果没有缓存,那么就跳转到设置好的 url 或者默认的 target url
认证信息保存和共享
每一个请求都调用服务器的一个线程进行处理。当用户登录认证成功后,之后的请求应当不再需要认证,因此需要通过 session 保存认证信息,用户之后请求的认证。于是乎就有两个问题:1. spring security 怎么利用这个 session 完成身份信息的认证 2. 认证什么时候设置的 session
如何利用session
SecurityContextPersistenceFilter 中的 doFilter 方法中获取 security context
在过滤器链中的 SecurityContextPersistenceFilter 中的 doFilter 方法部分源码如下
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 利用本次的请求和响应新建一个HttpRequestResponseHolder,用于下一句获得security context
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 传入HttpRequestResponseHolder获得security context
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
// 将context放入context holder中
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
}
- 利用本次的请求和响应新建一个HttpRequestResponseHolder,然后将这个 holder 传入 HttpSessionSecurityContextRepository 的 loadContext 方法中,这个方法源码参见 [HttpSessionSecurityContextRepository 的 loadContext 方法](#HttpSessionSecurityContextRepository 的 loadContext 方法)
- 得到 contextBeforeChainExecution 后,将其放入 SecurityContextHolder 中
- 执行 doFilter,让过滤器链后面的过滤器执行
HttpSessionSecurityContextRepository 的 loadContext 方法([跳回SecurityContextPersistenceFilter](#SecurityContextPersistenceFilter 中的 doFilter 方法中获取 security context))
HttpSessionSecurityContextRepository 的 loadContext 方法如下
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
// 根据session获取security context
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
// 没有得到context,新建一个context
context = generateNewContext();
}
// 将holder中的请求和响应包装
SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
return context;
}
从 holder 中取出 session,并将 session 传入 readSecurityContextFromSession 方法。这个方法其实就是从 session 中以 springSecurityContextKey(这是spring security设置的常量字符串 "SPRING_SECURITY_CONTEXT"
)得到 context。然后将 holder 中的请求和响应与 context 进行包装并放回 holder 中
认证后何时设置 session
SecurityContextPersistenceFilter 中的 doFilter 方法中保存 session
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// ...
finally {
// 获取security context
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// 移除holder里的context
SecurityContextHolder.clearContext();
// 在响应中包装context
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
}
}
以上代码是 SecurityContextPersistenceFilter 中 chain.doFilter 语句执行完之后的逻辑,即在后续过滤器链和 controller 处理完成后,又返回到这个方法,然后执行以上代码。
- 从 SecurityContextHolder 中获取 securityContext,然后移除 holder 里的 context
- 由于 SecurityContextPersistenceFilter 之前已经将 requestResponseHolder 中的请求和响应与 context 进行了包装,因此这里可以直接将获得 context 包装进 requestResponseHolder 中的请求和响应。
认证总结
- 在过滤器链中的 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法进行认证
- 在 ProviderManager 中遍历 provider 集合,直到找到一个支持认证当前请求的 provider,调用其 authenticate 方法得到认证结果
- 认证成功后,创建一个包含认证信息的 context,装入 ContextHolder 中
- 执行认证成功后的页面跳转
- 多请求之间的认证信息共享:SecurityContextPersistenceFilter 过滤器在 UsernamePasswordAuthenticationFilter之前,当请求进入 SecurityContextPersistenceFilter 时,获得请求对应的 session,根据 springSecurityContextKey 获得 context,装入 holder 中。当响应返回给前端途中经过 SecurityContextPersistenceFilter 时,从 SecurityContextHolder 中得到 context,然后检查是否有更新,有的话则装入响应中,以便下次请求使用