SpringSecurity(一)

目標:初步掌握Spring Security的認證功能實現。

案例介紹

  • 項目的地址是:項目初始化
  • 項目的數據庫腳本在src/main/resources目的下的security.sql。
  • 開發工具使用的是IntelliJ IDEA。
  • JDK版本是1.8。
  • MySQL的版本是5.7。
  • 後端的技術是SSM(SpringMVC、Spring、Mybatis)。
  • 前端的技術是AdminLTE2。訪問地址:http://localhost:8080/login.jsp。

案例介紹

初識權限管理

權限管理概念

  • 權限管理,通常是指根據系統設置的安全規則或安全策略,用戶能夠訪問並且只能訪問本身被授予的權限。權限管理幾乎出如今任何系統裏面,前提是須要用戶認證的系統。
在權限管理的概念中,有兩個很是重要的名詞:

認證:經過用戶名和密碼(固然也能夠是其它方式,好比郵箱、身份證等)成功登陸系統後,讓系統獲得當前用戶的角色身份。css

受權:系統根據當前用戶的角色,給其授予對應能夠操做的權限資源。html

徹底權限管理須要三個對象

  • 用戶:主要包含用戶名、密碼等,能夠實現認證操做。通常而言,在系統中給用戶分配角色。
  • 角色:角色是權限的集合。通常而言,在系統中給角色分配權限。
  • 權限:權限也能夠稱爲資源,包括地址、權限名稱等。
通常而言,用戶能夠分配多個角色,角色能夠分配多個權限。因此,在權限設計表的時候,通常設計5張表,分別爲用戶表、角色表、權限表、用戶角色表、角色權限表。業內有時也會將這5張表稱爲經典的RBAC權限設計模型。

初識Spring Security

Spring Security概念

  • Spring Security是Spring採用AOP思想,基於Servlet過濾器實現的安全框架。它提供了完善的認證機制和方法級的受權功能。是一個很是優秀的權限管理框架。

Spring Security簡單入門

建立web工程並導入相應的jar包

<!-- 
    spring-security-config是用來解析XML配置文件
 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<!--
    spring-security-core是Spring Security的核心jar包,任何Spring Security都須要用此jar包
 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<!-- 
    spring-security-taglibs是Spring Security提供的動態標籤庫,JSP頁面中可使用。
 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<!--
    spring-security-web是web工程的必備jar包,包含過濾器和相關的web安全基礎結構代碼
 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>

配置web.xml

<!-- 配置Spring Security的核心過濾器鏈 -->
<!--  
    filter-name必須是springSecurityFilterChain
-->
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

配置spring-security.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd
                http://www.springframework.org/schema/aop
                http://www.springframework.org/schema/aop/spring-aop.xsd
                http://www.springframework.org/schema/tx
                http://www.springframework.org/schema/tx/spring-tx.xsd
                http://www.springframework.org/schema/mvc
                http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/security
                http://www.springframework.org/schema/security/spring-security.xsd">

    <!--
        配置Spring Security
        auto-config="true"表示自定加載spring-security.xml配置文件
        use-expressions="true"表示使用spring的el表達式來配置spring security
    -->
    <security:http auto-config="true" use-expressions="true">
        <!--   攔截資源     -->
        <!--
            pattern="/**" 表示攔截全部的資源
            access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER的角色才能訪問資源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
    </security:http>

    <!--設置Spring Security認證用戶信息的來源-->
    <!--
        Spring Security的認證必須是加密的,{noop}表示不加密認證
    -->
    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <security:user name="user" password="{noop}user"
                               authorities="ROLE_USER"/>
                <security:user name="admin" password="{noop}admin"
                               authorities="ROLE_ADMIN"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>


</beans>

將spring-security.xml配置文件引入到applicationContext.xml中

<!--引入SpringSecurity主配置文件-->
<import resource="classpath:spring-security.xml"/>

運行項目

  • 運行項目會出現下面的界面。

Spring Security入門啓動

  • 其源碼以下:

Spring Security入門啓動的內部首頁源碼

  • 控制檯的日誌:
INFO  web.DefaultSecurityFilterChain  - Creating filter chain: any request, [org.springframework.security.web.context.SecurityContextPersistenceFilter@17455fed, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@97f4fa3, org.springframework.security.web.header.HeaderWriterFilter@1384edda, org.springframework.security.web.csrf.CsrfFilter@544ac02d, org.springframework.security.web.authentication.logout.LogoutFilter@694d477e, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@1abc0fa3, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2e62db13, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2c2b3d5a, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2fdef916, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@55e53a59, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6d67c9b5, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@18d3e8ae, org.springframework.security.web.session.SessionManagementFilter@409db4eb, org.springframework.security.web.access.ExceptionTranslationFilter@3cd67953, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6faf351d]
  • 最後,咱們在此登陸頁面輸入用戶名爲user,密碼爲user,便可登陸。

