在访问需要登录才可以访问的接口时,直接跳转到登录页。那Security是怎么做的呢?
首先来看一张时序图:
通过上图可以看到,请求顺序为AbstractAuthenticationProcessingFilter -> AnonymousAuthenticationFilter ->ExceptionTranslationFilter -> FilterSecurityInterceptor
AbstractAuthenticationProcessingFilter
请求先进入 AbstractAuthenticationProcessingFilter 的doFilter()方法。判断当前filter是否可以处理当前请求(也就是是否包含用户名密码信息),如果是,则调用其子类 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法进行验证(第一次请求时,没有用户名密码,是不会调用子类的)
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //判断当前的filter是否可以处理当前请求,不可以的话则交给下一个filter处理 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { //抽象方法由子类UsernamePasswordAuthenticationFilter实现 authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } //认证成功后,处理一些与session相关的方法 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) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //认证成功后的相关回调方法,主要将当前的认证放到SecurityContextHolder中 successfulAuthentication(request, response, chain, authResult); }
认证成功后的回调方法:
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); } //将认证结果保存到SecurityContextHolder中 SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //调用其它可扩展的 handlers 继续处理该认证成功以后的回调事件 //实现AuthenticationSuccessHandler接口即可 successHandler.onAuthenticationSuccess(request, response, authResult); }
认证失败后的回调方法:
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { //清除SecurityContextHolder的中数据 SecurityContextHolder.clearContext(); if (logger.isDebugEnabled()) { logger.debug("Authentication request failed: " + failed.toString(), failed); logger.debug("Updated SecurityContextHolder to contain null Authentication"); logger.debug("Delegating to authentication failure handler " + failureHandler); } rememberMeServices.loginFail(request, response); //调用其它可扩展的 handlers 处理该认证失败以后的回调事件 //实现 AuthenticationFailureHandler 接口即可 failureHandler.onAuthenticationFailure(request, response, failed);
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter本身不是过滤器,而是继承了AbstractAuthenticationProcessingFilter才拥有过滤器的性能,其主要是验证用户名密码。
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()); } //从request中获取 username 和 password String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); //封装Authenticaiton的实现类UsernamePasswordAuthenticationToken //传入用户名和密码,并将是否已经认证设为false UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); //设置UsernamePasswordAuthenticationToken中的详细信息。如remoteAddress、sessionId // Allow subclasses to set the "details" property setDetails(request, authRequest); //调用 AuthenticationManager 的实现类 ProviderManager 进行验证 return this.getAuthenticationManager().authenticate(authRequest); }
ExceptionTranslationFilter
ExceptionTranslationFilter是异常处理过滤器,该过滤器用来处理在系统认证授权过程中抛出的异常(也就是FilterSecurityInterceptor抛出来的),主要是处理
AccessDeniedException、AuthenticationException
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; try { chain.doFilter(request, response); logger.debug("Chain processed normally"); } catch (IOException ex) { throw ex; } catch (Exception ex) { //判断是不是AuthenticationException // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { //判断是不是AccessDeniedException ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); } if (ase != null) { //异常处理。包括缓存当前请求,跳转到登录页 handleSpringSecurityException(request, response, chain, ase); } else { // Rethrow ServletExceptions and RuntimeExceptions as-is if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } // Wrap other Exceptions. This shouldn't actually happen // as we've already covered all the possibilities for doFilter throw new RuntimeException(ex); } } }
在 handleSpringSecurityException 方法中,有一段:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //判断当前authentication是不是AnonymousAuthenticationToken(RememberMeAuthenticationToken)或者其子类 if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { logger.debug( "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception); sendStartAuthentication( request, response, chain, new InsufficientAuthenticationException( "Full authentication is required to access this resource")); }
其中sendStartAuthentication方法:
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { //清空SecurityContext SecurityContextHolder.getContext().setAuthentication(null); //缓存当前请求 requestCache.saveRequest(request, response); logger.debug("Calling Authentication entry point."); //调用AuthenticationEntryPoint的实现类LoginUrlAuthenticationEntryPoint(可扩展,实现AuthenticationEntryPoint即可) //跳转到可配置的登录页(如果不配置,则跳转到spring security默认的登录页) authenticationEntryPoint.commence(request, response, reason); }
FilterSecurityInterceptor
此过滤器为认证授权过滤器链中最后一个过滤器,该过滤器通过之后就是真正请求的服务
public void invoke(FilterInvocation fi) throws IOException, ServletException { ...... else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //调用父类AbstractSecurityInterceptor.beforeInvocation方法,进行最后一次过滤 InterceptorStatusToken token = super.beforeInvocation(fi); try { //请求真正的 /user 服务 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
在beforeInvocation方法中,会调用AccessDecisionManager.decide方法来验证当前认证成功的用户是否有权限访问该资源
protected InterceptorStatusToken beforeInvocation(Object object) { ...... Collectionattributes = this.obtainSecurityMetadataSource() .getAttributes(object); ...... Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { //授权认证 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } }
接下来对未登录访问处理扩展
自定义:ZyLoginUrlAuthenticationEntryPoint 替换默认的LoginUrlAuthenticationEntryPoint
在配置类配置:
http. ///... .exceptionHandling() .authenticationEntryPoint(new ZyLoginUrlAuthenticationEntryPoint("/loginPage"))
参考:
https://www.cnblogs.com/xuwenjin/p/9552303.html
https://blog.csdn.net/andy_zhang2007/article/details/85209178