当前位置:   article > 正文

SpringSecurity的默认Filter详解_disableencodeurlfilter

disableencodeurlfilter

对于这15个过滤器,针对他们的功能,可以做一个简单的划分
• 0-4 这几个过滤器是 功能性的前置过滤器,提供了SpringSecurity的基础必要能力。
• 5-14 则与认证和授权过程相关

1. DisableEncodeUrlFilter

从字面上可以看出,这个过滤器是用来禁用URL重新编码的;
现在看他的具体实现

public class DisableEncodeUrlFilter extends OncePerRequestFilter {
    public DisableEncodeUrlFilter() {
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 对response进行包装后直接放行
        filterChain.doFilter(request, new DisableEncodeUrlResponseWrapper(response)); 
    }

    private static final class DisableEncodeUrlResponseWrapper extends HttpServletResponseWrapper {
        private DisableEncodeUrlResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        public String encodeRedirectUrl(String url) {
        // 不重新url 直接返回
            return url;
        }

        public String encodeRedirectURL(String url) {
        // 不重新url 直接返回
            return url;
        }

        public String encodeUrl(String url) {
        // 不重新url 直接返回
            return url;
        }

        public String encodeURL(String url) {
        // 不重新url 直接返回
            return url;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

为什么要这样处理?
Session的会话持有在客户端是通过cookies来保存SessionId来实现的,每次客户端的请求都携带sessionId.
如果禁用了cookie,后端的默认响应会重写url将sessionId拼接到url后面,传递给页面,sessionId就在http访问日志中暴露了。
官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/session/DisableEncodeUrlFilter.html

2. WebAsyncManagerIntegrationFilter

从字面上理解的话,这个过滤器就是 Web异步处理整合过滤器。
看他的具体实现

public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {

    private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {
        // 尝试获取一个 Object asyncManagerAttr = servletRequest.getAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE);
        // asyncManagerAttr 如果已存在,返回已存在的对象,
        // asyncManagerAttr 为空,并将新对方放到servletRequest.setAttribute(WEB_ASYNC_MANAGER_ATTRIBUTE, asyncManager);
       WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
       // 尝试从asyncManager 中获取已经存储的拦截器
       SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
             .getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
       // 如果没有获取到
       if (securityProcessingInterceptor == null) {
          // 新建一个拦截器 将SecurityContextHolderStrategy存储到拦截器,供子线程可获取;注册拦截器放到asyncManager中
          asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
                new SecurityContextCallableProcessingInterceptor());
       }
       filterChain.doFilter(request, response);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

默认情况下securityContextHolderStrategy的存储策略为ThreadLocal,在ThreadLocal的存储策略下,只有当前线程可以获取到securityContextHolder。
WebAsyncManagerIntegrationFilter 通过创建拦截器的形式,将securityContextHolderStrategy传递给子线程,后续子线程可以通过该拦截器获取到用户认证信息。
官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilter.html

3. SecurityContextHolderFilter

从字面上理解就是 持有SecurityContext的过滤器。
该Filter是用于存储用户认证信息的,他有两个重要的属性SecurityContextRepository和securityContextHolderStrategy。

public class SecurityContextHolderFilter extends OncePerRequestFilter {
    // SecurityContextRepository接口 提供一种在整个请求上下文存储SecurityContext的能力
    // SecurityContextRepository接口有两个重要方法: loadContext - 获取SecurityContext loadDeferredContext-延期获取SecurityContext saveContext - 保存SecurityContext 
    // 该属性默认为 DelegatingSecurityContextRepository,DelegatingSecurityContextRepository也是实现SecurityContextRepository接口的一个代理类
    // DelegatingSecurityContextRepository允许代理多个SecurityContextRepository来实现SecurityContext的存储
    // DelegatingSecurityContextRepository对 loadContext 和 saveContext 实现
    // 默认被代理的SecurityContextRepository为:HttpSessionSecurityContextRepository 和 RequestAttributeSecurityContextRepository
    // 当调用 DelegatingSecurityContextRepository 时,他会遍历被代理的SecurityContextRepository
    // saveContext时:遍历被代理的SecurityContextRepository 都调用saveContext SecurityContext进行存储
    // loadContext时: 调用自身loadDeferredContext 获取SecurityContext
    // loadDeferredContext时:遍历被代理的SecurityContextRepository 调用loadDeferredContext 获取 SecurityContext
    private final SecurityContextRepository securityContextRepository;

    private boolean shouldNotFilterErrorDispatch;

    /**
     * Creates a new instance.
     * @param securityContextRepository the repository to use. Cannot be null.
     */
    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
       Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
       this.securityContextRepository = securityContextRepository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {
       SecurityContext securityContext = this.securityContextRepository.loadContext(request).get();
       try {
          SecurityContextHolder.setContext(securityContext);
          filterChain.doFilter(request, response);
       }
       finally {
          SecurityContextHolder.clearContext();
       }
    }

    @Override
    protected boolean shouldNotFilterErrorDispatch() {
       return this.shouldNotFilterErrorDispatch;
    }

    /**
     * Disables {@link SecurityContextHolderFilter} for error dispatch.
     * @param shouldNotFilterErrorDispatch if the Filter should be disabled for error
     * dispatch. Default is false.
     */
    public void setShouldNotFilterErrorDispatch(boolean shouldNotFilterErrorDispatch) {
       this.shouldNotFilterErrorDispatch = shouldNotFilterErrorDispatch;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

SecurityContextHolderFilter是第三个要执行的Filter,但是他是直接继承于GenericFilterBean。
看他的doFilter逻辑

public class SecurityContextHolderFilter extends GenericFilterBean {
    @Overridepublic
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 如果已经执行过 该过滤器
        if (request.getAttribute(FILTER_APPLIED) != null) {
            // 跳过 直接执行下一个过滤器
            chain.doFilter(request, response);
            return;
        }
        // 标记改过滤器在本次请求过程中已经执行过
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);// 从SecurityContextRepository获取到SecurityContext
        Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
        try {
            // 将SecurityContext存储到securityContextHolderStrategy中,也就是存储到线程中。
            this.securityContextHolderStrategy.setDeferredContext(deferredContext);// 继续执行Filter
            chain.doFilter(request, response);
        } finally {
            // 这时应该 后续所有的Filter都已执行完后,有回到当前Filter中
            // 请求执行完成后,清除SecurityContext
            this.securityContextHolderStrategy.clearContext();// 移除已执行标记
            request.removeAttribute(FILTER_APPLIED);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

从源码中可以看出,该过滤器最主要的作用是
如果请求上下文中存在 SecurityContext 则 SecurityContext存储到securityContextHolderStrategy默认是ThreadLocal
并在整个过滤器链执行完成后清除SecurityContext
存储到securityContextHolderStrategy可以保证后续的过滤器都可以从securityContextHolderStrategy中获取到SecurityContext。
官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/context/SecurityContextHolderFilter.html

4. HeaderWriterFilter

从字面上理解,头信息写入过滤器
看他的具体实现

public class HeaderWriterFilter extends OncePerRequestFilter {// 要写人的头信息
    private final List<HeaderWriter> headerWriters;// 默认是false 也就是在过滤器都执行完成后,回到该过滤器时向response中写入
    private boolean shouldWriteHeadersEagerly = false;// 构造方法,需要在构造该过滤器时就传入要写入ResponseHeader的头信息

    // 具体确定要写入那些头信息是由HeadersConfigurer来决定的
    // 默认是以下几个头信息
    // 0 = {XContentTypeOptionsHeaderWriter}  X-Content-Type-Options: nosniff
    // 1 = {XXssProtectionHeaderWriter}  X-XSS-Protection: 0
    // 2 = {CacheControlHeadersWriter}
    //    Header [name: Cache-Control, values: [no-cache, no-store, max-age=0, must-revalidate]]
    //    Header [name: Pragma, values: [no-cache]]
    //    Header [name: Expires, values: [0]]
    // 3 = {HstsHeaderWriter}  Strict-Transport-Security: max-age=31536000 ; includeSubDomains
    // 4 = {XFrameOptionsHeaderWriter}  X-Frame-Options: DENY
    public HeaderWriterFilter(List<HeaderWriter> headerWriters) {
        Assert.notEmpty(headerWriters, "headerWriters cannot be null or empty");
        this.headerWriters = headerWriters;
    }
    
    // 具体的过滤器执行方法
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (this.shouldWriteHeadersEagerly) {
            // 如果是提前写入响应头,则是直接调用了writeHeaders 方法,并继续执行过滤器
            doHeadersBefore(request, response, filterChain);
        } else {
            // 默认走该方法
            // 再过滤器执行完成后,再写入头信息
            doHeadersAfter(request, response, filterChain);
        }
    }

    private void doHeadersBefore(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {// 写入头信息
        writeHeaders(request, response);// 执行过滤器
        filterChain.doFilter(request, response);
    }

    private void doHeadersAfter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {// 对Req 和 Resp进行一个包装
        HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request, response);
        HeaderWriterRequest headerWriterRequest = new HeaderWriterRequest(request, headerWriterResponse);
        try {
            // 先执行后续过滤器
            filterChain.doFilter(headerWriterRequest, headerWriterResponse);
        } finally {
            // 执行完后续过滤器,回到该过滤器后,写入响应头信息
            headerWriterResponse.writeHeaders();
        }
    }

    void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
        for (HeaderWriter writer : this.headerWriters) {
            // 将头信息写入HttpServletResponse
            writer.writeHeaders(request, response);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

该过滤器的类描述信息:
为当前响应添加报头的过滤器实现。可以添加某些头,启用浏览器保护。像X-Frame-Options, X-XSS-Protection和X-Content-Type-Options。
官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/header/HeaderWriterFilter.html

5. CsrfFilter

该过滤器用于保护应用不受Csrf攻击,是比较重要的一个过滤器。
CsrfFilter是第五个要执行的过滤器,他的主要作用是:
使用同步令牌模式应用CSRF保护。开发人员需要确保任何允许状态改变的请求都会调用CsrfFilter。 通常这只是意味着他们应该确保他们的web应用程序遵循适当的REST语义(即不使用HTTP方法GET、HEAD、TRACE、OPTIONS改变状态)。
通常,CsrfTokenRepository实现选择将CsrfToken存储在HttpSession中,HttpSessionCsrfTokenRepository由LazyCsrfTokenRepository包装。 这比将令牌存储在可由客户端应用程序修改的cookie中更可取。
这个过滤器的主要作用是防范跨站请求伪造(英语:Cross-site request forgery)攻击。
CSRF攻击的原理是,当用户已经在某个网站认证完成后浏览器已存储用户的有效信息,攻击者诱导用户登录一个其他的网站,利用用户在原网站未过期的认证信息发起一个非用户本意的请求。
CSRF利用了用户对本地浏览器的信任。
CsrfFilter处理逻辑

public final class CsrfFilter extends OncePerRequestFilter {
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 尝试从存储组件中加载 CsrfToken
        DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);// 在Request中设置CsrfToken
        // request.setAttribute(CsrfToken.class.getName(), csrfToken);
        this.requestHandler.handle(request, response, deferredCsrfToken::get);// 判断请求是否需要防止Csrf
        // 默认忽略了("GET", "HEAD", "TRACE", "OPTIONS") 这种不会改变数据 的请求方式
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher);
            }
            filterChain.doFilter(request, response);
            return;
        }
        // 获取CsrfToken ,有就返回 没有初始化一个token
        CsrfToken csrfToken = deferredCsrfToken.get();// 获取请求中携带的token
        String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);// 对比 token
        if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {// 是否是未携带token
            boolean missingToken = deferredCsrfToken.isGenerated();
            this.logger.debug(LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));// 抛出异常
            AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken);// 调用异常处理器
            this.accessDeniedHandler.handle(request, response, exception);
            return;
        }
        filterChain.doFilter(request, response);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

代码中有this.requireCsrfProtectionMatcher.matches()这个方法,来判断当前请求是否要进行CSRFToken的验证。
这里的匹配规则是 排除 (“GET”, “HEAD”, “TRACE”, “OPTIONS”) 这几种,然后
官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfFilter.html

6. LogoutFilter

从字面上理解,这个过滤器的是处理用户登出请求的。他的逻辑代码比较简单。

public class LogoutFilter extends GenericFilterBean {
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 判断请求是否是注销登录 默认匹配/logout请求路径
        if (requiresLogout(request, response)) {// 获取认证信息
            Authentication auth = this.securityContextHolderStrategy.getContext().getAuthentication();
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(LogMessage.format("Logging out [%s]", auth));
            }// 调用登出方法
            this.handler.logout(request, response, auth);// 跳转到登出成功后指定的页面
            this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
            return;
        }
        chain.doFilter(request, response);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/logout/LogoutFilter.html

7. UsernamePasswordAuthenticationFilter

从名称上看,这个过滤器时 用户名密码认证的过滤器。
他是AbstractAuthenticationProcessingFilter的子类,AbstractAuthenticationProcessingFilter的作用是,用于提供针对某种类型AbstractAuthenticationToken的用户认证的具体实现。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 判断请求是否是需要请求用户认证的 默认是 POST /login
        if (!requiresAuthentication(request, response)) {// 如果不是请求认证的请求,则继续执行后面的filter
            // 本次案例中执行 /test/hello 不是login 所以会继续往后执行
            chain.doFilter(request, response);
            return;
        }// 如果是请求用户认证的请求
        try {// 调用子类的attemptAuthentication实现,尝试去认证,这里调用的是UsernamePasswordAuthenticationFilter.attemptAuthentication方法
            // attemptAuthentication有可能会抛出认证相关的异常 AuthenticationException
            Authentication authenticationResult = attemptAuthentication(request, response);// 如果此时返回的结果是null 表示认证尚未完成
            if (authenticationResult == null) { // 暂时停止后续的过滤器
                // return immediately as subclass has indicated that it hasn't completed
                return;
            }// 如果返回认证后信息
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);// 认证成功后是否继续执行过滤器链 默认false
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }// 成功后执行的操作
            successfulAuthentication(request, response, chain, authenticationResult);
        }// 认证异常处理
        catch (InternalAuthenticationServiceException failed) {
            this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
            unsuccessfulAuthentication(request, response, failed);
        } catch (AuthenticationException ex) {// Authentication failed
            unsuccessfulAuthentication(request, response, ex);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.html

8. DefaultLoginPageGeneratingFilter

这个过滤器 提供在默认配置下生成一个登录页面的能力
其具体的实现如下

public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
    @Overridepublic
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 判断是不是登录失败跳转页面
        boolean loginError = isErrorPage(request);// 判断是不是退出登录成功跳转页面         
        boolean logoutSuccess = isLogoutSuccess(request);// 判断是否需要需要生成登录页面
        if (isLoginUrlRequest(request) || loginError || logoutSuccess) {// 生产页面HTML代码
            String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);// 返回到浏览器
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);
            return;
        }// 继续执行过滤器
        chain.doFilter(request, response);
    }// 生成页面的HTML代码

    private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
        String errorMsg = "Invalid credentials";
        if (loginError) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                AuthenticationException ex = (AuthenticationException) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
                errorMsg = (ex != null) ? ex.getMessage() : "Invalid credentials";
            }
        }
        String contextPath = request.getContextPath();
        StringBuilder sb = new StringBuilder();
        sb.append("<!DOCTYPE html>\n");
        sb.append("<html lang=\"en\">\n");
        sb.append("  <head>\n");
        sb.append("    <meta charset=\"utf-8\">\n");
        sb.append("    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
        sb.append("    <meta name=\"description\" content=\"\">\n");
        sb.append("    <meta name=\"author\" content=\"\">\n");
        sb.append("    <title>Please sign in</title>\n");
        sb.append("    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" " + "rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n");
        sb.append("    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" " + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
        sb.append("  </head>\n");
        sb.append("  <body>\n");
        sb.append("     <div class=\"container\">\n");
        if (this.formLoginEnabled) {
            sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n");
            sb.append("        <h2 class=\"form-signin-heading\">Please sign in</h2>\n");
            sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "        <p>\n");
            sb.append("          <label for=\"username\" class=\"sr-only\">Username</label>\n");
            sb.append("          <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
            sb.append("        </p>\n");
            sb.append("        <p>\n");
            sb.append("          <label for=\"password\" class=\"sr-only\">Password</label>\n");
            sb.append("          <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n");
            sb.append("        </p>\n");
            sb.append(createRememberMe(this.rememberMeParameter) + renderHiddenInputs(request));
            sb.append("        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
            sb.append("      </form>\n");
        }
        if (this.oauth2LoginEnabled) {
            sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
            sb.append(createError(loginError, errorMsg));
            sb.append(createLogoutSuccess(logoutSuccess));
            sb.append("<table class=\"table table-striped\">\n");
            for (Map.Entry<String, String> clientAuthenticationUrlToClientName : this.oauth2AuthenticationUrlToClientName.entrySet()) {
                sb.append(" <tr><td>");
                String url = clientAuthenticationUrlToClientName.getKey();
                sb.append("<a href=\"").append(contextPath).append(url).append("\">");
                String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue());
                sb.append(clientName);
                sb.append("</a>");
                sb.append("</td></tr>\n");
            }
            sb.append("</table>\n");
        }
        if (this.saml2LoginEnabled) {
            sb.append("<h2 class=\"form-signin-heading\">Login with SAML 2.0</h2>");
            sb.append(createError(loginError, errorMsg));
            sb.append(createLogoutSuccess(logoutSuccess));
            sb.append("<table class=\"table table-striped\">\n");
            for (Map.Entry<String, String> relyingPartyUrlToName : this.saml2AuthenticationUrlToProviderName.entrySet()) {
                sb.append(" <tr><td>");
                String url = relyingPartyUrlToName.getKey();
                sb.append("<a href=\"").append(contextPath).append(url).append("\">");
                String partyName = HtmlUtils.htmlEscape(relyingPartyUrlToName.getValue());
                sb.append(partyName);
                sb.append("</a>");
                sb.append("</td></tr>\n");
            }
            sb.append("</table>\n");
        }
        sb.append("</div>\n");
        sb.append("</body></html>");
        return sb.toString();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.html

9. DefaultLogoutPageGeneratingFilter

与DefaultLoginPageGeneratingFilter类似,这个过滤器 提供在默认配置下生成一个登出页面的能力

public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter {
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 判断是否是 /logout 页面
        if (this.matcher.matches(request)) {// 生成页面返回到浏览器
            renderLogout(request, response);
        }// 不是 /logout 继续执行后续的过滤器
        else {
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.format("Did not render default logout page since request did not match [%s]", this.matcher));
            }
            filterChain.doFilter(request, response);
        }
    }

    private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append("<!DOCTYPE html>\n");
        sb.append("<html lang=\"en\">\n");
        sb.append("  <head>\n");
        sb.append("    <meta charset=\"utf-8\">\n");
        sb.append("    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
        sb.append("    <meta name=\"description\" content=\"\">\n");
        sb.append("    <meta name=\"author\" content=\"\">\n");
        sb.append("    <title>Confirm Log Out?</title>\n");
        sb.append("    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" " + "rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" " + "crossorigin=\"anonymous\">\n");
        sb.append("    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" " + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
        sb.append("  </head>\n");
        sb.append("  <body>\n");
        sb.append("     <div class=\"container\">\n");
        sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n");
        sb.append("        <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n");
        sb.append(renderHiddenInputs(request) + "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n");
        sb.append("      </form>\n");
        sb.append("    </div>\n");
        sb.append("  </body>\n");
        sb.append("</html>");
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(sb.toString());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/ui/DefaultLogoutPageGeneratingFilter.html

10. BasicAuthenticationFilter

这个过滤器时用来做BASIC认证的。
与表单认证的作用一样,都是一种用户认证的方式。
这种认证方式用的很少,大致过一下他的核心代码既可以了。

public class BasicAuthenticationFilter extends OncePerRequestFilter {
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {// 从Request中解析到 凭证信息
            UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);// 未获取到 凭证信息
            if (authRequest == null) {
                this.logger.trace("Did not process authentication request since failed to find " + "username and password in Basic Authorization header");
                chain.doFilter(request, response);
                return;
            }// 获取到用户名
            String username = authRequest.getName();
            this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));// 从SecurityContextHolder中获取认证信息,判断用户名是否需要认证 如果已经认证过 就不再认证
            if (authenticationIsRequired(username)) {// 调用认证接口
                Authentication authResult = this.authenticationManager.authenticate(authRequest);// 创建一个空的SecurityContext
                SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();// 将认证结果存放到SecurityContext
                context.setAuthentication(authResult);// securityContextHolder存储SecurityContext
                this.securityContextHolderStrategy.setContext(context);
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
                }// 调用rememberMeServices 存储用户登录成功方法
                this.rememberMeServices.loginSuccess(request, response, authResult);// 在本次请求上下文中保存认证结果
                this.securityContextRepository.saveContext(context, request, response);// 调用认证成功后的处理方法
                onSuccessfulAuthentication(request, response, authResult);
            }
        }// 异常处理
        catch (AuthenticationException ex) {
            this.securityContextHolderStrategy.clearContext();
            this.logger.debug("Failed to process authentication request", ex);
            this.rememberMeServices.loginFail(request, response);
            onUnsuccessfulAuthentication(request, response, ex);
            if (this.ignoreFailure) {
                chain.doFilter(request, response);
            } else {
                this.authenticationEntryPoint.commence(request, response, ex);
            }
            return;
        }// 继续执行过滤器
        chain.doFilter(request, response);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.html

