自定义SpringSecurity授权跳转地址

Posted 木叶之荣

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义SpringSecurity授权跳转地址相关的知识,希望对你有一定的参考价值。

自定义SpringSecurity授权跳转地址

在我们用SpringSecurity+Oauth2做权限验证和访问控制的时候,如果要访问的请求处于未登录状态,会被框架进行拦截,并重定向到一个/login的请求(1),再重定向我们授权服务器的/oauth/authorize请求(这里是使用的授权码模式),接着再重定向到我们授权服务器的/login请求上,即我们授权服务器的登录页面。在工作中遇到了一个要修改(1)这个地方请求的问题。既然要修改(1)的/login请求,我们首先要弄清楚这里的/login是从哪里来的,因为我们从来没有在业务系统中定义过这个请求。


在弄清楚/login请求是从哪里来的之前,我们需要先弄明白,我们的请求为什么会被拦截。在SpringSecurity中定义了一堆的Filter来进行权限验证和访问控制,显然请求被拦截也是Filter来处理的。我们先看看一个未授权的请求会被哪些过滤器处理。

在上图中我们目前需要关注的是2和3这个地方,那我们先看看2这里是怎么处理的,在这里牵扯到的逻辑太多,我们只说重点的部分。在我们的框架中通过DelegatingFilterProxyRegistrationBean生成了DelegatingFilterProxy,再通过DelegatingFilterProxy引用了FilterChainProxy,非常的绕,不过我们不用管那么多,只需要记得,我们所有的请求都会被org.springframework.security.web.FilterChainProxy#doFilter来处理就行来。FilterChainProxy#doFilter的代码如下:

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException 
		boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
		if (clearContext) 
			try 
				request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
                //这里是重点
				doFilterInternal(request, response, chain);
			
			finally 
				SecurityContextHolder.clearContext();
				request.removeAttribute(FILTER_APPLIED);
			
		
		else 
			doFilterInternal(request, response, chain);
		
	

	private void doFilterInternal(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException 
		//	对request、response进行再包装
		FirewalledRequest fwRequest = firewall
				.getFirewalledRequest((HttpServletRequest) request);
		HttpServletResponse fwResponse = firewall
				.getFirewalledResponse((HttpServletResponse) response);
		//这里就是获取SpringSecurity的Filter了
		List<Filter> filters = getFilters(fwRequest);

		if (filters == null || filters.size() == 0) 
			if (logger.isDebugEnabled()) 
				logger.debug(UrlUtils.buildRequestUrl(fwRequest)
						+ (filters == null ? " has no matching filters"
								: " has an empty filter list"));
			

			fwRequest.reset();

			chain.doFilter(fwRequest, fwResponse);

			return;
		
		//组装SpringSecurity的过滤器链,作用和ApplicationFilterChain类似。chain是原链,filters
        //是SpringSecurity处理自己逻辑的过滤器的集合
		VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
		vfc.doFilter(fwRequest, fwResponse);
	

在上面的代码中,我们最终获取到的SpringSecurity的Filter如下图所示:

这些过滤器的作用这里先不讨论,只说后面的几个过滤器:SessionManagementFilter、ExceptionTranslationFilter、FilterSecurityInterceptor我们先来看看SessionManagementFilter的doFilter方法的代码:

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException 
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
        //如果这里有FILTER_APPLIED这个属性的话,说明这个请求经过Session验证了,直接进行下一个过滤器处理
		if (request.getAttribute(FILTER_APPLIED) != null) 
			chain.doFilter(request, response);
			return;
		
        //这里是给FILTER_APPLIED赋一个值,说明请求Session验证过了。
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        //从Request中获取Session信息,如果没有获取到则进行下面的逻辑处理
		if (!securityContextRepository.containsContext(request)) 
            //从Security上下文中获取授权信息
			Authentication authentication = SecurityContextHolder.getContext()
					.getAuthentication();
			//如果获取到授权,且不是匿名授权,则进行下面的权限验证
			if (authentication != null && !trustResolver.isAnonymous(authentication)) 
				// The user has been authenticated during the current request, so call the
				// session strategy
				try 
                    //session验证 后面可以单独分析
					sessionAuthenticationStrategy.onAuthentication(authentication,
							request, response);
				
				catch (SessionAuthenticationException e) 
					// The session strategy can reject the authentication
					logger.debug(
							"SessionAuthenticationStrategy rejected the authentication object",
							e);
					SecurityContextHolder.clearContext();
					failureHandler.onAuthenticationFailure(request, response, e);
					return;
				
				// Eagerly save the security context to make it available for any possible
				// re-entrant
				// requests which may occur before the current request completes.
				// SEC-1396.
				securityContextRepository.saveContext(SecurityContextHolder.getContext(),
						request, response);
			
			else 
				// No security context or authentication present. Check for a session
				// timeout
				if (request.getRequestedSessionId() != null
						&& !request.isRequestedSessionIdValid()) 
					if (logger.isDebugEnabled()) 
						logger.debug("Requested session ID "
								+ request.getRequestedSessionId() + " is invalid.");
					
                    //这里如果有自定义的session过期策略的话,会走session过期处理的逻辑,
                    //就不会走后续的过滤器处理了
					if (invalidSessionStrategy != null) 
						invalidSessionStrategy
								.onInvalidSessionDetected(request, response);
						return;
					
				
			
		
		chain.doFilter(request, response);
	

ExceptionTranslationFilter#doFilter 这个方法的主要作用就是对授权异常进行处理

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException 
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;d
		try 
			chain.doFilter(request, response);
		
		catch (IOException ex) 
			throw ex;
		
		catch (Exception ex) 
			// Try to extract a SpringSecurityException from the stacktrace
            //从异常栈中获取相应的异常信息
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
            //AuthenticationException 权限验证异常 这是一个抽象类,有很多的具体实现子类
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);
			if (ase == null) 
                //访问权限异常
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			
			if (ase != null) 
                //如果请求已经结束了
				if (response.isCommitted()) 
					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
				
                //这里是处理的重点方法,我们要重点分析
				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);
			
		
	
