Spring Security4.1.3实现拦截登录后向登录页面跳转方式(redirect或forward)返回被拦截界面

一、看下内部原理

简化后的认证过程分为7步:

  1. 用户访问网站,打开了一个链接(origin url)。

  2. 请求发送给服务器,服务器判断用户请求了受保护的资源。

  3. 由于用户没有登录,服务器重定向到登录页面

  4. 填写表单,点击登录

  5. 浏览器将用户名密码以表单形式发送给服务器

  6. 服务器验证用户名密码。成功,进入到下一步。否则要求用户重新认证(第三步)

  7. 服务器对用户拥有的权限(角色)判定: 有权限,重定向到origin url; 权限不足,返回状态码403("forbidden").

从第3步,我们可以知道,用户的请求被中断了。

用户登录成功后(第7步),会被重定向到origin url,spring security通过使用缓存的request,使得被中断的请求能够继续执行。


使用缓存

用户登录成功后,页面重定向到origin url。浏览器发出的请求优先被拦截器RequestCacheAwareFilter拦截,RequestCacheAwareFilter通过其持有的RequestCache对象实现request的恢复。

public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        
        // request匹配,则取出,该操作同时会将缓存的request从session中删除
        HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
                (HttpServletRequest) request, (HttpServletResponse) response);
        
        // 优先使用缓存的request
        chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
                response);
    }


何时缓存

首先,我们需要了解下RequestCache以及ExceptionTranslationFilter。

RequestCache

RequestCache接口声明了缓存与恢复操作。默认实现类是HttpSessionRequestCache。HttpSessionRequestCache的实现比较简单,这里只列出接口的声明:

public interface RequestCache {

    // 将request缓存到session中
    void saveRequest(HttpServletRequest request, HttpServletResponse response);
    
    // 从session中取request
    SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
    
    // 获得与当前request匹配的缓存,并将匹配的request从session中删除
    HttpServletRequest getMatchingRequest(HttpServletRequest request,
            HttpServletResponse response);
    
    // 删除缓存的request
    void removeRequest(HttpServletRequest request, HttpServletResponse response);
}

ExceptionTranslationFilter

ExceptionTranslationFilter 是Spring Security的核心filter之一,用来处理AuthenticationException和AccessDeniedException两种异常。

在我们的例子中,AuthenticationException指的是未登录状态下访问受保护资源,AccessDeniedException指的是登陆了但是由于权限不足(比如普通用户访问管理员界面)。

ExceptionTranslationFilter 持有两个处理类,分别是AuthenticationEntryPoint和AccessDeniedHandler。

ExceptionTranslationFilter 对异常的处理是通过这两个处理类实现的,处理规则很简单:

规则1. 如果异常是 AuthenticationException,使用 AuthenticationEntryPoint 处理
规则2. 如果异常是 AccessDeniedException 且用户是匿名用户,使用 AuthenticationEntryPoint 处理
规则3. 如果异常是 AccessDeniedException 且用户不是匿名用户,如果否则交给 AccessDeniedHandler 处理。


对应以下代码

private void handleSpringSecurityException(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, RuntimeException exception)
            throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
            logger.debug(
                    "Authentication exception occurred; redirecting to authentication entry point",
                    exception);

            sendStartAuthentication(request, response, chain,
                    (AuthenticationException) exception);
        }
        else if (exception instanceof AccessDeniedException) {
            if (authenticationTrustResolver.isAnonymous(SecurityContextHolder
                    .getContext().getAuthentication())) {
                logger.debug(
                        "Access is denied (user is anonymous); redirecting to authentication entry point",
                        exception);

                sendStartAuthentication(
                        request,
                        response,
                        chain,
                        new InsufficientAuthenticationException(
                                "Full authentication is required to access this resource"));
            }
            else {
                logger.debug(
                        "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
                        exception);

                accessDeniedHandler.handle(request, response,
                        (AccessDeniedException) exception);
            }
        }
    }


AccessDeniedHandler 默认实现是 AccessDeniedHandlerImpl。该类对异常的处理是返回403错误码。

public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException,
            ServletException {
    if (!response.isCommitted()) {
        if (errorPage != null) {  // 定义了errorPage
            // errorPage中可以操作该异常
            request.setAttribute(WebAttributes.ACCESS_DENIED_403,
                    accessDeniedException);

            // 设置403状态码
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);

            // 转发到errorPage
            RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
            dispatcher.forward(request, response);
        }
        else { // 没有定义errorPage,则返回403状态码(Forbidden),以及错误信息
            response.sendError(HttpServletResponse.SC_FORBIDDEN,
                    accessDeniedException.getMessage());
        }
    }
}


AuthenticationEntryPoint 默认实现是 LoginUrlAuthenticationEntryPoint, 该类的处理是转发或重定向到登录页面