11. RequestCacheAwareFilter

这个过滤器是一个比较重要的,当客户端访问资源时,RequestCacheAwareFilter尝试冲缓存中查找已经保存的Request,默认是存储到Session的Attitude种的,默认的key为SPRING_SECURITY_SAVED_REQUEST。

public class RequestCacheAwareFilter extends GenericFilterBean {// 默认是:HttpSessionRequestCache 也就是将请求缓存在session
    private RequestCache requestCache;

    @Overridepublic
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// 尝试从缓存中获取已缓存的请求
        HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest) request, (HttpServletResponse) response);// 如果有缓存的请求,则用缓存的请求继续执行,否则用本次请求继续执行。
        chain.doFilter((wrappedSavedRequest != null) ? wrappedSavedRequest : request, response);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

从代码中可以看出,如果从缓存中找到了已经存储的请求,则继续原请求,如果没找到,则继续当前请求。
官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html

12. SecurityContextHolderAwareRequestFilter

该过滤器的主要作用是将ServletRequest包装成SecurityContextHolderAwareRequestWrapper

SecurityContextHolderAwareRequestWrapper 包装了SpringContext定义的 Authentication 对象。
SecurityContextHolderAwareRequestWrapper包含securityContextHolderStrategy属性,所以从SecurityContextHolderAwareRequestWrapper中可以直接获取到SecurityContext。