//看方法名就知道是做什么的,这里不得不说一下 FilterChain chain这个参数估计是之前冗余用的参数,在后面没有一点用处
private void handleSpringSecurityException(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, RuntimeException exception)
			throws IOException, ServletException 
		if (exception instanceof AuthenticationException) 
            //具体的异常处理
			sendStartAuthentication(request, response, chain,
					(AuthenticationException) exception);
		
		else if (exception instanceof AccessDeniedException) 
            //从SpringSecurity上下文中获取授权信息
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            //如果是匿名权限验证或者是配置了RememberMe
			if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) 
				//具体的异常处理 重点
				sendStartAuthentication(
						request,
						response,
						chain,
                    	//包装出来一个AuthenticationException的具体实现类,为了后面的通用处理
						new InsufficientAuthenticationException(
							messages.getMessage(
								"ExceptionTranslationFilter.insufficientAuthentication",
								"Full authentication is required to access this resource")));
			
			else 
                //访问拒绝的处理类 这个是可以配置的
				accessDeniedHandler.handle(request, response,
						(AccessDeniedException) exception);
			
		
	
	
	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);
		requestCache.saveRequest(request, response);
		logger.debug("Calling Authentication entry point.");
        //重点来了 这里默认的authenticationEntryPoint DelegatingAuthenticationEntryPoint如下图所示
		authenticationEntryPoint.commence(request, response, reason);
	