public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {

    String redirectUrl = null;

    if (useForward) {

        if (forceHttps && "http".equals(request.getScheme())) {
            // First redirect the current request to HTTPS.
            // When that request is received, the forward to the login page will be
            // used.
            redirectUrl = buildHttpsRedirectUrlForRequest(request);
        }

        if (redirectUrl == null) {
            String loginForm = determineUrlToUseForThisRequest(request, response,
                    authException);

            if (logger.isDebugEnabled()) {
                logger.debug("Server side forward to: " + loginForm);
            }

            RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
    
            // 转发
            dispatcher.forward(request, response);

            return;
        }
    }
    else {
        // redirect to login page. Use https if forceHttps true

        redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

    }
    
    // 重定向
    redirectStrategy.sendRedirect(request, response, redirectUrl);
}

了解完这些,回到我们的例子。

第3步时,用户未登录的情况下访问受保护资源,ExceptionTranslationFilter会捕获到AuthenticationException异常(规则1)。页面需要跳转,ExceptionTranslationFilter在跳转前使用requestCache缓存request。

protected void sendStartAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain,
            AuthenticationException reason) throws ServletException, IOException {
    // SEC-112: Clear the SecurityContextHolder's Authentication, as the
    // existing Authentication is no longer considered valid
    SecurityContextHolder.getContext().setAuthentication(null);
    // 缓存 request
    requestCache.saveRequest(request, response);
    logger.debug("Calling Authentication entry point.");
    authenticationEntryPoint.commence(request, response, reason);
}


二、了解了以上原理以及上篇的forward和redirect的区别,配置实现如下,基于springsecurity4.1.3版本

配置文件:完整的




		
	
		   
         
         
         
		
		
		
	
	
	
	  
          
        	
            
            
        
    
    
    
    
    	
    	
    
	


主要配置



 
    
    	
    	
    

 
  

从上面的分析可知,默认情况下采用的是redirect方式,这里通过配置从而实现了forward方式,这里还是直接利用的security自带的类LoginUrlAuthenticationEntryPoint,只不过进行了以上配置:

/**
	 * Performs the redirect (or forward) to the login form URL.
	 */
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {

		String redirectUrl = null;

		if (useForward) {

			if (forceHttps && "http".equals(request.getScheme())) {
				// First redirect the current request to HTTPS.
				// When that request is received, the forward to the login page will be
				// used.
				redirectUrl = buildHttpsRedirectUrlForRequest(request);
			}

			if (redirectUrl == null) {
				String loginForm = determineUrlToUseForThisRequest(request, response,
						authException);

				if (logger.isDebugEnabled()) {
					logger.debug("Server side forward to: " + loginForm);
				}

				RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

				dispatcher.forward(request, response);

				return;
			}
		}
		else {
			// redirect to login page. Use https if forceHttps true

			redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

		}

		redirectStrategy.sendRedirect(request, response, redirectUrl);
	}

登录成功后的类配置,存入登录user信息后交给认证成功后的处理类MyAuthenticationSuccessHandler,该类集成了SavedRequestAwareAuthenticationSuccessHandler,他会从缓存中提取请求,从而可以恢复之前请求的数据

/**
 * 登录后操作
 * 
 * @author HHL
 * @date
 * 
 */
@Component
public class MyAuthenticationSuccessHandler extends
		SavedRequestAwareAuthenticationSuccessHandler {

	@Autowired
	private IUserService userService;

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws IOException, ServletException {

		// 认证成功后,获取用户信息并添加到session中
		UserDetails userDetails = (UserDetails) authentication.getPrincipal();
		MangoUser user = userService.getUserByName(userDetails.getUsername());
		request.getSession().setAttribute("user", user);
		
		super.onAuthenticationSuccess(request, response, authentication);
	
	}


}

SavedRequestAwareAuthenticationSuccessHandler中的onAuthenticationSuccess方法;

@Override
	public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws ServletException, IOException {
		SavedRequest savedRequest = requestCache.getRequest(request, response);

		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);

			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request
						.getParameter(targetUrlParameter)))) {
			requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);

			return;
		}

		clearAuthenticationAttributes(request);

		// Use the DefaultSavedRequest URL
		String targetUrl = savedRequest.getRedirectUrl();
		logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}
4.1.3中如果默认不配置的话也是采用的SavedRequestAwareAuthenticationSuccessHandler进行处理,详情可参见: Spring实战篇系列----源码解析Spring Security中的过滤器Filter初始化


上述实现了跳转到登录界面采用forward方式,就是浏览器地址栏没有变化,当然也可采用redirect方式,地址栏变为登录界面地址栏,当登录完成后恢复到原先的请求页面,请求信息会从requestCache中还原回来。可参考 Spring实战篇系列----spring security4.1.3配置以及踩过的坑



参考:

https://segmentfault.com/a/1190000004183264

http://gtbald.iteye.com/blog/1214132








你可能感兴趣的:(java,架构,Spring,Spring应用)