public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean {// 默认是  HttpServlet3RequestFactory
    private HttpServletRequestFactory requestFactory;

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {// 将请求包装成 SecurityContextHolderAwareRequestWrapper 这里默认是 Servlet3SecurityContextHolderAwareRequestWrapper
        chain.doFilter(this.requestFactory.create((HttpServletRequest) req, (HttpServletResponse) res), res);
    }
}

final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
    public HttpServletRequest create(HttpServletRequest request, HttpServletResponse response) {
        Servlet3SecurityContextHolderAwareRequestWrapper wrapper = new Servlet3SecurityContextHolderAwareRequestWrapper(request, this.rolePrefix, response);
        wrapper.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
        return wrapper;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.html

13. AnonymousAuthenticationFilter

该过滤器的作用:如果在经过该过滤器时,依然没有获取到用户的认证信息,则创建一个匿名用户。
大致的执行过程见下方源码。

public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {// 从SecurityContextHolderStrategy中获取SecurityContext
        Supplier<SecurityContext> deferredContext = this.securityContextHolderStrategy.getDeferredContext();// 再给SecurityContextHolderStrategy从新设置一次SecurityContext
        // 该方法根据请求和 从SecurityContextHolderStrategy中获取的SecurityContext 再次生产一个Supplier<SecurityContext> 
        this.securityContextHolderStrategy.setDeferredContext(defaultWithAnonymous((HttpServletRequest) req, deferredContext));// 继续执行
        chain.doFilter(req, res);
    }

    private Supplier<SecurityContext> defaultWithAnonymous(HttpServletRequest request, Supplier<SecurityContext> currentDeferredContext) {
        return SingletonSupplier.of(() -> {// 获取SecurityContext
            SecurityContext currentContext = currentDeferredContext.get();// 创建一个默认的Anonymous SecurityContext
            return defaultWithAnonymous(request, currentContext);
        });
    }

    private SecurityContext defaultWithAnonymous(HttpServletRequest request, SecurityContext currentContext) {// 从currentContext中获取 Authentication
        Authentication currentAuthentication = currentContext.getAuthentication();// 如果凭证信息为空
        if (currentAuthentication == null) {// 创建一个匿名的Authentication信息
            Authentication anonymous = createAuthentication(request);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.of(() -> "Set SecurityContextHolder to " + anonymous));
            } else {
                this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
            }// 将anonymous Authentication 设置到SecurityContext 中
            SecurityContext anonymousContext = this.securityContextHolderStrategy.createEmptyContext();
            anonymousContext.setAuthentication(anonymous);// 返回 匿名的 SecurityContext
            return anonymousContext;
        } else {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.of(() -> "Did not set SecurityContextHolder since already authenticated " + currentAuthentication));
            }
        }// 如果不为空 直接返回原SecurityContext
        return currentContext;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/authentication/AnonymousAuthenticationFilter.html

