SpringSecurity 默认表单登录页展示流程源码

SpringSecurity 默认表单登录页展示流程源码

本篇主要讲解 SpringSecurity提供的默认表单登录页 它是如何展示的的流程,
涉及
1.FilterSecurityInterceptor,
2.ExceptionTranslationFilter ,
3.DefaultLoginPageGeneratingFilter 过滤器,
并且简单介绍了 AccessDecisionManager 投票机制

?1.准备工作(体验SpringSecurity默认表单认证)

??1.1 创建SpringSecurity项目

??先通过IDEA 创建一个SpringBoot项目 并且依赖SpringSecurity,Web依赖

??此时pom.xml会自动添加

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
</dependency>

??1.2 提供一个接口

@RestController
public class HelloController {

@RequestMapping("/hello")
public String hello() {
    return "Hello SpringSecurity";
  }
}

??1.3 启动项目

??直接访问 提供的接口

http://localhost:8080/hello

??会发现浏览器被直接重定向到了 /login 并且显示如下默认的表单登录页

http://localhost:8080/login

??1.4 登录

??在启动项目的时候 控制台会打印一个 seuciryt password : xxx

Using generated security password: f520875f-ea2b-4b5d-9b0c-f30c0c17b90b

??直接登录

用户名:user  密码 :f520875f-ea2b-4b5d-9b0c-f30c0c17b90b 

??登录成功并且 浏览器又会重定向到 刚刚访问的接口

?2.springSecurityFilterchain 过滤器链

?如果你看过我另一篇关于SpringSecurity初始化源码的博客,那么你一定知道当SpringSecurity项目启动完成后会初始化一个 springSecurityFilterchain 它内部 additionalFilters属性初始化了很多Filter 如下
所有的请求都会经过这一系列的过滤器 Spring Security就是通过这些过滤器 来进行认证授权等

?3.FilterSecurityInterceptor (它会判断这次请求能否通过)

?FilterSecurityInterceptor是过滤器链中最后一个过滤器,主要用于判断请求能否通过,内部通过AccessDecisionManager 进行投票判断

?当我们未登录访问

http://localhost:8080/hello

?请求会被 FilterSecurityInterceptor 拦截

public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
    FilterInvocation fi = new FilterInvocation(request, response, chain);
    invoke(fi);
}

?重点看invoke方法

public void invoke(FilterInvocation fi) throws IOException, ServletException {
    if ((fi.getRequest() != null)
            && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
            && observeOncePerRequest) {
        // filter already applied to this request and user wants us to observe
        // once-per-request handling, so don't re-do security checking
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    }
    else {
        // first time this request being called, so perform security checking
        if (fi.getRequest() != null && observeOncePerRequest) {
            fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
        }

        InterceptorStatusToken token = super.beforeInvocation(fi);

        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        finally {
            super.finallyInvocation(token);
        }

        super.afterInvocation(token, null);
    }
}

?源码中有这样一句,其实就是判断当前用户是否能够访问指定的接口,可以则执行 fi.getChain().doFilter 调用访问的接口
否则 内部会抛出异常

InterceptorStatusToken token = super.beforeInvocation(fi);

?beforeInvocation 方法内部是通过 accessDecisionManager 去做决定的
?Spring Security已经内置了几个基于投票的AccessDecisionManager包括(AffirmativeBased ,ConsensusBased ,UnanimousBased)当然如果需要你也可以实现自己的AccessDecisionManager

?使用这种方式,一系列的AccessDecisionVoter将会被AccessDecisionManager用来对Authentication是否有权访问受保护对象进行投票,然后再根据投票结果来决定是否要抛出AccessDeniedException

this.accessDecisionManager.decide(authenticated, object, attributes);

?AffirmativeBased的 decide的实现如下

public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    int deny = 0;
    Iterator var5 = this.getDecisionVoters().iterator();

    while(var5.hasNext()) {
        AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
        int result = voter.vote(authentication, object, configAttributes);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Voter: " + voter + ", returned: " + result);
        }

        switch(result) {
        case -1:
            ++deny;
            break;
        case 1:
            return;
        }
    }

    if (deny > 0) {
        throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    } else {
        this.checkAllowIfAllAbstainDecisions();
    }
}

?AffirmativeBased的逻辑是这样的:

   (1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;

   (2)如果全部弃权也表示通过;

   (3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。

?当我们第一次访问的时候

http://localhost:8080/hello的时候 

?返回 result = -1 会抛出 AccessDeniedException 拒绝访问异常

?4.ExceptionTranslationFilter (捕获AccessDeniedException异常)

?该过滤器它会接收到FilterSecurityInterceptor抛出的 AccessDeniedException异常)并且进行捕获,然后发送重定向到/login请求

?源码如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    try {
        chain.doFilter(request, response);

        logger.debug("Chain processed normally");
    }
    catch (IOException ex) {
        throw ex;
    }
    catch (Exception ex) {
        // Try to extract a SpringSecurityException from the stacktrace
        Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
        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);
        }
    }
}

