在用户请求成功认证后,会将认证成功后的上下文信息SecurityContext通过该类存储在HttpSession中。当用户下次进行请求时,可以直接从通过该类从HttpSession中获取已经认证后的上下文信息SecurityContext,从而避免重复认证。
先来看下该类的 doFilter() 方法
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); // 获取Spring Security上下文对象 SecurityContext 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(); // 将 SecurityContext 中的信息保存到 HttpSession 中 repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); if (debug) { logger.debug("SecurityContextHolder now cleared, as request processing completed"); } } }最终调用的 HttpSessionSecurityContextRepository 的 saveContext() 方法
@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) { // 将 SecurityContext 信息保存到 HttpSession 中 httpSession.setAttribute(springSecurityContextKey, context); if (logger.isDebugEnabled()) { logger.debug("SecurityContext '" + context + "' stored to HttpSession: '" + httpSession); } } } }进行用户名和密码的认证。
UsernamePasswordAuthenticationFilter
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 表单提交一定要是 POST 方法,否则将抛出异常 if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); // 封装用户名和密码 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); setDetails(request, authRequest); // 调用 AuthenticationManager 接口的 authenticate()方法进行认证 // 实际上是调用它的实现类 ProviderManager类中的 authenticate()方法 return this.getAuthenticationManager().authenticate(authRequest); }主要内容
1.将用户名和密码封装成 UsernamePasswordAuthenticationToken 2.调用 ProviderManager 类中的 authenticate() 方法进行认证
ProviderManager
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; boolean debug = logger.isDebugEnabled(); /* 遍历所有的provider SpringSecurity提供了许多AuthenticationProvider的实现类 用于处理各种认证 */ for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // 找到处理该认证的provider类并进行认证 // 这里找到的是 AbstractUserDetailsAuthenticationProvider result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { try { result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { } catch (AuthenticationException e) { lastException = parentException = e; } } ... }主要内容 1.遍历所有的 AuthenticationProvider 接口的实现类,找到对应的provider类处理认证信息
AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // Determine username String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // 从缓存中获取 username并封装成 UserDetails对象(实际上是它的实现类org.springframework.security.core.userdetails.User) UserDetails user = this.userCache.getUserFromCache(username); // 缓存中获取不到 UserDetails对象 if (user == null) { cacheWasUsed = false; try { // 调用该方法获取 UserDetails对象,具体实现方法在 // DaoAuthenticationProvider中 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } ... return createSuccessAuthentication(principalToReturn, authentication, user); }主要内容 1.先尝试从缓存中获取 UserDetails对象,UserDetails 接口有唯一实现类User 2.如果没有从缓存中获取到对象,则调用 DaoAuthenticationProvider类中的retrieveUser()获取 UserDetails对象
DaoAuthenticationProvider
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { // 调用 UserDetailsService接口中的 loadUserByUsername()方法获取 UserDetails 对象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }主要内容 1.调用了 UserDetailsService接口中的loadUserByUsername()方法实现认证
综上所述,我们只要自定义类实现UserDetailsService接口,并重写其中的loadUserByUsername()方法,就可以实现自己的认证逻辑。
再回到AbstractUserDetailsAuthenticationProvider类的authenticate()方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException { ... // 创建 UsernamePasswordAuthenticationToken对象 return createSuccessAuthentication(principalToReturn, authentication, user); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // 使用构造器构造 UsernamePasswordAuthenticationToken 对象 UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { // 调用父类构造器 super(authorities); // 用户名 this.principal = principal; // 密码 this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override } public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) { if (authorities == null) { this.authorities = AuthorityUtils.NO_AUTHORITIES; return; } // 进行授权处理 for (GrantedAuthority a : authorities) { if (a == null) { throw new IllegalArgumentException( "Authorities collection cannot contain any null elements"); } } ArrayList<GrantedAuthority> temp = new ArrayList<>( authorities.size()); temp.addAll(authorities); this.authorities = Collections.unmodifiableList(temp); }总结: 最后获取到的 UsernamePasswordAuthenticationToken包含了用户名,密码,权限信息
注意:用户一定要被授予相关权限,否则会认证失败,后面的文章我还会提到
用于在过滤器链中抛出AccessDeniedException和AuthenticationException异常,抛出的异常由AccessDeniedHandler接口的实现类AccessDeniedHandlerImpl处理
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { if (!response.isCommitted()) { // 如果定义了异常页面 if (errorPage != null) { request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); // 设置响应码为403 response.setStatus(HttpStatus.FORBIDDEN.value()); // 跳转到错误页面 RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage); dispatcher.forward(request, response); } else { response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); } } }主要内容 1.ExceptionTranslationFilter 用于在过滤器链中抛出AccessDeniedException 和 AuthenticationException异常,并交给 AccessDeniedHandler接口的实现类 AccessDeniedHandlerImpl处理 2.如果自定义了错误页面,AccessDeniedHandlerImpl会跳往该页面并设置响应码为403
用于对http请求进行过滤 (引用了https://www.jb51.net/article/176217.htm)这里的解释
public void invoke(FilterInvocation fi) throws IOException, ServletException { //获取当前http请求的地址,比如说“/login” if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } // 这里做主要URL比对,将当前URL与securityMetadataSource(我们自己配置)中的URL过滤条件进行比对 // 首先判断当前URL是permit的还是需要验证的 // 若需要验证,尝试加载保存在SecurityContext类中的已登录信息 // 调用 AbstractSecurityInterceptor中的 AccessDecisionManager对象的decide方法 // 如果对于配置中需要登录才可访问的URL,已经查找到登录信息,则执行下一个Filter InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }这里大家可能有疑问,用户信息为什么保存在SecurityContext类中,我们再回到UsernamePasswordAuthenticationFilter类中,它是一个Filter类,那么肯定有doFilter()方法对请求进行过滤,该方法继承自它的父类AbstractAuthenticationProcessingFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { // 该方法在前面具体讲了 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); // 认证失败 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // 认证失败 unsuccessfulAuthentication(request, response, failed); return; } // 认证成功,继续沿着Filter链执行 if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } // 最终认证成功 successfulAuthentication(request, response, chain, authResult); }主要内容 1.UsernamePasswordAuthenticationFilter调用了父类AbstractAuthenticationProcessingFilter中的doFilter()方法进行过滤,如果认证失败,则调用unsuccessfulAuthentication()方法,如果认证成功,则调用successfulAuthentication()方法
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } // 将认证成功后的用户信息保存在SecurityContext类中 SecurityContextHolder.getContext().setAuthentication(authResult); // 实现记住我功能 rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }主要内容 1.在successfulAuthentication()方法中,通过SecurityContextHolder.getContext().setAuthentication(authResult)这个方法将认证成功后的用户信息保存在了SecurityContext类中