14. ExceptionTranslationFilter

ExceptionTranslationFilter是第14个过滤器,主要作用是用来处理 过滤器链中抛出来的权限校验异常。

如果检测到AuthenticationException这种认证异常,过滤器将启动authenticationEntryPoint 跳转到登录页面去认证。

如果检测到AccessDeniedException,过滤器将确定用户是否是匿名用户。如果是匿名用户,authenticationEntryPoint将启动,跳转到登录页面去,如果他们不是匿名用户,过滤器将委托给AccessDeniedHandler。默认情况下,过滤器将使用AccessDeniedHandlerImpl。

public class ExceptionTranslationFilter extends GenericFilterBean implements MessageSourceAware {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {// 执行doFilter 继续执行后续过滤器 如果后续的过滤器抛出异常,也会被catch到
            chain.doFilter(request, response);
        } catch (IOException ex) {// 不处理IO异常
            throw ex;
        } catch (Exception ex) {// Try to extract a SpringSecurityException from the stacktrace
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);// 先尝试获取AuthenticationException异常
            RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (securityException == null) {// 在尝试获取AccessDeniedException异常
                securityException = (AccessDeniedException) this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            }// 如果不是 AuthenticationException 和 AccessDeniedException异常
            if (securityException == null) {// 再抛出异常不做处理
                rethrow(ex);
            }
            if (response.isCommitted()) {
                throw new ServletException("Unable to handle the Spring Security Exception " + "because the response is already committed.", ex);
            }// 处理异常
            handleSpringSecurityException(request, response, chain, securityException);
        }
    }// 抛出异常

    private void rethrow(Exception ex) throws ServletException {// Rethrow ServletExceptions and RuntimeExceptions as-is
        if (ex instanceof ServletException) {
            throw (ServletException) ex;
        }
        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);
    }// 处理 AccessDeniedException和AuthenticationException。

    private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {// 如果是AuthenticationException异常
        if (exception instanceof AuthenticationException) {
            handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
        }// 如果是AccessDeniedException异常
        else if (exception instanceof AccessDeniedException) {
            handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/access/ExceptionTranslationFilter.html

15. AuthorizationFilter

他的主要作用是判断当前的用户用没有权限可以访问目标资源。
如果用户没有权限访问当前资源 会抛出 AccessDeniedException。异常会被ExceptionTranslationFilter处理。

public class AuthorizationFilter extends GenericFilterBean {
    private final AuthorizationManager<HttpServletRequest> authorizationManager;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {// 获取到请求和响应
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;// 判断是否 监控每一次请求 并且 当前请求已经执行过该过滤器 则跳过认证
        if (this.observeOncePerRequest && isApplied(request)) {
            chain.doFilter(request, response);
            return;
        }// 当请求的转发方式 为ERROR 和 ASYNC时 跳过认证
        // DispatcherType.ERROR  当容器将处理过程传递给错误处理机制(如定义的错误页)时。
        // DispatcherType.ASYNC  在以下几种情况下 AsyncContext.dispatch(), AsyncContext.dispatch(String) and AsyncContext.addListener(AsyncListener, ServletRequest, ServletResponse)
        if (skipDispatch(request)) {
            chain.doFilter(request, response);
            return;
        }// 获取记录已经执行过该过滤器的标记setAttribute KEY
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();// 设置为TRUE 表示已经执行了该过滤器
        request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
        try {// 调用authorizationManager 判断当前用户是否有权限访问该资源
            // this::getAuthentication -> 获取用户的认证信息
            // request: 当前请求
            // AuthorizationDecision: 授权描述  decision.isGranted() 表示是否有该资源的权限
            AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);// 发布一个认证的消息
            this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);// 如果未授权,抛出异常AccessDeniedException
            if (decision != null && !decision.isGranted()) {
                throw new AccessDeniedException("Access Denied");
            }// 已授权 继续向后执行
            chain.doFilter(request, response);
        } finally {// 请求执行完成后 移除掉 已执行标记
            request.removeAttribute(alreadyFilteredAttributeName);
        }
    }

    private boolean skipDispatch(HttpServletRequest request) {
        if (DispatcherType.ERROR.equals(request.getDispatcherType()) && !this.filterErrorDispatch) {
            return true;
        }
        if (DispatcherType.ASYNC.equals(request.getDispatcherType()) && !this.filterAsyncDispatch) {
            return true;
        }
        return false;
    }

    private String getAlreadyFilteredAttributeName() {
        String name = getFilterName();
        if (name == null) {
            name = getClass().getName();
        }
        return name + ".APPLIED";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

官方文档地址:
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/access/intercept/AuthorizationFilter.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/921566
推荐阅读
相关标签
  

闽ICP备14008679号