?当获取异常后 调用

handleSpringSecurityException(request, response, chain, ase);

?handleSpringSecurityException 源码如下:

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) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
            logger.debug(
                    "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
                    exception);

            sendStartAuthentication(
                    request,
                    response,
                    chain,
                    new InsufficientAuthenticationException(
                        messages.getMessage(
                            "ExceptionTranslationFilter.insufficientAuthentication",
                            "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);
        }
    }
}

?先判断获取的异常是否是AccessDeniedException 再判断是否是匿名用户,如果是则调用 sendStartAuthentication 重定向到登录页面

?重定向登录页面之前会保存当前访问的路径,这就是为什么我们访问 /hello接口后 再登录成功后又会跳转到 /hello接口,因为在重定向到/login接口前 这里进行了保存 requestCache.saveRequest(request, response);

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.commence(request, response, reason);
}

?authenticationEntryPoint.commence(request, response, reason);方法内部

?调用LoginUrlAuthenticationEntryPoint 的 commence方法

?LoginUrlAuthenticationEntryPoint 的commence方法内部有 构造重定向URL的方法

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

protected String buildRedirectUrlToLoginPage(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException authException) {

    String loginForm = determineUrlToUseForThisRequest(request, response,
            authException);

protected String determineUrlToUseForThisRequest(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException exception) {

    return getLoginFormUrl();
}

?最终会获取到需要重定向的URL /login

?然后sendRedirect 既会重定向到 /login 请求

?5.DefaultLoginPageGeneratingFilter (会捕获重定向的/login 请求)

?DefaultLoginPageGeneratingFilter是过滤器链中的一个用于捕获/login请求,并且渲染出一个默认表单页面

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    boolean loginError = isErrorPage(request);
    boolean logoutSuccess = isLogoutSuccess(request);
    if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
        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);
}

?isLoginUrlRequest 判断请求是否是 loginPageUrl

private boolean isLoginUrlRequest(HttpServletRequest request) {
    return matches(request, loginPageUrl);
}

?因为我们没有配置所以 默认的 loginPageUrl = /login

?验证通过请求路径 能匹配 loginPageUrl

String loginPageHtml = generateLoginPageHtml(request, loginError,
                logoutSuccess);

?generateLoginPageHtml 绘制默认的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";
        }
    }

    StringBuilder sb = new StringBuilder();

    sb.append("<!DOCTYPE html>\n"
            + "<html lang=\"en\">\n"
            + "  <head>\n"
            + "    <meta charset=\"utf-8\">\n"
            + "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"
            + "    <meta name=\"description\" content=\"\">\n"
            + "    <meta name=\"author\" content=\"\">\n"
            + "    <title>Please sign in</title>\n"
            + "    <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"
            + "    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
            + "  </head>\n"
            + "  <body>\n"
            + "     <div class=\"container\">\n");

    String contextPath = request.getContextPath();
    if (this.formLoginEnabled) {
        sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"
                + "        <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
                + createError(loginError, errorMsg)
                + createLogoutSuccess(logoutSuccess)
                + "        <p>\n"
                + "          <label for=\"username\" class=\"sr-only\">Username</label>\n"
                + "          <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
                + "        </p>\n"
                + "        <p>\n"
                + "          <label for=\"password\" class=\"sr-only\">Password</label>\n"
                + "          <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n"
                + "        </p>\n"
                + createRememberMe(this.rememberMeParameter)
                + renderHiddenInputs(request)
                + "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
                + "      </form>\n");
    }

    if (openIdEnabled) {
        sb.append("      <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n"
                + "        <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n"
                + createError(loginError, errorMsg)
                + createLogoutSuccess(logoutSuccess)
                + "        <p>\n"
                + "          <label for=\"username\" class=\"sr-only\">Identity</label>\n"
                + "          <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
                + "        </p>\n"
                + createRememberMe(this.openIDrememberMeParameter)
                + renderHiddenInputs(request)
                + "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
                + "      </form>\n");
    }

    if (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 : 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 : 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();
}

至此 SpringSecurity 默认表单登录页展示流程源码部分已经全部讲解完毕,会渲染出下面的页面,但是一定要有网的情况,否则样式可能会变化

6.总结

本篇主要讲解 SpringSecurity提供的默认表单登录页 它是如何展示的的流程,包括涉及这一流程中相关的 3个过滤器
1.FilterSecurityInterceptor,
2.ExceptionTranslationFilter ,
3.DefaultLoginPageGeneratingFilter 过滤器,
并且简单介绍了一下 AccessDecisionManager 它主要进行投票来判断该用户是否能够访问相应的 资源
AccessDecisionManager 投票机制我也没有深究 后续我会详细深入一下再展开

个人博客地址: https://www.askajohnny.com 欢迎访问!
本文由博客一文多发平台 OpenWrite 发布!

