自定义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 授权标头