我们接着去看DelegatingAuthenticationEntryPoint#commence方法

	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException 
		//这里的entryPoints就是上图中的MediaTypeRequestMatcher和RequestHeaderRequestMatcher
		for (RequestMatcher requestMatcher : entryPoints.keySet()) 
			if (logger.isDebugEnabled()) 
				logger.debug("Trying to match using " + requestMatcher);
			
            //这里主要的实现是根据请求header中的accept来判断的,如果我们是从网页来发送请求的话,
            //基本上就是匹配的LoginUrlAuthenticationEntryPoint
			if (requestMatcher.matches(request)) 
				AuthenticationEntryPoint entryPoint = entryPoints.get(requestMatcher);
				if (logger.isDebugEnabled()) 
					logger.debug("Match found! Executing " + entryPoint);
				
                //按照上面的分析,我们这里就是调用的LoginUrlAuthenticationEntryPoint的commence方法
				entryPoint.commence(request, response, authException);
				return;
			
		
		if (logger.isDebugEnabled()) 
			logger.debug("No match found. Using default entry point " + defaultEntryPoint);
		
		// No EntryPoint matched, use defaultEntryPoint
        //如果没有找到匹配的EntryPoint就用默认的EntryPoint,默认的是LoginUrlAuthenticationEntryPoint
		defaultEntryPoint.commence(request, response, authException);
	

LoginUrlAuthenticationEntryPoint#commence方法

	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException 
		String redirectUrl = null;
        //如果使用的是转发,默认的是false
		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.
                //构建https的请求 暂时不用管
				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
            //获取重定向的URL  这里默认获取到的URI即是 /login
			redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
		
        //请求重定向
		redirectStrategy.sendRedirect(request, response, redirectUrl);
	
	//这里获取到的loginFormUrl是可以配置的,也终于到我们要分析的地方了,所以一路分析下来,我们问题的
	//重点是怎么配置loginFormUrl的值
	protected String determineUrlToUseForThisRequest(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException exception) 

		return getLoginFormUrl();
	
	//可以配置
	public String getLoginFormUrl() 
        
		return loginFormUrl;
	

按照上面的分析,咱们需要关注的是在哪里给loginFormUrl来进行赋值。通过我们打断点分析来看,赋值是在org.springframework.boot.autoconfigure.security.oauth2.client.SsoSecurityConfigurer#configure这里进行赋值的。
![image.png](https://img-blog.csdnimg.cn/img_convert/8a5ddfb50dfd1f9ec1357b9c0490d05c.png#align=left&display=inline&height=150&margin=[object Object]&name=image.png&originHeight=150&originWidth=1360&size=233473&status=done&style=none&width=1360)

	public void configure(HttpSecurity http) throws Exception 
        //在这里可以看到OAuth2SsoProperties是从Spring IOC中获取的,我们再看看OAuth2SsoProperties是什么
		OAuth2SsoProperties sso = this.applicationContext
				.getBean(OAuth2SsoProperties.class);
		// Delay the processing of the filter until we know the
		// SessionAuthenticationStrategy is available:
		http.apply(new OAuth2ClientAuthenticationConfigurer(oauth2SsoFilter(sso)));
		addAuthenticationEntryPoint(http, sso);
	
//从这里来看loginPath是一个配置项的值了,也就是说我们通过配置就可以达到我们的要求了,配置一个security.oauth2.sso.loginPath
//的值
@ConfigurationProperties(prefix = "security.oauth2.sso")
public class OAuth2SsoProperties 

	public static final String DEFAULT_LOGIN_PATH = "/login";
    /**
	 * Path to the login page, i.e. the one that triggers the redirect to the OAuth2
	 * Authorization Server.
	 */
	private String loginPath = DEFAULT_LOGIN_PATH;

以上是关于自定义SpringSecurity授权跳转地址的主要内容,如果未能解决你的问题,请参考以下文章

[SpringSecurity]web权限方案_用户授权_自定义403页面

具有 CAS 身份验证和自定义授权的 Spring Security

使用 Spring Security 自定义 Http 授权标头

SpringSecurity的session管理

Spring Security4实战与原理分析视频课程( 扩展+自定义)

Spring Security4实战与原理分析视频课程( 扩展+自定义)