赞
踩
在我们用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;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。