原文地址:https://www.cnblogs.com/askajohnny/p/12227881.html

时间: 2024-07-29 04:30:59

SpringSecurity 默认表单登录页展示流程源码的相关文章

SpringSecurity 自定义表单登录

SpringSecurity 自定义表单登录 本篇主要讲解 在SpringSecurity中 如何 自定义表单登录 , SpringSecurity默认提供了一个表单登录,但是实际项目里肯定无法使用的,本篇就主要讲解如何自定义表单登录 ?1.创建SpringSecurity项目 ??1.1 使用IDEA ??先通过IDEA 创建一个SpringBoot项目 并且依赖SpringSecurity,Web依赖 ??此时pom.xml会自动添加 <dependency> <groupId>

SpringBoot集成Spring Security(4)——自定义表单登录

通过前面三篇文章,你应该大致了解了 Spring Security 的流程.你应该发现了,真正的 login 请求是由 Spring Security 帮我们处理的,那么我们如何实现自定义表单登录呢,比如添加一个验证码- 源码地址:https://github.com/jitwxs/blog_sample 文章目录 一.添加验证码 1.1 验证码 Servlet 1.2 修改 login.html 1.3 添加匿名访问 Url二.AJAX 验证三.过滤器验证 3.1 编写验证码过滤器 3.2 注

sharepoint 2013基于AD的Form表单登录(二)——form登录页面自定义

配置好了sharepoint 2013基于AD的Form登录,只是成功了第一步,如何自定义登录页呢?特别是不要出现sharepoint2013自带登录页面,每次登录前还需要选择是否是form或者windows验证. 打开vs2012新建sharepoint 2013 project,在layouts目录下添加application page,页面命名为CustomLogin.aspx. 前台页面:为避免母版页中其他控件影响,注意继承的是simple.master         后台页面:继承F

sharepoint 2013基于AD的Form表单登录(四)——开发自定义登录过程需要引用文件路径。

1.Microsoft.IdentityModel.dll 位置 %ProgramFiles%\ReferenceAssemblies\Windows Identity Foundation\v3.5 2.Microsoft.SharePoint.IdentityModel.dll位置 C:\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.SharePoint.IdentityModel\v4.0_15.0.0.0__71e9bce111e94

【从零开始学BPM,Day2】默认表单开发

[课程主题]主题:5天,一起从零开始学习BPM [课程形式]1.为期5天的短任务学习2.每天观看一个视频,视频学习时间自由安排. [第二天课程] Step 1 软件下载:H3 BPM10.0全开放免费下载:http://bbs.h3bpm.com/read.php?tid=861&fid=11 Step 2 安装资料:参考本博的"安装资料"分类 Step 3 产品在线帮助浏览:http://bbs.h3bpm.com/read.php?tid=286&fid=11 St

VC POST表单——登录验证新浪邮箱

1.本机环境: Windows XP SP3.ADSL 2.开发工具: WildPackets OmniPeek V5.1.4 Visual C++ 6.0 IE6.0 FlexEdit V2.3.1871 如果觉得OmniPeek文件太大不方面下载的朋友,可以使用开源的简易抓包分析工具,先登录codeproject,再单击此处开始下载或邮件给我索取源码 ([email protected] 或 [email protected]) 注意:如果没有登录codeproject的话是无法下载的,你可

Spring Security教程(八):用户认证流程源码详解

本篇文章主要围绕下面几个问题来深入源码: 用户认证流程 认证结果如何在多个请求之间共享 获取认证用户信息 一.用户认证流程 上节中提到Spring Security核心就是一系列的过滤器链,当一个请求来的时候,首先要通过过滤器链的校验,校验通过之后才会访问用户各种信息. 这里要说明的是在过滤器的最前端有一个SecurityContextPersistenceFilter,当请求进来和返回的时候都会经过这个过滤器,它主要存放用户的认证信息.这里先简单提一下,后面会详解. 当用户发送登录请求的时候(

JobTracker启动流程源码级分析

org.apache.hadoop.mapred.JobTracker类是个独立的进程,有自己的main函数.JobTracker是在网络环境中提交及运行MR任务的核心位置. main方法主要代码有两句: 1 //创建jobTracker对象 2 JobTracker tracker = startTracker(new JobConf()); 3 //启动各个服务,包括JT内部一些重要的服务或者线程 4 tracker.offerService(); 一.startTracker(new Jo

Activity启动流程源码分析之Launcher启动(二)

1.前述 在前一篇文章中我们简要的介绍Activity的启动流程Activity启动流程源码分析之入门(一),当时只是简单的分析了一下流程,而且在上一篇博客中我们也说了Activity的两种启动方式,现在我们就来分析其中的第一种方式--Launcher启动,这种启动方式的特点是会创建一个新的进程来加载相应的Activity(基于Android5.1源码). 2.Activity启动流程时序图 好啦,接下来我们先看一下Launcher启动Activity的时序图: 好啦,接下来我们将上述时序图用代