入門登陸成功

Spring Security經常使用的過濾器鏈

Spring Security經常使用過濾器介紹

  • org.springframework.security.web.context.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一個SecurityContext,並將SecurityContext給之後的過濾器使用個,爲後續Filter創建所須要的上下文。SecurityContext中存儲了當前用戶的認證和權限信息。
  • org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
此過濾器用語集成SecurityContext到Spring異步執行機制中的WebAsyncManager。
  • org.springframework.security.web.header.HeaderWriterFilter
向請求的Header中添加相應的信息,可在http標籤內部使用security:headers來控制。
  • org.springframework.security.web.csrf.CsrfFilter
csrf又稱爲跨域請求僞造,SpringSecurity會對全部POST、PUT、DELETE請求驗證是否包含系統生成的csrf的token信息,若是不包含,就報錯。起到防止csrf攻擊的效果。
  • org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL爲/logout的請求,實現用戶退出,清除認證信息。
  • org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
認證操做全靠這個過濾器,默認匹配URL爲/login且必須爲POST請求。
  • org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
若是沒有在配置文件中執行認證頁面,則由該過濾器生成一個默認認證頁面。
  • org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
此過濾器產生的一個默認的退出登陸的頁面。
  • org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此過濾器會自動解析HTTP請求中頭部帶有Authentication,且以Basic開頭的頭信息。
  • org.springframework.security.web.savedrequest.RequestCacheAwareFilter
經過HttpSessionRequestCache內部維護了一個RequestCache,用於緩存HttpServletRequest。
  • org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
針對ServletRequest進行了一次包裝,使得request具備更加豐富的API。
  • org.springframework.security.web.authentication.AnonymousAuthenticationFilter
當SecurityContextHolder中認證信息爲空,則會建立一個匿名用戶存入到SecurityContextHolder中,SpringSecurity爲了兼容未登陸的訪問,也走了一套認證流程,只不過是一個匿名身份。
  • org.springframework.security.web.session.SessionManagementFilter
SecurityContextRepository限制同一用戶開啓多個會話的數量。
  • org.springframework.security.web.access.ExceptionTranslationFilter
異常轉換過濾器位於整個SpringSecurityFilterChain的後方,用來轉換整個鏈路中出現的異常。
  • org.springframework.security.web.access.intercept.FilterSecurityInterceptor
獲取所配置資源訪問的受權信息,根據SecurityContextHolder中存儲的用戶信息來決定其是否有權限。

Spring Security過濾器鏈加載原理

DelegatingFilterProxy

  • 咱們在web.xml中配置了一個名稱爲springSecurityFilterChain的過濾器DelegatingFilterProxy,接下來咱們看其中的重要源碼便可。
public class DelegatingFilterProxy extends GenericFilterBean {
    @Nullable
    private String contextAttribute;
    @Nullable
    private WebApplicationContext webApplicationContext;
    @Nullable
    private String targetBeanName;
    private boolean targetFilterLifecycle = false;
    @Nullable
    private volatile Filter delegate; //注意:這個過濾器纔是真正加載的過濾器
    private final Object delegateMonitor = new Object();

    //注意:doFilter是過濾器的入口,直接從這邊看。
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
            synchronized (this.delegateMonitor) {
                delegateToUse = this.delegate;
                if (delegateToUse == null) {
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac == null) {
                        throw new IllegalStateException("No WebApplicationContext found: " +
                                "no ContextLoaderListener or DispatcherServlet registered?");
                    }
                    //第一步:doFilter中最重要的一步,初始化上面私有過濾器屬性delegate
                    delegateToUse = initDelegate(wac);
                }
                this.delegate = delegateToUse;
            }
        }
        //第三步:執行FilterChainProxy過濾器
        invokeDelegate(delegateToUse, request, response, filterChain);
    }
    //第二步:直接看最終加載的過濾器是誰
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        //debug得知targetBeanName爲springSecurityFilterChain
        String targetBeanName = getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        //debug得知Filter對象爲FilterChainProxy
        Filter delegate = wac.getBean(targetBeanName, Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }

    
    protected void invokeDelegate(
            Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        delegate.doFilter(request, response, filterChain);
    }

}
  • 第二步debug的結果以下圖所示:

DelegatingFilterProxy

  • 由此可知,DelegatingFilterProxy經過springSecurityFilterChain這個名詞,獲得了一個FilterChainProxy過濾器,最終在第三步執行了該過濾器。

FilterChainProxy

public class FilterChainProxy extends GenericFilterBean {
    private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
    private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
    private List<SecurityFilterChain> filterChains;
    private FilterChainProxy.FilterChainValidator filterChainValidator;
    private HttpFirewall firewall;

    //能夠經過一個叫SecurityFilterChain的對象實例化一個FilterChainProxy對象,可能SecurityFilterChain纔是真正的過濾器對象。
    public FilterChainProxy(SecurityFilterChain chain) {
        this(Arrays.asList(chain));
    }
    //又是SecurityFilterChain對象。
    public FilterChainProxy(List<SecurityFilterChain> filterChains) {
        this.filterChainValidator = new FilterChainProxy.NullFilterChainValidator();
        this.firewall = new StrictHttpFirewall();
        this.filterChains = filterChains;
    }

    //注意:直接從doFilter看
    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);
                this.doFilterInternal(request, response, chain);
            } finally {
                SecurityContextHolder.clearContext();
                request.removeAttribute(FILTER_APPLIED);
            }
        } else {
            //第一步:具體操做調用下面的doFilterInternal方法
            this.doFilterInternal(request, response, chain);
        }

    }

    private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
        HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
        //第二步:封裝要執行的過濾器鏈,這麼多的過濾器鏈就在這裏封裝進去了。
        List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
        if (filters != null && filters.size() != 0) {
            FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
            //第四步:加載過濾器鏈
            vfc.doFilter(fwRequest, fwResponse);
        } else {
            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);
        }
    }

    private List<Filter> getFilters(HttpServletRequest request) {
        Iterator var2 = this.filterChains.iterator();
        //封裝過濾器鏈到SecurityFilterChain對象
        SecurityFilterChain chain;
        do {
            if (!var2.hasNext()) {
                return null;
            }

            chain = (SecurityFilterChain)var2.next();
        } while(!chain.matches(request));

        return chain.getFilters();
    }
}
  • 第二步debug的結果以下圖所示:

FilterChainProxy

SecurityFilterChain

  • SecurityFilterChain是一個接口,實現類也只有一個,這纔是web.xml配置的過濾器鏈對象。
//接口
public interface SecurityFilterChain {
    boolean matches(HttpServletRequest var1);

    List<Filter> getFilters();
}
//實現類
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
    private static final Log logger = LogFactory.getLog(DefaultSecurityFilterChain.class);
    private final RequestMatcher requestMatcher;
    private final List<Filter> filters;

    public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) {
        this(requestMatcher, Arrays.asList(filters));
    }

    public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
        logger.info("Creating filter chain: " + requestMatcher + ", " + filters);
        this.requestMatcher = requestMatcher;
        this.filters = new ArrayList(filters);
    }

    public RequestMatcher getRequestMatcher() {
        return this.requestMatcher;
    }

    public List<Filter> getFilters() {
        return this.filters;
    }

    public boolean matches(HttpServletRequest request) {
        return this.requestMatcher.matches(request);
    }

    public String toString() {
        return "[ " + this.requestMatcher + ", " + this.filters + "]";
    }
}

SpringSecurity使用自定義認證頁面

