Spring Security 是一个基于过滤器链来提供认证和授权功能的框架,本文主要分析其中的认证过程
过滤器链如下图所示,过滤器链中主要是第二部分用于进行认证。本文分析 UsernamePasswordAuthenticationFilter,它用于处理表单提交的登录请求。SecurityContextPersistenceFilter 主要用于从请求读取或向响应装入认证信息 securityContext
@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); }
@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; }
在这里,ProviderManager 只有一个 provider:AnonymousAuthenticationProvider,它不能对本次的认证请求进行认证,因此把认证请求委托给父 manager。父 manager 的也只有一个 provider:DaoAuthenticationProvider。DaoAuthenticationProvider 可以对本次认证请求进行认证
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
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); }
上一步中的最后一个的 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
在过滤器链中的 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()); } }
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 中
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 处理完成后,又返回到这个方法,然后执行以上代码。