在SpringSecurity的主配置文件中指定認證頁面配置信息

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd
                http://www.springframework.org/schema/aop
                http://www.springframework.org/schema/aop/spring-aop.xsd
                http://www.springframework.org/schema/tx
                http://www.springframework.org/schema/tx/spring-tx.xsd
                http://www.springframework.org/schema/mvc
                http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/security
                http://www.springframework.org/schema/security/spring-security.xsd">

    <!--直接釋放無需通過SpringSecurity過濾器的靜態資源-->
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/plugins/**" security="none"/>
    <security:http pattern="/failer.jsp" security="none"/>
    <security:http pattern="/favicon.ico" security="none"/>

    <!--
        配置Spring Security
        auto-config="true"表示自定加載spring-security.xml配置文件
        use-expressions="true"表示使用spring的el表達式來配置spring security
    -->
    <security:http auto-config="true" use-expressions="true">

        <!--指定login.jsp頁面能夠被匿名訪問-->
        <security:intercept-url pattern="/login.jsp" access="permitAll()"/>
        <!--   攔截資源     -->
        <!--
            pattern="/**" 表示攔截全部的資源
            access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER的角色才能訪問資源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
        <!-- 配置認證信息,指定自定義的認證頁面 -->
        <!--
            login-page 指定登陸頁面的地址
            login-processing-url 處理登陸的處理器的地址
            default-target-url 登陸成功跳轉的地址
            authentication-failure-url 登陸失敗跳轉的地址

            默認的用戶名是username,密碼是password,固然也可使用username-parameter和password-parameter修改。
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"/>
        <!--
            指定退出登陸後跳轉的頁面
            logout-url 處理退出登陸的處理器地址
            logout-success-url 退出登陸成功跳轉的地址
        -->
        <security:logout logout-url="/logout" logout-success-url="/login.jsp"/>
    </security:http>

    <!--設置Spring Security認證用戶信息的來源-->
    <!--
        Spring Security的認證必須是加密的,{noop}表示不加密認證
    -->
    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <security:user name="user" password="{noop}user"
                               authorities="ROLE_USER"/>
                <security:user name="admin" password="{noop}admin"
                               authorities="ROLE_ADMIN"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
</beans>
  • 修改認證頁面的請求地址:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>數據 - AdminLTE2定製版 | Log in</title>

<meta
    content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
    name="viewport">

<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css">
</head>

<body class="hold-transition login-page">
    <div class="login-box">
        <div class="login-logo">
            <a href="#"><b></b>後臺管理系統</a>
        </div>
        <!-- /.login-logo -->
        <div class="login-box-body">
            <p class="login-box-msg">登陸系統</p>
            <!--
                action的地址必須是/login
                method 必須是post
            -->
            <form action="${pageContext.request.contextPath}/login" method="post">

                <div class="form-group has-feedback">
                    <input type="text" name="username" class="form-control"
                        placeholder="用戶名"> <span
                        class="glyphicon glyphicon-envelope form-control-feedback"></span>
                </div>
                <div class="form-group has-feedback">
                    <input type="password" name="password" class="form-control"
                        placeholder="密碼"> <span
                        class="glyphicon glyphicon-lock form-control-feedback"></span>
                </div>
                <div class="row">
                    <div class="col-xs-8">
                        <div class="checkbox icheck">
                            <label><input type="checkbox" name="remember-me" value="true"> 記住 下次自動登陸</label>
                        </div>
                    </div>
                    <!-- /.col -->
                    <div class="col-xs-4">
                        <button type="submit" class="btn btn-primary btn-block btn-flat">登陸</button>
                    </div>
                    <!-- /.col -->
                </div>
            </form>

            <a href="#">忘記密碼</a><br>


        </div>
        <!-- /.login-box-body -->
    </div>
    <!-- /.login-box -->

    <!-- jQuery 2.2.3 -->
    <!-- Bootstrap 3.3.6 -->
    <!-- iCheck -->
    <script
        src="${pageContext.request.contextPath}/plugins/jQuery/jquery-2.2.3.min.js"></script>
    <script
        src="${pageContext.request.contextPath}/plugins/bootstrap/js/bootstrap.min.js"></script>
    <script
        src="${pageContext.request.contextPath}/plugins/iCheck/icheck.min.js"></script>
    <script>
        $(function() {
            $('input').iCheck({
                checkboxClass : 'icheckbox_square-blue',
                radioClass : 'iradio_square-blue',
                increaseArea : '20%' // optional
            });
        });
    </script>
</body>

</html>
  • 再次啓動項目就能夠看到自定義的登陸頁面了。

自定義登陸頁面

  • 可是當咱們輸入用戶名爲user,密碼爲user的時候,卻會出現以下的頁面:

登陸頁面沒有配置csrf的token

  • 403在Spring Security中是權限不足?爲何?Spring Security內置的認證頁面源代碼中有_csrf隱藏input,問題就在這裏,並且後臺日誌是這樣的。

登陸頁面忘記配置csrf

Spring Security的csrf防禦機制

Spring Security中CsrfFilter過濾器的說明

public final class CsrfFilter extends OncePerRequestFilter {
    public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
    private final Log logger = LogFactory.getLog(this.getClass());
    private final CsrfTokenRepository tokenRepository;
    private RequestMatcher requireCsrfProtectionMatcher;
    private AccessDeniedHandler accessDeniedHandler;

    public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
        this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
        this.accessDeniedHandler = new AccessDeniedHandlerImpl();
        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
        this.tokenRepository = csrfTokenRepository;
    }
    //從這裏能夠看出Spring Security的csrf機制把請求方式分爲兩類
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }

        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
        //第一類:GET、HEAD、TRACE、OPTIONS四類請求能夠直接經過
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
        } else {
            //第二類:除去上面的四種方式,包括POST、DELETE、PUT等都須要攜帶token才能經過
            String actualToken = request.getHeader(csrfToken.getHeaderName());
            if (actualToken == null) {
                actualToken = request.getParameter(csrfToken.getParameterName());
            }

            if (!csrfToken.getToken().equals(actualToken)) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
                }

                if (missingToken) {
                    this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
                } else {
                    this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
                }

            } else {
                filterChain.doFilter(request, response);
            }
        }
    }

    public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
        Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
        this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
    }

    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
        this.accessDeniedHandler = accessDeniedHandler;
    }

    private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
        private final HashSet<String> allowedMethods;

        private DefaultRequiresCsrfMatcher() {
            this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
        }

        public boolean matches(HttpServletRequest request) {
            return !this.allowedMethods.contains(request.getMethod());
        }
    }
}
  • 經過源碼,咱們知道,咱們本身的登陸頁面的請求方式是POST,可是卻沒有攜帶token,因此纔會出現403權限不足的異常,那麼如何處理?前端

    • ①直接禁用csrf,不推薦。
    • ②在認證頁面攜帶token請求。

禁用csrf防禦機制

  • 在Spring Security的主配置文件中添加禁用csrf防禦的機制。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd
                http://www.springframework.org/schema/aop
                http://www.springframework.org/schema/aop/spring-aop.xsd
                http://www.springframework.org/schema/tx
                http://www.springframework.org/schema/tx/spring-tx.xsd
                http://www.springframework.org/schema/mvc
                http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/security
                http://www.springframework.org/schema/security/spring-security.xsd">

    <!--直接釋放無需通過SpringSecurity過濾器的靜態資源-->
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/plugins/**" security="none"/>
    <security:http pattern="/failer.jsp" security="none"/>
    <security:http pattern="/favicon.ico" security="none"/>

    <!--
        配置Spring Security
        auto-config="true"表示自定加載spring-security.xml配置文件
        use-expressions="true"表示使用spring的el表達式來配置spring security
    -->
    <security:http auto-config="true" use-expressions="true">

        <!--指定login.jsp頁面能夠被匿名訪問-->
        <security:intercept-url pattern="/login.jsp" access="permitAll()"/>
        <!--   攔截資源     -->
        <!--
            pattern="/**" 表示攔截全部的資源
            access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER的角色才能訪問資源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
        <!-- 配置認證信息,指定自定義的認證頁面 -->
        <!--
            login-page 指定登陸頁面的地址
            login-processing-url 處理登陸的處理器的地址
            default-target-url 登陸成功跳轉的地址
            authentication-failure-url 登陸失敗跳轉的地址

            默認的用戶名是username,密碼是password,固然也可使用username-parameter和password-parameter修改。
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"/>
        <!--
            指定退出登陸後跳轉的頁面
            logout-url 處理退出登陸的處理器的地址
            logout-success-url 退出登陸成功跳轉的地址
        -->
        <security:logout logout-url="/logout" logout-success-url="/login.jsp"/>

        <!-- 禁用csrf防禦機制 -->
        <security:csrf disabled="true"/>
    </security:http>

    <!--設置Spring Security認證用戶信息的來源-->
    <!--
        Spring Security的認證必須是加密的,{noop}表示不加密認證
    -->
    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <security:user name="user" password="{noop}user"
                               authorities="ROLE_USER"/>
                <security:user name="admin" password="{noop}admin"
                               authorities="ROLE_ADMIN"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
</beans>

在認證頁面攜帶token請求

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%-- 添加標籤庫 --%>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>數據 - AdminLTE2定製版 | Log in</title>

<meta
    content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
    name="viewport">

<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css">
<link rel="stylesheet"
    href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css">
</head>

<body class="hold-transition login-page">
    <div class="login-box">
        <div class="login-logo">
            <a href="#"><b></b>後臺管理系統</a>
        </div>
        <!-- /.login-logo -->
        <div class="login-box-body">
            <p class="login-box-msg">登陸系統</p>
            <form action="${pageContext.request.contextPath}/login" method="post">
                <%-- 在認證頁面攜帶token --%>
                <security:csrfInput/>

                <div class="form-group has-feedback">
                    <input type="text" name="username" class="form-control"
                        placeholder="用戶名"> <span
                        class="glyphicon glyphicon-envelope form-control-feedback"></span>
                </div>
                <div class="form-group has-feedback">
                    <input type="password" name="password" class="form-control"
                        placeholder="密碼"> <span
                        class="glyphicon glyphicon-lock form-control-feedback"></span>
                </div>
                <div class="row">
                    <div class="col-xs-8">
                        <div class="checkbox icheck">
                            <label><input type="checkbox" name="remember-me" value="true"> 記住 下次自動登陸</label>
                        </div>
                    </div>
                    <!-- /.col -->
                    <div class="col-xs-4">
                        <button type="submit" class="btn btn-primary btn-block btn-flat">登陸</button>
                    </div>
                    <!-- /.col -->
                </div>
            </form>

            <a href="#">忘記密碼</a><br>


        </div>
        <!-- /.login-box-body -->
    </div>
    <!-- /.login-box -->

    <!-- jQuery 2.2.3 -->
    <!-- Bootstrap 3.3.6 -->
    <!-- iCheck -->
    <script
        src="${pageContext.request.contextPath}/plugins/jQuery/jquery-2.2.3.min.js"></script>
    <script
        src="${pageContext.request.contextPath}/plugins/bootstrap/js/bootstrap.min.js"></script>
    <script
        src="${pageContext.request.contextPath}/plugins/iCheck/icheck.min.js"></script>
    <script>
        $(function() {
            $('input').iCheck({
                checkboxClass : 'icheckbox_square-blue',
                radioClass : 'iradio_square-blue',
                increaseArea : '20%' // optional
            });
        });
    </script>
</body>

</html>

註銷

  • 須要將header.jsp中的註銷功能,改成form表單提交,而且提交的方式是POST提交,並且在表單攜帶token請求。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<!-- 頁面頭部 -->
<header class="main-header">
    <!-- Logo -->
    <a href="${pageContext.request.contextPath}/pages/main.jsp" class="logo">
        <!-- mini logo for sidebar mini 50x50 pixels -->
        <span class="logo-mini"><b>數據</b></span> <!-- logo for regular state and mobile devices -->
        <span class="logo-lg"><b>數據</b>後臺管理</span>
    </a>
    <!-- Header Navbar: style can be found in header.less -->
    <nav class="navbar navbar-static-top">
        <!-- Sidebar toggle button-->
        <a href="#" class="sidebar-toggle" data-toggle="offcanvas"
           role="button"> <span class="sr-only">Toggle navigation</span>
        </a>

        <div class="navbar-custom-menu">
            <ul class="nav navbar-nav">

                <li class="dropdown user user-menu"><a href="#"
                                                       class="dropdown-toggle" data-toggle="dropdown"> <img
                        src="${pageContext.request.contextPath}/img/user2-160x160.jpg"
                        class="user-image" alt="User Image">
                    <span class="hidden-xs">
                            <%--<security:authentication property="principal.username" />--%>
                            <%--<security:authentication property="name" />--%>
                    </span>
                </a>
                    <ul class="dropdown-menu">
                        <!-- User image -->
                        <li class="user-header"><img
                                src="${pageContext.request.contextPath}/img/user2-160x160.jpg"
                                class="img-circle" alt="User Image"></li>

                        <!-- Menu Footer-->
                        <li class="user-footer">
                            <div class="pull-left">
                                <a href="#" class="btn btn-default btn-flat">修改密碼</a>
                            </div>
                            <div class="pull-right">
                                <%-- 將原來的註銷註釋,使用form表單的形式提交,在表單攜帶token請求 --%>
                                <%--                                <a href="${pageContext.request.contextPath}/login.jsp"--%>
                                <%--                                   class="btn btn-default btn-flat">註銷</a>--%>
                                <form action="${pageContext.request.contextPath}/logout" method="post">
                                    <security:csrfInput/>
                                    <input type="submit" class="btn btn-default btn-flat">註銷</input>
                                </form>
                            </div>
                        </li>
                    </ul>
                </li>
            </ul>
        </div>
    </nav>
</header>
<!-- 頁面頭部 /-->

Spring Security使用數據庫完成認證

認證流程分析

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter是用來負責認證的過濾器。
package org.springframework.security.web.authentication;

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    //視圖認證的方法
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //必須爲POST請求,不然會拋出異常
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            //將填寫的用戶名和密碼封裝到UsernamePasswordAuthenticationToken對象中
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            //調用AuthenticationManager對應進行認證
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

AuthenticationManager

  • 由上面的源碼可知,真正的認證操做在AuthenticationManager裏面。可是AuthenticationManager是接口,其子類是ProviderManager。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    private static final Log logger = LogFactory.getLog(ProviderManager.class);
    private AuthenticationEventPublisher eventPublisher;
    private List<AuthenticationProvider> providers;
    protected MessageSourceAccessor messages;
    private AuthenticationManager parent;
    private boolean eraseCredentialsAfterAuthentication;
    //注意AuthenticationProvider,Spring Security針對每一種認證,什麼QQ登陸,微信登陸都封裝到一個AuthenticationProvider對象中
    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, (AuthenticationManager)null);
    }

    public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
        this.eventPublisher = new ProviderManager.NullEventPublisher();
        this.providers = Collections.emptyList();
        this.messages = SpringSecurityMessageSource.getAccessor();
        this.eraseCredentialsAfterAuthentication = true;
        Assert.notNull(providers, "providers list cannot be null");
        this.providers = providers;
        this.parent = parent;
        this.checkState();
    }

    public void afterPropertiesSet() throws Exception {
        this.checkState();
    }

    private void checkState() {
        if (this.parent == null && this.providers.isEmpty()) {
            throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required");
        }
    }
    //認證的方法
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var8 = this.getProviders().iterator();
        //循環遍歷全部的AuthenticationProvider,匹配當前認證類型
        while(var8.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var8.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    //找到對應的認證類型繼續調用AuthenticationProvider對象完成認證業務
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var13) {
                    this.prepareException(var13, authentication);
                    throw var13;
                } catch (InternalAuthenticationServiceException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = parentResult = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var11) {
            } catch (AuthenticationException var12) {
                parentException = var12;
                lastException = var12;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }

    private void prepareException(AuthenticationException ex, Authentication auth) {
        this.eventPublisher.publishAuthenticationFailure(ex, auth);
    }

    private void copyDetails(Authentication source, Authentication dest) {
        if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) {
            AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;
            token.setDetails(source.getDetails());
        }

    }

    public List<AuthenticationProvider> getProviders() {
        return this.providers;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
        Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
        this.eventPublisher = eventPublisher;
    }

    public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
        this.eraseCredentialsAfterAuthentication = eraseSecretData;
    }

    public boolean isEraseCredentialsAfterAuthentication() {
        return this.eraseCredentialsAfterAuthentication;
    }

    private static final class NullEventPublisher implements AuthenticationEventPublisher {
        private NullEventPublisher() {
        }

        public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
        }

        public void publishAuthenticationSuccess(Authentication authentication) {
        }
    }
}

AuthenticationProvider

  • AuthenticationProvider是接口,其子類是AbstractUserDetailsAuthenticationProvider。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    protected final Log logger = LogFactory.getLog(this.getClass());
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private UserCache userCache = new NullUserCache();
    private boolean forcePrincipalAsString = false;
    protected boolean hideUserNotFoundExceptions = true;
    private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    public AbstractUserDetailsAuthenticationProvider() {
    }

    protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;

    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        this.doAfterPropertiesSet();
    }
    //認證的方法
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                //獲取UserDetails對象,即SpringSecurity本身的用戶對象
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

       //這是個抽象方法,由子類實現
    protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException; 
}
  • DaoAuthenticationProvider是AbstractUserDetailsAuthenticationProvider的子類,有對應retrieveUser方法的實現。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    private PasswordEncoder passwordEncoder;
    private volatile String userNotFoundEncodedPassword;
    private UserDetailsService userDetailsService;
    private UserDetailsPasswordService userDetailsPasswordService;

    public DaoAuthenticationProvider() {
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    ////獲取UserDetails對象,即SpringSecurity本身的用戶對象
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            //UserDetails對象,即SpringSecurity本身的用戶對象
            //loadUserByUsername是真正的認證邏輯,即咱們能夠直接編寫一個UserDetailsService()的實現呢類,告訴SpringSecurity就能夠了。
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }  
}

初步實現本身的認證功能

咱們本身的UserService接口繼承UserDetailService。

package com.weiwei.xu.service;

import com.weiwei.xu.domain.SysUser;
import org.springframework.security.core.userdetails.UserDetailsService;

import java.util.List;
import java.util.Map;

public interface UserService extends UserDetailsService {

    public void save(SysUser user);

    public List<SysUser> findAll();

    public Map<String, Object> toAddRolePage(Integer id);

    public void addRoleToUser(Integer userId, Integer[] ids);
}

編寫loadUserByUsername的邏輯

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        SysUser sysUser = userDao.findByName(username);

        if (null == sysUser) {
            //若是用戶名不對,直接返回null,表示認證失敗
            return null;
        }

        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        List<SysRole> roles = sysUser.getRoles();

        if (null != roles && roles.size() != 0) {
            roles.forEach(role -> {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
                authorities.add(simpleGrantedAuthority);
            });
        }

        //返回UserDetails對象,"{noop}"+密碼錶示不加密認證
        UserDetails userDetails = new User(sysUser.getUsername(), "{noop}" + sysUser.getPassword(), authorities);


        return userDetails;
    }
}

修改SpringSecurity的主配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context
                http://www.springframework.org/schema/context/spring-context.xsd
                http://www.springframework.org/schema/aop
                http://www.springframework.org/schema/aop/spring-aop.xsd
                http://www.springframework.org/schema/tx
                http://www.springframework.org/schema/tx/spring-tx.xsd
                http://www.springframework.org/schema/mvc
                http://www.springframework.org/schema/mvc/spring-mvc.xsd
                http://www.springframework.org/schema/security
                http://www.springframework.org/schema/security/spring-security.xsd">

    <!--直接釋放無需通過SpringSecurity過濾器的靜態資源-->
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/plugins/**" security="none"/>
    <security:http pattern="/failer.jsp" security="none"/>
    <security:http pattern="/favicon.ico" security="none"/>

    <!--
        配置Spring Security
        auto-config="true"表示自定加載spring-security.xml配置文件
        use-expressions="true"表示使用spring的el表達式來配置spring security
    -->
    <security:http auto-config="true" use-expressions="true">

        <!--指定login.jsp頁面能夠被匿名訪問-->
        <security:intercept-url pattern="/login.jsp" access="permitAll()"/>
        <!--   攔截資源     -->
        <!--
            pattern="/**" 表示攔截全部的資源
            access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER的角色才能訪問資源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
        <!-- 配置認證信息,指定自定義的認證頁面 -->
        <!--
            login-page 指定登陸頁面的地址
            login-processing-url 處理登陸的處理器的地址
            default-target-url 登陸成功跳轉的地址
            authentication-failure-url 登陸失敗跳轉的地址

            默認的用戶名是username,密碼是password,固然也可使用username-parameter和password-parameter修改。
        -->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"/>
        <!--
            指定退出登陸後跳轉的頁面
            logout-url 處理退出登陸的處理器的地址
            logout-success-url 退出登陸成功跳轉的地址
        -->
        <security:logout logout-url="/logout" logout-success-url="/login.jsp"/>

        <!-- 禁用csrf防禦機制 -->
<!--        <security:csrf disabled="true"/>-->
    </security:http>

    <!--設置Spring Security認證用戶信息的來源-->
    <!--
        Spring Security的認證必須是加密的,{noop}表示不加密認證
    -->
    <security:authentication-manager>
        <security:authentication-provider user-service-ref="userServiceImpl">
           
        </security:authentication-provider>
    </security:authentication-manager>
</beans>

加密認證

在IOC容器中添加加密對象

  • 在SpringSecurity的主配置文件中添加加密對象。
<!-- 加密對象 -->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>

<!--設置Spring Security認證用戶信息的來源-->
<!--
    Spring Security的認證必須是加密的,{noop}表示不加密認證
-->
<security:authentication-manager>
    <security:authentication-provider user-service-ref="userServiceImpl">
        <!--    指定認證使用的加密對象        -->
        <security:password-encoder ref="passwordEncoder"/>
    </security:authentication-provider>
</security:authentication-manager>

修改認證方法

  • 去掉nohup
@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        SysUser sysUser = userDao.findByName(username);

        if (null == sysUser) {
            //若是用戶名不對,直接返回null,表示認證失敗
            return null;
        }

        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        List<SysRole> roles = sysUser.getRoles();

        if (null != roles && roles.size() != 0) {
            roles.forEach(role -> {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
                authorities.add(simpleGrantedAuthority);
            });
        }

        //返回UserDetails對象,"{noop}"+密碼錶示不加密認證
        UserDetails userDetails = new User(sysUser.getUsername(), sysUser.getPassword(), authorities);


        return userDetails;
    }

修改添加用戶的方法

@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;

@Override
public void save(SysUser user) {
    user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
    userDao.save(user);
}

手動修改數據庫中用戶對應的密碼

  • 將xiaoming帳號對應的密碼改成
$2a$10$ynlaufZM048G5jsp98seeuvkAXNCVD5RFEudlrW.xiNihU.2Tjm9W

手動修改xiaoming帳號對應的密碼

相關文章
相關標籤/搜索