天天看點

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

SpringSecurity

  • SpringSecurity概述
  • SpringSecurity程式設計起步
  • CSRF通路控制
  • 擴充登入和登出功能
  • 擷取認證與授權資訊使用者認證成功後
  • 基于資料庫實作使用者登入
    • 基于SpringSecurity标準認證
    • UserDetailsService
  • Session管理
  • RememberMe
  • 過濾器
  • SpringSecurity注解
  • 投票器
    • AccessDecisionVoter
    • RoleHierarchy
  • 基于Bean配置
    • 基礎配置
    • 深入配置
    • 配置投票管理器

SpringSecurity是Spring提供用于認證與授權檢測處理的架構,利用它可以友善的實作登入認證與授權控制管理。

項目源碼:https://gitee.com/tirklee/lea-springsecurity.git

SpringSecurity是最早提供登入認證與授權檢測的架構。當下最流行的是Shiro開發架構(即SSM開發架構Spring+Shiro+MyBatis組成),雖然SpringSecurity已經很少出現在Web項目項目中,但微服務架構(SpringCloud)的流行,使得很多項目使用了SpringSecurity。

SpringSecurity概述

SpringSecurity由Acegi Security開發架構演變而來,是一套完整的Web安全解決方案,給予SpringAOP與過濾器實作安全通路控制。主要有兩大部分使用者認證與使用者授權。

  • 使用者認證(Authentication):判斷某個使用者是否是系統的合法操作體,是否具有系統的操作權力。在進行使用者認證進行中,核心資訊為使用者名與密碼。
  • 使用者授權(Authorization):一個系統中不同的使用者擁有不同的權限(或稱為角色),利用權限可以實作不同級别使用者的劃分,進而保證系統操作的安全。

為了實作安全管理,SpringSecurity提供了一系列通路過濾器。所有通路過濾器都圍繞着認證管理和決策管理展開。認證管理由SpringSecurity負責,開發者隻需要掌握UserDetailsService(使用者認證服務)、AccessDecisionVoter(決策管理器)兩個核心接口即可。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

SpringSecurity中除了提供認證與授權外,還提供了Session管理、RememberMe(記住我)等常見功能。

SpringSecurity程式設計起步

1.【lea-springsecurity項目】建立一個MessageAction程式類,進行資訊顯示。

package com.xiyue.leaspring.action;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller//定義控制器
@RequestMapping("/pages/info")//定義通路父路徑,與方法中的路徑組合為完整的路徑
public class MessageAction {//自定義Action程式
    @RequestMapping("/url")//通路的路徑為url
    @ResponseBody
    public Object url(){
        return "www.xiyue.com";
    }
}
           

利用SpringSecurity元件利用Web過濾器對指定的請求攔截路徑通路進行檢測,檢測通過後,可正常通路相應服務;如果檢測失敗,會顯示相應的錯誤資訊。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

2.【lea-springsecurity項目】修改pom.xml配置檔案,追加SpringSecurity相關依賴庫管理。

<spring.security.version>5.4.5</spring.security.version>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring.security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>${spring.security.version}</version>
</dependency>
           

3.【lea-springsecurity項目】修改web.xml配置檔案,追加過濾器配置。

<!--SpringSecurity過濾器配置-->
 <filter-mapping>
     <filter-name>springSecurityFilterChain</filter-name>
     <url-pattern>/*</url-pattern>
 </filter-mapping>
 <!--配置編碼過濾器,已解決資料傳輸亂碼問題-->
 <filter-mapping><!--所有路徑都必須經過此過濾器-->
     <filter-name>encoding</filter-name>
     <url-pattern>/*</url-pattern>
 </filter-mapping>
           

/*表示所有的請求都需要SpringSecurity進行檢測。

4.【lea-springsecurity項目】在spring.xml中配置定義認證路徑以及使用者資訊。

<security:http auto-config="true"><!--啟用HTTP安全認證,并采用自動配置模式-->
        <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
        <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
    </security:http>
    <security:authentication-manager><!--定義認證管理器-->
        <security:authentication-provider><!--配置認證管理配置類-->
            <security:user-service><!--建立使用者資訊-->
                <!--定義使用者名、密碼(使用BCryptPasswordEncode加密器進行加密)、角色資訊(必須追加ROLE_字首,否則無法識别)-->
                <!--使用者名:admin,密碼:hello,角色:ADMIN、USER-->
                <security:user name="admin" authorities="ROLE_ADMIN,ROLE_USER"
                    password="{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16"/>
                <!--使用者名:xiyue,密碼:java,角色:USER-->
                <security:user name="xiyue" authorities="ROLE_USER"
                    password="{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
           

spring.xml檔案主要配置了要使用的使用者認證與授權資訊,且采用了自動配置模式(<security:http auto-config=“true”>),是以并沒有具體的登入頁面,而是登入時自動為使用者提供内置登入頁。

提問:關于密碼定義的一些問題。進行使用者資訊配置時,密碼使用“{bcrypt}$2a$10$2…”形式進行了定義。這是什麼含義?是如何生成的?回答:bcrypt是Spring提供的一種加密算法。為了使用者認證資訊安全,密碼通路時,SpringSecurity使用标準接口org.springframework.security.crypto.password.PasswordEncoder定義加密處理标準。此接口及其常用子類如圖所示。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

新版SpringSecurity推薦的加密算法為bcrypt,是以這裡定義密碼時使用了{bcrypt}标記。為了友善管理不同的加密器,SpringSecurity還提供了PasswordEncoderFactories工廠類,該類中注冊了所有的加密器(有一些加密器已經不建議使用,如{noop}、{ldap}、{MD5}等)。如果想定義自己的密碼,可利用如下程式完成。

package com.xiyue.leaspring.test;


import org.junit.Test;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

public class TestPasswordEncoder {

    @Test
    public void testPassWord(){
        String password = "java";//定義明文密碼
        //預設采用BCryptPasswordEncode加密處理器
        PasswordEncoder passwordEncoder = PasswordEncoderFactories
                .createDelegatingPasswordEncoder();//擷取加密器執行個體
        String encode = passwordEncoder.encode(password);//加密
        System.out.println("加密後的密碼:"+encode);//加密密碼
        System.out.println("密碼比較:"+passwordEncoder.matches(password,encode));
    }
}
           

在SpringSecurity配置檔案裡,最重要的是攔截路徑與授權資訊檢測表達式(access屬性,該屬性為boolean類型)。對于授權檢測,常見配置如表所示。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

5.【lea-springsecurity項目】程式配置完成後可以直接啟動Web容器,當通路到/pages/info/url程式路徑時會自動跳轉到/login路徑進行登入,如圖所示。由于此時該路徑通路需要具有ADMIN角色的使用者才可以通路,是以輸入admin/hello賬戶資訊,随後将顯示如圖

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置
SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

提示:登入登出路徑。 在預設情況下,SpringSecurity會提供登入表單,并且會自動将表單的送出路徑設定為/login,如果登入後需要登出,可以使用/login?logout路徑進行登出。

8.【lea-springsecurity項目】如果使用者覺得利用表單登入不太友善,也可以采用http-basic模式實作登入控制,隻需要修改spring.xml配置檔案即可。

<security:http auto-config="true"><!--啟用HTTP安全認證,并采用自動配置模式-->
        <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
        <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:http-basic/><!--采用http-basic模式登入 -->
    </security:http>
           

由于采用了http-basic模式進行登入控制,當使用者需要進行認證處理時将不會跳轉到登入表單,會出現如圖所示的彈出界面,在相應位置上輸入使用者名與密碼即可正常通路。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

CSRF通路控制

CSRF(Cross-Site Request Forgery,跨站請求僞造)是一種常見的網絡攻擊模式,攻擊者可以在受害者完全不知情的情況下,以受害者身份發送各種請求(如郵件處理、賬号操作等),且伺服器認為這些操作屬于合法通路。CSRF攻擊的基本操作流程如圖所示。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

使用者通路伺服器A時,會在用戶端浏覽器中記錄相應的Cookie資訊,利用此Cookie進行使用者身份的标注。在通路伺服器B時,被植入了惡意程式代碼,是以這些代碼會在使用者不知情的情況下以伺服器A認證的身份通路其中的資料。

在實際開發中,有3種形式可以解決CSRF漏洞:**驗證HTTP請求頭資訊中的Referer資訊,在通路路徑中追加token标記,以及在HTTP資訊頭中定義驗證屬性。**在SpringSecurity中可以采用token的形式進行驗證,下面通過程式示範CSRF攻擊的防範操作,本程式所采用的通路流程如圖所示;

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

1.【lea-springsecurity項目】建立EchoAction程式類,主要負責資訊輸入頁面的跳轉與内容回顯處理。

package com.xiyue.leaspring.action;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller//定義控制器
@RequestMapping("/pages/message/")//定義通路父路徑,與方法中的路徑組合為完整的路徑
public class EchoAction {//自定義Action程式

    @RequestMapping("/show")//通路的路徑為url
    public ModelAndView echo(String msg){
        return new ModelAndView("message/message_show").addObject("echoMessage","[ECHO]msg="+msg);
    }

    @GetMapping("/input")//通路的路徑
    public String input(){
        return "message/message_input";//jump route
    }
}
           

2.【lea-springsecurity項目】修改spring-security.xml配置檔案,追加請求攔截路徑。

<security:http auto-config="true"><!--啟用HTTP安全認證,并采用自動配置模式-->
        <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
        <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <security:http-basic/><!--采用http-basic模式登入 -->
    </security:http>
           

3.【lea-springsecurity項目】定義/WEB-INF/pages/message/message_input.jsp頁面,在表單定義時傳送CSRF-Token資訊。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page isELIgnored="false" %><%--啟動EL表達式解析--%>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
        +request.getContextPath();
    String message_input_url = basePath+"/pages/message/show";
%>
<base href="<%=basePath%>">
<form action="<%=message_input_url%>" method="post">
消息内容:<input type="text" name="msg" value="www.nieyi.com">
    <%--傳遞CSRF-Token資訊,參數名稱_csrf,參數内容為随機生成的Token資料--%>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
    <input type="submit" value="發送"><input type="reset" value="重置">
</form>
           

在進行表單定義時,利用隐藏域實作了CSRF-Token資訊的定義。下圖示範了生成後的token内容。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

4.【lea-springsecurity項目】定義/WEB-INF/pages/message/message_show.jsp頁面,回顯輸入内容。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
            +request.getContextPath();
    String message_input_url = basePath+"/pages/message/show";
%>
<base href="<%=basePath%>">
<h1>ECHO消息顯示:${echoMessage}</h1>
           

至此,程式的基本流程開發完畢。使用者進行資訊輸入時,如果發現沒有CSRF-Token資訊,将會跳轉到security:access-denied-handler元素定義的錯誤資訊顯示頁面。

5.【lea-springsecurity項目】雖然CSRF-Token可以解決CSRF攻擊問題,但如果有些項目不需要處理CSRF漏洞,也可以通過配置的方式關閉CSRF校驗。隻要直接修改spring-security.xml配置檔案中的security:http配置項即可。

<security:http auto-config="true"><!--啟用HTTP安全認證,并采用自動配置模式-->
   <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
    <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
    <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
    <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
    <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
    <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
    <security:http-basic/><!--采用http-basic模式登入 -->
    <security:csrf disabled="true"/><!--關閉CSRF校驗-->
</security:http>
           

此時項目中,即便表單送出時沒有CSRF-Token也可以正常通路。

擴充登入和登出功能

在SpringSecurity中使用者也可以修改預設的登入與登出操作,自定義相關頁面進行顯示。

1.【lea-springsecurity項目】建立/WEB-INF/pages/login.jsp使用者登入頁面,該頁面主要提供登入表單。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page isELIgnored="false" %><%--啟動EL表達式解析--%>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"
            +request.getServerPort()+request.getContextPath()+"/";
    String login_url = basePath + "xylogin";
%>
<html>
<head>
    <title>登陸</title>
    <base href="<%=basePath%>">
</head>
<body>
<h3>使用者登陸請輸入使用者名與密碼</h3>

<form action="<%=login_url%>" method="post">
    使用者名:
    <input name="mid" type="text" placeholder="請輸入字元串使用者名"><br>
    密碼:
    <input name="pwd" type="password" placeholder="請輸入密碼"><br>
    <input type="submit" value="登陸">
</form>
</body>
</html>
           

自定義登陸路徑/sclogin(在spring.xml中配置)。

2.【lea-springsecurity項目】建立/WEB-INF/pages/welcome.jsp頁面,作為使用者登入成功後的首頁。

<%@page pageEncoding="UTF-8"%>
<html>
    <head>
        <%
            request.setCharacterEncoding("UTF-8");
            String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
                +request.getContextPath()+"/";
            String logoutUrl = basePath+"xylogout";
        %>
        <title>SpringSecurity安全架構</title>
        <base href="<%=basePath%>"/>
    </head>
    <body>
        <h2>登入成功,歡迎您回來,也可以選擇<a href="<%=logoutUrl%>">登出</a>!</h2>
        <h3>更多内容請通路<a href="http://www.baidu.com">喜悅</a></h3>    
    </body>
</html>
           

頁面中提供了登出路徑/logout(需要在spring.xml中配置)

3.【lea-springsecurity項目】建立/WEB-INF/pages/logout.jsp頁面進行登出後的顯示頁面。

<%@page pageEncoding="UTF-8"%>
<html>
    <head>
        <%
            request.setCharacterEncoding("UTF-8");
            String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
                +request.getContextPath();
        %>
        <title>SpringSecurity安全架構</title>
        <base href="<%=basePath%>"/>
    </head>
    <body>
        <h2>登出成功,歡迎您再來!</h2>
        <h3>更多内容請通路<a href="http://www.baidu.com">喜悅</a></h3>    
    </body>
</html>
           

4.【lea-springsecurity項目】由于所有JSP頁面都儲存在WEB-INF目錄下,是以為了更友善通路頁面,可以定義一個GlobalAction程式類,利用此程式類實作跳轉。

package com.xiyue.leaspring.action;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class GlobalAction {

    @RequestMapping("/loginPage")
    public String loginpage(){
        return "login";
    }
    
    @RequestMapping("/welcomePage")
    public String welcome(){
        return "welcome";
    }
    
    @RequestMapping("/logoutPage")
    public String logout(){
        return "logout";
    }
}

           

5.【lea-springsecurity項目】修改spring.xml配置檔案,配置登入與登出。

<security:http auto-config="true"><!--啟用HTTP安全認證,并采用自動配置模式-->
        <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
        <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <!--采用http-basic模式登入 -->
        <!--
            <security:http-basic/>
        -->
        <!--登入成功後的首頁,需要在使用者已經認證後才可以顯示-->
        <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
        <security:csrf disabled="true"/><!--關閉CSRF校驗-->
        <!--配置表單登入-->
        <security:form-login
            username-parameter="mid"
            password-parameter="pwd"
            authentication-success-forward-url="/welcomePage"
            login-page="/loginPage"
            login-processing-url="/xylogin"
            authentication-failure-url="/loginPage?error=true"
            />
        <security:logout
            logout-url="/xylogout"
            logout-success-url="/logoutPage"
            delete-cookies="JSESSIONID"
        />
    </security:http>
           

建立完成後,SpringSecurity認證檢測失敗後會自動跳轉到/loginPage.action路徑,登入成功後會自動跳轉到/welcomePage.action路徑,登出時會自動清除對應的Cookie資料,進而實作了自定義登入與登出操作。

擷取認證與授權資訊使用者認證成功後

SpringSecurity會将使用者的認證資訊與授權資訊儲存在Session中,而儲存的資訊類型為org.springframework.security.core.userdetails.User類對象,此類的繼承結構如下:

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

SpringSecurity中有兩個核心接口儲存使用者資訊:

  • GrantedAuthority:儲存授權資訊。
  • UserDetails:描述使用者的詳情與使用者授權資訊。

    在SpringSecurity預設配置下,Spring容器會自動幫助使用者建立User類對象,并且将使用者對應的認證資訊與授權資訊儲存在User類對象中,要想擷取這些資訊可以采用下表的方法完成。

    SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置
    1.【lea-springsecurity項目】Action中擷取認證與授權資訊時,所有認證資料都儲存在Authentication接口執行個體中,是以要先擷取Authentication接口對象,然後才可以通過Authentication得到UserDetails接口對象。
@RequestMapping("/welcomePage")//通路路徑
    public String welcome(){//登入成功路徑
        Authentication authentication = SecurityContextHolder
                .getContext().getAuthentication();//擷取認證對象
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();//使用者詳情
        String username = userDetails.getUsername();//獲得使用者名
        this.logger.info("使用者名:"+username);
        //通過userDetail對象擷取目前使用者的所有授權資訊
        Collection<? extends GrantedAuthority> authorities =userDetails.getAuthorities();
        this.logger.info("授權資訊:"+authorities);
        return "welcome";//設定跳轉路徑
    }
           

2.【lea-springsecurity項目】在實際項目中,通常需要在JSP頁面中擷取相應的認證與授權資訊,此時可以直接引入SpringSecurity的标簽來獲得。

<%@page pageEncoding="UTF-8"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<html>
    <head>
        <%
            request.setCharacterEncoding("UTF-8");
            String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()
                +request.getContextPath()+"/";
            String logoutUrl = basePath+"xylogout";
        %>
        <title>SpringSecurity安全架構</title>
        <base href="<%=basePath%>"/>
    </head>
    <body>
        <security:authorize access="isAuthenticated()"><%--是否為認證過的使用者--%>
            使用者已經登入成功了!
        </security:authorize>
        <security:authorize access="hasRole('USER')"><%--是否擁有USER角色--%>
            擁有USER角色
        </security:authorize>
        <security:authorize access="hasRole('ADMIN')"><%--是否擁有ADMIN角色--%>
            擁有ADMIN角色
        </security:authorize>
        <h2>登入成功,歡迎【<security:authentication property="principal.username"/>】您回來,也可以選擇<a href="<%=logoutUrl%>">登出</a>!</h2>
        <h3>更多内容請通路<a href="http://www.baidu.com">喜悅</a></h3>    
    </body>
</html>
           

本程式主要使用了兩個标簽,核心作用如下。  

security:authentication:擷取認證資訊,通過Authentication擷取UserDetails,得到使用者名。  

security:authorize:授權資訊,采用Spring表達式進行判斷(與攔截路徑判斷一緻)。

基于資料庫實作使用者登入

為了靈活管理使用者的登入資訊,在實際項目中需要将使用者資訊儲存在資料庫中,登入時利用資料庫對認證資訊進行檢測。SpringSecurity與資料庫的認證整合處理。

基于SpringSecurity标準認證

SpringSecurity本身可以直接利用配置檔案實作使用者認證與授權資訊的查詢處理,但是需要開發者在項目中配置好相應的資料庫連接配接,同時由于SpringSecurity自身的查詢約定,在定義查詢語句時也需要對傳回查詢列的名稱統一。

提示:關于本次基于資料庫查詢的操作。

為了盡可能幫助讀者了解SpringSecurity中的自動查詢處理支援,在本程式中将使用自定義表結構的形式完成(在查詢的時候将為列定義别名以符合SpringSecurity查詢要求),同時對于資料庫的配置也将使用之前講解過的Druid連接配接池。

資料表結構如下:

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

1.【lea-springsecurity項目】定義資料庫建立腳本,該腳本資訊的組成與之前固定認證資訊的結構相同。

--删除資料庫
drop database lea_springsecurity;
--建立資料庫
create database lea_springsecurity default character set utf8;
--使用資料庫
use lea_springsecurity;
--建立使用者表(mid:登入ID;name:真實姓名;password:登入密碼;enabled:啟用狀态 )
--enabled取值有兩種:啟用(enabled=1),鎖定(enabled=0)
CREATE TABLE member(
    mid varchar(50),
    name varchar(50),
    password varchar(68),
    enabled INT(1),
    CONSTRAINT pk_mid PRIMARY KEY(mid)
) ENGINE = innodb;
--建立角色表(rid:角色ID,也就是檢測的名稱;title:角色名稱)
CREATE TABLE role(
    rid varchar(50),
    title varchar(50),
    CONSTRAINT pk_rid PRIMARY KEY(rid)
) ENGINE = innodb;
--建立使用者角色關聯表(mid:使用者ID;rid:角色ID)
CREATE TABLE member_role(
    mid varchar(50),
    rid varchar(50),
    CONSTRAINT fk_mid FOREIGN KEY (mid) REFERENCES member(mid) ON DELETE CASCADE,
    CONSTRAINT fk_rid FOREIGN KEY (rid) REFERENCES role(rid) ON DELETE CASCADE
) ENGINE = innodb;
--增加使用者資料(admin/hello,xiyue/java)
INSERT INTO member(mid,name,password,enabled)values('admin','admin','{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16',1);
INSERT INTO member(mid,name,password,enabled)values('xiyue','xiyue','{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em',0);
--增加角色資料
INSERT INTO role(rid,title)values('ROLE_ADMIN','ADMIN');
INSERT INTO role(rid,title)values('ROLE_USER','USER');
--增加使用者與角色資訊
INSERT INTO member_role(mid,rid)values('admin','ROLE_ADMIN');
INSERT INTO member_role(mid,rid)values('admin','ROLE_USER');
INSERT INTO member_role(mid,rid)values('xiyue','ROLE_USER');
--送出事務
COMMIT;
           

2.【lea-springsecurity項目】修改spring.xml配置檔案中的security:authentication-provider元素定義,将固定資訊驗證修改為JDBC的形式。

<security:authentication-manager><!--定義認證管理器-->
        <security:authentication-provider><!--配置認證管理配置類-->
            <security:jdbc-user-service
                    data-source-ref="dataSource"
                    users-by-username-query="select mid as username,password,enabled from member where mid=?"
                    authorities-by-username-query="select mid as username,rid as authorities from member_role where mid=?"/>
        </security:authentication-provider>
    </security:authentication-manager>
           

本程式主要使用security:jdbc-user-service元素配置資料庫認證,屬性作用如下。

  • data-source-ref:定義要使用的資料源對象。
  • users-by-username-query:使用者認證查詢,要求傳回使用者名(username)、密碼(password)、啟用狀态(enabled),由于資料表中列的名稱與查詢要求不符,是以在查詢時需要為查詢列定義别名。
  • authorities-by-username-query:使用者角色查詢,根據認證的使用者名傳回相應的角色資訊,傳回結構要求擁有使用者名(username)、角色(authorities)兩個資訊。

UserDetailsService

在SpringSecurity中除了可以使用配置檔案進行查詢配置外,還可以由使用者通過UserDetailsService接口标準實作自定義認證與授權資訊查詢處理,而使用UserDetailsService實作的查詢會比配置檔案定義查詢更加靈活。UserDetailsService接口如下。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

在UserDetailsService接口裡隻有一個loadUserByUsername方法,此方法會根據使用者名查詢對應的使用者資訊與授權資訊,而使用者和授權資訊由于都儲存在資料庫中,是以可利用SpringDataJPA實作查詢操作。下面示範UserDetailsService接口的使用。

1.【lea-springsecurity項目】定義使用者資訊資料層操作接口。

package com.xiyue.leaspring.po;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.io.Serializable;

@Entity
public class Member implements Serializable {

    @Id
    private String mid;
    private String name;
    private String password;
    private Integer enabled;

    public String getMid() {
        return mid;
    }

    public void setMid(String mid) {
        this.mid = mid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getEnabled() {
        return enabled;
    }

    public void setEnabled(Integer enabled) {
        this.enabled = enabled;
    }

    @Override
    public String toString() {
        return "Member{" +
                "mid='" + mid + '\'' +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", enabled=" + enabled +
                '}';
    }
}
           
package com.xiyue.leaspring.dao;

import com.xiyue.leaspring.po.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface IMemberDAO extends JpaRepository<Member,String> {
}
           

2.【lea-springsecurity項目】定義授權資訊資料層操作接口。

package com.xiyue.leaspring.po;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.io.Serializable;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 18:44:48
 */
@Entity
public class Role implements Serializable {

    @Id
    private String rid;
    private String title;

    public String getRid() {
        return rid;
    }

    public void setRid(String rid) {
        this.rid = rid;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return "Role{" +
                "rid='" + rid + '\'' +
                ", title='" + title + '\'' +
                '}';
    }
}
           
package com.xiyue.leaspring.dao;

import com.xiyue.leaspring.po.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Set;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 18:47:46
 */
public interface IRoleDAO extends JpaRepository<Role,String> {
    /**
     * 根據使用者ID查詢對應的角色ID
     * @param mid 使用者ID
     * @return 使用者擁有的全部角色ID
     */
    @Query(nativeQuery = true,value = "select rid from member_role where " +
            "mid=:mid")
    public Set<String> findAllByMember(@Param("mid") String mid);
}
           

3.【lea-springsecurity項目】定義UserDetailsService接口子類,注入相應的資料層接口對象,實作資料查詢。在對查詢結果進行判斷時,可以用AuthenticationException異常類抛出異常。

AuthenticationException異常類的常用子類如下圖。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置
package com.xiyue.leaspring.service.impl;

import com.xiyue.leaspring.dao.IMemberDAO;
import com.xiyue.leaspring.dao.IRoleDAO;
import com.xiyue.leaspring.po.Member;
import com.xiyue.leaspring.service.UserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * 使用者接口實作類
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 18:59:10
 */
@Service(value = "userDetailService")//注解配置,此名稱要在spring.xml中使用
public class UserDetailsServiceImpl implements UserDetailService {

    @Autowired
    private IMemberDAO memberDAO;//注入使用者資料操作接口

    @Autowired
    private IRoleDAO roleDAO;//注入角色操作接口

    @Override
    public UserDetails loadUserByUsername(String username)
        throws UsernameNotFoundException {
        Optional<Member> optionalMember = this.memberDAO.findById(username);//根據使用者ID進行查詢
        if(!optionalMember.isPresent()){//使用者資訊不存在
            throw new UsernameNotFoundException("使用者“"+
                    username+"”資訊不存在,無法進行登入。");
        }
        Member member = optionalMember.get();//擷取使用者對象
        //使用者對應的所有角色需要通過GrantedAuthority集合儲存
        List<GrantedAuthority> allGrantedAuthority = new ArrayList<>();
        Set<String> allRoles= this.roleDAO.findAllByMember(username);//擷取使用者角色資訊
        Iterator<String> roleIter = allRoles.iterator();//疊代輸出角色資訊
        while (roleIter.hasNext()){
            allGrantedAuthority.add(new SimpleGrantedAuthority(roleIter.next()));
        }
        boolean enabled = member.getEnabled().equals(1);//判斷用狀态
        UserDetails userDetails = new User(username,member.getPassword(),enabled
            ,true,true,true,allGrantedAuthority);

        return userDetails;//傳回UserDetails對象
    }
}
           

4.【lea-springsecurity項目】修改spring.xml配置檔案,使用UserDetailsService處理登入。

<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
    <context:component-scan base-package="com.xiyue.leaspring">
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.action"/>
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.dao"/>
    </context:component-scan>
    <context:property-placeholder location="classpath:database.properties"/>
    <!--<bean id="dataSource"
          class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${database.driverClass}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.user}"/>
        <property name="password" value="${database.password}"/>
    </bean>-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init">
        <property name="driverClassName" value="${database.druid.driverClassName}"/><!--驅動-->
        <property name="url" value="${database.druid.url}"/><!--位址-->
        <property name="username" value="${database.druid.username}"/><!--使用者-->
        <property name="password" value="${database.druid.password}"/><!--密碼-->
        <property name="maxActive" value="${database.druid.maxActive}"/><!--最大連接配接數-->
        <property name="minIdle" value="${database.druid.minIdle}"/><!--最小連接配接池-->
        <property name="initialSize" value="${database.druid.initialSize}"/><!--初始化連接配接大小-->
        <property name="maxWait" value="${database.druid.maxWait}"/><!--最大等待時間-->
        <property name="timeBetweenEvictionRunsMillis" value="${database.druid.timeBetweenEvictionRunsMillis}"/><!--檢測空閑連接配接間隔-->
        <property name="minEvictableIdleTimeMillis" value="${database.druid.minEvictableIdleTimeMillsis}"/><!--連接配接最小生存時間-->
        <property name="validationQuery" value="${database.druid.validationQuery}"/><!--驗證-->
        <property name="testWhileIdle" value="${database.druid.testWhileIdle}"/><!--申請檢測-->
        <property name="testOnBorrow" value="${database.druid.testIOnBorrow}"/><!--有效檢測-->
        <property name="testOnReturn" value="${database.druid.testIOnReturn}"/><!--歸還檢測-->
        <property name="poolPreparedStatements" value="${database.druid.poolPreparedStatements}"/><!--是否緩存preparedStatement,也就是PSCache。PSCache能提升支援遊标的資料庫性能,如Oracle、Mysql下建議關閉-->
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${database.druid.maxpoolPreparedStatementPerConnectionSize}"/><!--啟用PSCache,必須配置大于0,當大于0時-->
        <property name="filters" value="${database.druid.filters}"/><!--驅動-->
    </bean>
    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/><!-- 資料源 -->
        <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/><!-- JPA核心配置檔案 -->
        <property name="persistenceUnitName" value="LEA_SPRING_JPA"/><!-- 持久化單元名稱 -->
        <property name="packagesToScan" value="com.xiyue.leaspring.dao"/><!-- PO類掃描包 -->
        <property name="persistenceProvider"><!-- 持久化提供類,本次為hibernate -->
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
        </property>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
        </property>
    </bean>
    <!-- 定義SpringDataJPA的資料層接口所在包,該包中的接口一定義是Repository子接口 -->
    <jpa:repositories base-package="com.xiyue.leaspring.dao"/>
    <!-- 定義事務管理的配置,必須配置PlatformTransactionManager接口子類 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <security:http auto-config="true"><!--啟用HTTP安全認證,并采用自動配置模式-->
        <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
        <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <!--采用http-basic模式登入 -->
        <!--
            <security:http-basic/>
        -->
        <!--登入成功後的首頁,需要在使用者已經認證後才可以顯示-->
        <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
        <security:csrf disabled="true"/><!--關閉CSRF校驗-->
        <!--配置表單登入-->
        <security:form-login
            username-parameter="mid"
            password-parameter="pwd"
            authentication-success-forward-url="/welcomePage"
            login-page="/loginPage"
            login-processing-url="/xylogin"
            authentication-failure-url="/loginPage?error=true"
            />
        <security:logout
            logout-url="/xylogout"
            logout-success-url="/logoutPage"
            delete-cookies="JSESSIONID"
        />
    </security:http>
    <security:authentication-manager><!--定義認證管理器-->
        <security:authentication-provider><!--配置認證管理配置類-->
            <security:jdbc-user-service
                    data-source-ref="dataSource"
                    users-by-username-query="select mid as username,password,enabled from member where mid=?"
                    authorities-by-username-query="select mid as username,rid as authorities from member_role where mid=?"/>
        </security:authentication-provider>
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>
</beans>
           

配置完成後,目前程式會使用UserDetailsServiceImpl實作子類進行使用者認證資訊與授權資訊的擷取,并且按照SpringSecurity的要求所有的資訊都會包裝在UserDetails接口對象中。

Session管理

在系統管理中,為了使用者資訊的安全往往會對同一個賬戶的并發登入狀态進行控制,是以在這種情況下往往需要對使用者登入狀态進行監聽,即需要在記憶體中儲存相應使用者的Session清單,當出現賬戶重複登入的時候就可以進行指定Session的剔除操作。

1.【lea-springsecurity項目】修改web.xml配置檔案,追加Session管理監聽器。

<!--定義Session管理監聽器-->
<listener>
   <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
           

2.【lea-springsecurity項目】修改spring.xml配置檔案,取消自動配置,同時追加Session并發管理。

<security:http auto-config="false">
        <security:session-management invalid-session-url="/loginPage">
            <!--并發Session管理-->
            <!--max-sessions="1" 每個賬戶并發通路量-->
            <!--error-if-maximum-exceeded="false" Session剔除模式-->
            <!--expired-url="/loginPage" Session剔除後的錯誤顯示路徑-->
            <security:concurrency-control
                max-sessions="1"
                error-if-maximum-exceeded="false"
                expired-url="/loginPage"
            />
        </security:session-management>
        <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
        <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <!--采用http-basic模式登入 -->
        <!--
            <security:http-basic/>
        -->
        <!--登入成功後的首頁,需要在使用者已經認證後才可以顯示-->
        <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
        <security:csrf disabled="true"/><!--關閉CSRF校驗-->
        <!--配置表單登入-->
        <security:form-login
            username-parameter="mid"
            password-parameter="pwd"
            authentication-success-forward-url="/welcomePage"
            login-page="/loginPage"
            login-processing-url="/xylogin"
            authentication-failure-url="/loginPage?error=true"
            />
        <security:logout
            logout-url="/xylogout"
            logout-success-url="/logoutPage"
            delete-cookies="JSESSIONID"
        />
    </security:http>
           

對于并發Session管理,需要考慮的是要剔除之前已登入的Session還是剔除之後登入的Session使用者,這點可通過error-if-maximum-exceeded屬性進行配置。該屬性配置為true,表示剔除新登入Session使用者;為false,表示剔除之前登入過的Session使用者。

RememberMe

為了防止使用者重複登入表單的填寫,在實際項目中往往采用RememberMe功能,将使用者登入資訊暫時儲存在Cookie中,這樣每次通路時就可以通過請求Cookie資訊擷取使用者登入狀态。SpringSecurity對這一功能提供了配置實作。

1.【lea-springsecurity項目】修改login.jsp頁面,追加免登入元件(複選框)。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page isELIgnored="false" %><%--啟動EL表達式解析--%>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"
            +request.getServerPort()+request.getContextPath()+"/";
    String login_url = basePath + "xylogin";
%>
<html>
<head>
    <title>登陸</title>
    <base href="<%=basePath%>">
</head>
<body>
<h3>使用者登陸請輸入使用者名與密碼</h3>

<form action="<%=login_url%>" method="post">
    使用者名:
    <input name="mid" type="text" placeholder="請輸入字元串使用者名"><br>
    密碼:
    <input name="pwd" type="password" placeholder="請輸入密碼"><br>
    <input type="checkbox" id="remember" name="remember" value="true"/>下次免登入</br>
    <input type="submit" value="登入">
    <input type="reset" value="重置">
</form>
</body>
</html>
           

2.【lea-springsecurity項目】修改spring.xml配置檔案,追加RememberMe配置項。

<security:http auto-config="false">
    <security:session-management invalid-session-url="/loginPage">
        <!--并發Session管理-->
        <!--max-sessions="1" 每個賬戶并發通路量-->
        <!--error-if-maximum-exceeded="false" Session剔除模式-->
        <!--expired-url="/loginPage" Session剔除後的錯誤顯示路徑-->
        <security:concurrency-control
            max-sessions="1"
            error-if-maximum-exceeded="false"
            expired-url="/loginPage"/>
    </security:session-management>
    <!--啟用RememberMe功能-->
    <!-- remember-me-parameter="remember" 登入表單參數-->
    <!-- key="xiyue-li" Cookies加密密鑰-->
    <!-- token-validity-seconds="2592000" 免登入失效(機關為s)-->
    <!-- remember-me-cookie="xiyue-remember-cookies" Cookies名稱-->
    <!-- user-service-ref="userDetailService" 處理類-->
    <!-- data-source-ref="dataSource" 持久化儲存資料源-->
    <security:remember-me
        remember-me-parameter="remember"
        key="xiyue-li"
        token-validity-seconds="2592000"
        remember-me-cookie="xiyue-remember-cookies"
        user-service-ref="userDetailService"
        data-source-ref="dataSource"
    />
    <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
    <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
    <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
    <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
    <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
    <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
    <!--采用http-basic模式登入 -->
    <!--
        <security:http-basic/>
    -->
    <!--登入成功後的首頁,需要在使用者已經認證後才可以顯示-->
    <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
    <security:csrf disabled="true"/><!--關閉CSRF校驗-->
    <!--配置表單登入-->
    <security:form-login
        username-parameter="mid"
        password-parameter="pwd"
        authentication-success-forward-url="/welcomePage"
        login-page="/loginPage"
        login-processing-url="/xylogin"
        authentication-failure-url="/loginPage?error=true"
        />
    <security:logout
        logout-url="/xylogout"
        logout-success-url="/logoutPage"
        delete-cookies="JSESSIONID"
    />
</security:http>
           

本配置檔案中定義了表單要使用的登入參數remember,這樣當使用者登入後會自動在Cookie中提供mldn-rememberme-cookie内容下圖。該Cookie的失效時間為30天,是以使用者在30天年内重新打開浏覽器不必再重複編寫登入表單。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

3.【lea-springsecurity項目】現在已經實作了RememberMe功能,此時的使用者資訊是在伺服器記憶體中儲存的。如果有需要,也可以将所有RememberMe資訊在資料庫中記錄。此時需要使用如下的資料庫建立腳本。

--使用資料庫
use lea_springsecurity;
--建立資料表儲存免費登入資訊(資料表名稱預設為persistent_logins)
CREATE TABLE persistent_logins(
    series varchar(64),
    username varchar(100),
    token varchar(64),
    last_used TIMESTAMP,
    CONSTRAINT pk_series PRIMARY KEY (series)
);
           

4.【lea-springsecurity項目】修改spring.xml配置檔案,在RememberMe的配置中追加資料源設定。

<!--true啟用HTTP安全認證,并采用自動配置模式-->
    <!--flase取消自動配置模式-->
    <security:http auto-config="false">
        <!--啟用RememberMe功能-->
        <!-- remember-me-parameter="remember" 登入表單參數-->
        <!-- key="xiyue-li" Cookies加密密鑰-->
        <!-- token-validity-seconds="2592000" 免登入失效(機關為s)-->
        <!-- remember-me-cookie="xiyue-remember-cookies" Cookies名稱-->
        <!-- user-service-ref="userDetailService" 處理類-->
        <!-- data-source-ref="dataSource" 持久化儲存資料源-->
        <security:remember-me
                remember-me-parameter="remember"
                key="xiyue-li"
                token-validity-seconds="2592000"
                remember-me-cookie="xiyue-rememberme-cookies"
                data-source-ref="dataSource"
                user-service-ref="userDetailService"/>
        <security:session-management invalid-session-url="/loginPage">
            <!--并發Session管理-->
            <!--max-sessions="1" 每個賬戶并發通路量-->
            <!--error-if-maximum-exceeded="false" Session剔除模式-->
            <!--expired-url="/loginPage" Session剔除後的錯誤顯示路徑-->
            <security:concurrency-control
                max-sessions="1"
                error-if-maximum-exceeded="false"
                expired-url="/loginPage"/>
        </security:session-management>

        <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
        <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
        <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
        <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
        <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
        <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
        <!--采用http-basic模式登入 -->
        <!--
            <security:http-basic/>
        -->
        <!--登入成功後的首頁,需要在使用者已經認證後才可以顯示-->
        <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
        <security:csrf disabled="true"/><!--關閉CSRF校驗-->
        <!--配置表單登入-->
        <security:form-login
            username-parameter="mid"
            password-parameter="pwd"
            authentication-success-forward-url="/welcomePage"
            login-page="/loginPage"
            login-processing-url="/xylogin"
            authentication-failure-url="/loginPage?error=true"
            />
        <security:logout
            logout-url="/xylogout"
            logout-success-url="/logoutPage"
            delete-cookies="JSESSIONID"
        />
    </security:http>
           

在進行持久化儲存時隻需要按照資料表結建構立資料表,随後配置上要使用的資料源,這樣在使用者進行免登入選擇的時候就可以自動将相應的資訊儲存在資料庫中,即便伺服器重新啟動也不會丢失使用者的免登入配置。

過濾器

SpringSecurity的核心操作是依據過濾器實作的認證與授權檢測,但是在DelegatingFilterProxy過濾器中為了友善進行認證與授權管理還提供了一套自定義的過濾鍊見下表進行配置。同時這些過濾鍊擁有嚴格的執行順序,才可以實作最終安全檢測。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

下面通過自定義過濾器實作一個登入驗證碼的檢測處理操作,由于驗證碼的檢測需要結合使用者登入處理,是以本次将直接繼承UsernamePasswordAuthenticationFilter父類實作過濾器定義。

提示:使用kaptcha實作驗證碼。

為了友善将直接使用Google開源的驗證碼元件kaptcha,該元件的核心配置如下。

1.修改pom.xml配置檔案,追加kaptcha元件依賴。

<kaptcha.version>0.0.9</kaptcha.version>
<dependency>
    <groupId>com.github.axet</groupId>
    <artifactId>kaptcha</artifactId>
    <version>${kaptcha.version}</version>
</dependency>
           

2.為友善配置建立一個KaptchaConfig配置類,進行DefaultKaptcha類對象的建立。

package com.xiyue.leaspring.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 22:19:28
 */
@Configuration
public class KaptchaConfig {
    @Bean
    public DefaultKaptcha captchaProducer(){
        DefaultKaptcha captchaProducer = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.setProperty("kaptcha.border","yes");
        properties.setProperty("kaptcha.border.color","105,179,90");
        properties.setProperty("kaptcha.textproducer.font.color","red");
        properties.setProperty("kaptcha.image.width","125");
        properties.setProperty("kaptcha.image.heigth","45");
        properties.setProperty("kaptcha.textproducer.font.size","35");
        properties.setProperty("kaptcha.session.key","captcha");
        properties.setProperty("kaptcha.textproducer.char.length","4");
        properties.setProperty("kaptcha.textproducer.font.names","宋體,楷體,微軟雅黑");
        Config config = new Config(properties);
        captchaProducer.setConfig(config);
        return  captchaProducer;
    }
}
           

本程式中,通過properties.setProperty(“kaptcha.session.key”, “captcha”);語句設定了驗證碼的名稱。

3.在GlobalAction類中追加一個驗證碼的顯示路徑。

@RequestMapping(value = "/RandomCode")
 public ModelAndView kaptcha(){
     HttpServletRequest request = ((ServletRequestAttributes)
             RequestContextHolder.getRequestAttributes()).getRequest();
     HttpServletResponse response = ((ServletRequestAttributes)
             RequestContextHolder.getRequestAttributes()).getResponse();
     HttpSession session = request.getSession();
     response.setHeader("Pragma","No-cache");//不緩存資料
     response.setHeader("Cache-Control","no-cache");//不緩存資料
     response.setDateHeader("Expires",0);//不失效
     response.setContentType("image/jpeg");//MIME類型
     String capText = captchaProducer.createText();//擷取驗證碼上的文字
     //将驗證碼上的文字儲存在Session中
     session.setAttribute(Constants.KAPTCHA_SESSION_KEY,capText);
     String code = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
     this.logger.info("驗證碼:"+code);
     BufferedImage image = this.captchaProducer.createImage(capText);//圖像
     try {
         OutputStream outputStream = response.getOutputStream();
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         ImageIO.write(image,"JPEG",bos);//圖像輸出
         byte[] buf = bos.toByteArray();
         response.setContentLength(buf.length);
         outputStream.write(buf);
         bos.close();
         outputStream.close();
     }catch (Exception e){

     }

     return null;
 }
           

上述程式中,session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);語句非常關鍵,作用是儲存Session中的屬性名稱,随後的代碼将通過此屬性名稱擷取生成的驗證碼資料以實作與輸入驗證碼的比對。

4.驗證碼設定在根路徑上顯示,是以還需要在spring.xml配置檔案中追加攔截路徑。

為了友善,本處隻列出了核心代碼,完整代碼可以參考對應項目中的程式檔案。

1.【lea-springsecurity項目】修改login.jsp頁面,追加驗證碼輸入框。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@page isELIgnored="false" %><%--啟動EL表達式解析--%>
<%
    request.setCharacterEncoding("UTF-8");
    String basePath = request.getScheme()+"://"+request.getServerName()+":"
            +request.getServerPort()+request.getContextPath()+"/";
    String login_url = basePath + "xylogin";
%>
<html>
<head>
    <title>登陸</title>
    <base href="<%=basePath%>">
</head>
<body>
<h3>使用者登陸請輸入使用者名與密碼</h3>

<form action="<%=login_url%>" method="post">
    使用者名:
    <input name="mid" type="text" placeholder="請輸入字元串使用者名"><br>
    密碼:
    <input name="pwd" type="password" placeholder="請輸入密碼"><br>
    驗證碼:<input type="text" maxlength="4" size="4" name="code">
                <img src="/RandomCode"></br>
            </input>
    <input type="checkbox" id="remember" name="remember" value="true" checked="true"/>下次免登入</br>
    <input type="submit" value="登入">
    <input type="reset" value="重置">
</form>
</body>
</html>
           

2.【lea-springsecurity項目】建立UsernamePasswordAuthenticationFilter子類,以實作驗證碼檢測。

package com.xiyue.leaspring;

import com.google.code.kaptcha.Constants;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/18 23:26:08
 */
public class ValidatorCodeUsernamePasswordAuthenticationFilter extends
        UsernamePasswordAuthenticationFilter {
    private String codeParameter = "code";
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String captcha = (String) request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);//擷取生成的驗證碼
        String code = request.getParameter(this.codeParameter);//擷取輸入驗證碼
        String username = super.obtainUsername(request).trim();//擷取使用者名
        String password = super.obtainPassword(request).trim();//取得密碼
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,password);//生成認證标記
        super.setDetails(request,authRequest);//設定認證詳情
        //當沒有輸入驗證,沒有生成驗證碼時或者驗證碼不比對時會提示錯誤
        if(captcha == null || "".equals(captcha) || code == null ||
            "".equals(code) || !captcha.equalsIgnoreCase(code)){
            request.getSession().setAttribute("SPRING_SECURITY_LAST_USERNAME",username);
            throw new AuthenticationServiceException("驗證碼不正确!");
        }
        return  super.getAuthenticationManager().authenticate(authRequest);
    }


    public void setCodeParameter(String codeParameter){
        this.codeParameter = codeParameter;
    }
}
           

3.【lea-springsecurity項目】此時要采用的是自定義的登入認證過濾器,是以最好的做法是單獨配置一個登入控制操作,替換原始的security:form-login配置項,具體配置項如下。

  • 定義一個新的登入處理終端,實作認證控制。
<bean id="authenticationEntryPoint"   class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg index="0" value="/loginPage"/>
</bean>
           
  • 定義登入成功處理Bean。
<bean id="loginLogAuthenticationSuccessHandler"
class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
   <property name="defaultTargetUrl" value="/welcomePage"/>
</bean>
           
  • 定義登入失敗處理Bean。
<bean id="simpleUrlAuthenticationFailureHandle"  class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="loginPage?error=true"/>
</bean>
           
  • 由于此時需要單獨配置登入操作,是以還需要為認證管理器設定一個ID,友善引用。
<security:authentication-manager><!--定義Security認證管理器-->
    <security:authentication-provider user-service-ref="userDetailService"/>
</security:authentication-manager>
           
  • 配置新定義的過濾器ValidatorCodeUsernamePasswordAuthenticationFilter,配置好相應的路徑處理類對象,與認證管理器引用。
<bean id="validatorCode"     class="com.xiyue.leaspring.filter.ValidatorCodeUsernamePasswordAuthenticationFilter">
    <property name="authenticationSuccessHandler" ref="loginLogAuthenticationSuccessHandler"/>
    <property name="authenticationFailureHandler" ref="simpleUrlAuthenticationFailureHandle"/>
    <property name="authenticationManager" ref="authenticationManager"/>
    <property name="filterProcessesUrl" value="/xylogin"/>
    <property name="usernameParameter" value="mid"/>
    <property name="passwordParameter" value="pwd"/>
</bean>
           
  • 修改security:http元素配置,配置新的認證終端處理,同時需要在登入前使用驗證碼過濾器。
<security:http auto-config="false" entry-point-ref="authenticationEntryPoint">
   <security:custom-filter ref="validatorCode" before="FORM_LOGIN_FILTER"/>
  <!--啟用RememberMe功能-->
  <!-- remember-me-parameter="remember" 登入表單參數-->
  <!-- key="xiyue-li" Cookies加密密鑰-->
  <!-- token-validity-seconds="2592000" 免登入失效(機關為s)-->
  <!-- remember-me-cookie="xiyue-remember-cookies" Cookies名稱-->
  <!-- user-service-ref="userDetailService" 處理類-->
  <!-- data-source-ref="dataSource" 持久化儲存資料源-->
  <security:remember-me
          remember-me-parameter="remember"
          key="xiyue-li"
          token-validity-seconds="2592000"
          remember-me-cookie="xiyue-rememberme-cookies"
          data-source-ref="dataSource"
          user-service-ref="userDetailService"/>
  <security:session-management invalid-session-url="/loginPage">
      <!--并發Session管理-->
      <!--max-sessions="1" 每個賬戶并發通路量-->
      <!--error-if-maximum-exceeded="false" Session剔除模式-->
      <!--expired-url="/loginPage" Session剔除後的錯誤顯示路徑-->
      <security:concurrency-control
          max-sessions="1"
          error-if-maximum-exceeded="false"
          expired-url="/loginPage"/>
  </security:session-management>

  <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
  <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
  <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
  <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
  <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
  <security:intercept-url pattern="/pages/message/**" access="hasRole('USER')"/>
  <!--采用http-basic模式登入 -->
  <!--
      <security:http-basic/>
  -->
  <!--登入成功後的首頁,需要在使用者已經認證後才可以顯示-->
  <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
  <security:intercept-url pattern="/**" access="permitAll()"/>
  <security:csrf disabled="true"/><!--關閉CSRF校驗-->
  <!--配置表單登入-->
  <!--<security:form-login
      username-parameter="mid"
      password-parameter="pwd"
      authentication-success-forward-url="/welcomePage"
      login-page="/loginPage"
      login-processing-url="/xylogin"
      authentication-failure-url="/loginPage?error=true"/>-->
  <security:logout
      logout-url="/xylogout"
      logout-success-url="/logoutPage"
      delete-cookies="JSESSIONID"
  />
</security:http>
           

至此可以實作驗證碼檢測,當驗證碼檢測不通過時将不會進行使用者資訊認證操作。

SpringSecurity注解

對于通路路徑的安全檢測除了攔截路徑的配置外,還可以通過注解的形式進行控制層或業務層中指定方法的通路驗證,在SpringSecurity中提供的注解有如下兩組。

  • @Secured:該注解為早期注解,可以直接進行角色驗證,但是不支援SpEL表達式。
  • @PreAuthorize / @PostAuthorize:該注解支援SpEL表達式,其中@PreAuthorize在方法執行前驗證,而@PostAuthorize在方法執行後驗證,一般使用較少。

    1.【lea-springsecurity項目】由于所有注解都要寫在Action或業務層中,是以修改spring-mvc.xml配置檔案,添加SpringSecurity注解的啟用配置,同時删除spring.xml配置檔案在security:http元素中定義的請求攔截路徑。

<!--啟用SpringSecurity注解功能-->
 <!--啟用@PreAuthorize/PostAuthorize功能-->
 <!--啟用@secured-->
 <security:global-method-security
         pre-post-annotations="enabled"
         secured-annotations="enabled"/>
           

2.【lea-springsecurity項目】修改GlobalAction類中的welcomePage路徑。該路徑要求認證過的使用者才可以通路。

@PreAuthorize("isAuthenticated()")
@RequestMapping("/welcomePage")//通路路徑
public String welcome(){//登入成功路徑
    Authentication authentication = SecurityContextHolder
            .getContext().getAuthentication();//擷取認證對象
    UserDetails userDetails = (UserDetails) authentication.getPrincipal();//使用者詳情
    String username = userDetails.getUsername();//獲得使用者名
    this.logger.info("使用者名:"+username);
    //通過userDetail對象擷取目前使用者的所有授權資訊
    Collection<? extends GrantedAuthority> authorities =userDetails.getAuthorities();
    this.logger.info("授權資訊:"+authorities);
    return "welcome";//設定跳轉路徑
}
           

由于需要使用SpEL表達式,是以在進行認證檢測時直接使用了@PreAuthorize注解。這樣,當進行通路時如果目前使用者沒有登入過,則會跳轉到登入表單頁面;如果使用者登入過,就可以直接通路。

3.【lea-springsecurity項目】修改EchoAction程式類,追加角色判斷。

package com.xiyue.leaspring.action;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller//定義控制器
@RequestMapping("/pages/message")//定義通路父路徑,與方法中的路徑組合為完整的路徑
public class EchoAction {//自定義Action程式

    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping("/show")//通路的路徑為url
    public ModelAndView echo(String msg){
        return new ModelAndView("/message/message_show").addObject("echoMessage","[ECHO]msg="+msg);
    }

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/input")//通路的路徑
    public String input(){
        return "/message/message_input";//jump route
    }
}
           

本程式利用注解為具體的Action方法進行了授權檢測,當不具有指定角色使用者通路時會自動跳轉到錯誤頁顯示。

投票器

對于安全通路,除了使用攔截路徑形式進行通路控制外,還可以利用決策管理器根據實際業務實作安全通路。利用決策管理器中的投票機制,可以決定是否需要進行授權控制。

在之前的通路控制中使用過兩個投票器:角色投票器(RoleVoter)與認證投票器(AuthenticatedVoter)。所有的投票器都會被通路決策管理器所管理,這些類之間的結構如下圖所示。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

通過上圖可以發現,決策管理器的父接口為AccessDecisionManager,同時在此接口中定義了投票的3種狀态:贊成(ACCESS_GRANTED)、棄權(ACCESS_ABSTAIN)、反對(ACCESS_DENIED)。在SpringSecurity中定義了3個常用的投票管理器,具體作用如下。

  • org.springframework.security.access.vote.AffirmativeBased:一票通過,隻要有一個支援就允許通路。
  • org.springframework.security.access.vote.ConsensusBased:半數以上支援票數就可以通路。
  • org.springframework.security.access.vote.UnanimousBased:全票通過後才可以通路。

如果要實作一個自定義的投票器并且可以被投票管理器所管理,那麼該投票類需要實作AccessDecisionVoter父接口,在AccessDecisionVoter接口中定義的方法如下表所示。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

AccessDecisionVoter

下面采用自定義投票器的方式實作本地通路控制。本例中,通過localhost(127.0.0.1)通路的使用者不需要登入就可以直接進行操作。

1.【lea-springsecurity項目】建立一個IP位址的投票器。

package com.xiyue.leaspring.config;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import java.util.Collection;
import java.util.Iterator;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/19 22:28:38
 */
public class IPAdressVoter implements AccessDecisionVoter<Object> {

    private static final String LOCAL_FLAG = "LOCAL_IP";//需要判斷的通路标記

    @Override
    public boolean supports(ConfigAttribute attribute) {//如果有指定配置屬性,則執行投票器
        return attribute!=null &&
                attribute.toString().contains(LOCAL_FLAG);
    }

    @Override
    public boolean supports(Class<?> clazz) {//對所有通路類均支援投票
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if(!(authentication.getDetails() instanceof WebAuthenticationDetails)){//如果不是來自Web通路
            return AccessDecisionVoter.ACCESS_DENIED;//拒絕該使用者通路
        }
        //通過認證資訊擷取使用者的詳情内容,該内容類型WebAuthenticationDetails
        WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
        String ip = details.getRemoteAddress();//取得目前操作的IP位址
        Iterator<ConfigAttribute> iterator = attributes.iterator();//擷取每一個配置屬性
        while (iterator.hasNext()){//循環每一個配置屬性
            ConfigAttribute configAttribute = iterator.next();//擷取屬性
            if(configAttribute.toString().contains(LOCAL_FLAG)){//如果在本地執行
                if("0:0:0:0:0:0:0:1".equals(ip) || "127.0.0.1".equals(ip)){//本地通路
                    return AccessDecisionVoter.ACCESS_GRANTED;//通路通過
                }
            }
        }
        return AccessDecisionVoter.ACCESS_ABSTAIN;//棄權不參與投票
    }
}
           

本程式中設定了一個新的通路控制标記LOCAL_IP,如果攔截路徑上使用了此通路類型,同時又屬于本機直接通路的情況,将不會進行認證處理,可以直接使用。

2.【lea-springsecurity項目】由于需要引入新的投票器,是以此時需要修改spring.xml配置檔案,定義新的投票管理器,且配置相應的投票器。

<bean id="accessDecisionManager"
        class="org.springframework.security.access.vote.AffirmativeBased"><!--管理器-->
    <constructor-arg name="decisionVoters"><!--投票器-->
         <list>
             <!--定義角色投票器,進行角色認證-->
             <bean class="org.springframework.security.access.vote.RoleVoter"/>
             <!--定義認證投票器,用于判斷使用者是否已經認證-->
             <bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
             <!--自定義投票器,如果是本機IP則不進行過濾-->
             <bean class="com.xiyue.leaspring.config.IPAdressVoter"/>
             <!--開啟表達式支援,這樣就可以在進行攔截時使用SpEL-->
             <bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
         </list>
     </constructor-arg>
 </bean>
           

在本次定義的決策管理器中一共設定了3個投票器,由于使用的是AffirmativeBased管理器,是以隻要有一個投票器投出贊成票,就可以進行通路。

3.【lea-springsecurity項目】修改spring.xml配置檔案中的security:http配置項,引入自定義的通路決策管理器,同時在需要驗證的路徑上定義LOCAL_IP标記。

<security:http auto-config="false"
                   entry-point-ref="authenticationEntryPoint"
                    access-decision-manager-ref="accessDecisionManager">
    <security:custom-filter ref="validatorCode" before="FORM_LOGIN_FILTER"/>
     <!--啟用RememberMe功能-->
     <!-- remember-me-parameter="remember" 登入表單參數-->
     <!-- key="xiyue-li" Cookies加密密鑰-->
     <!-- token-validity-seconds="2592000" 免登入失效(機關為s)-->
     <!-- remember-me-cookie="xiyue-remember-cookies" Cookies名稱-->
     <!-- user-service-ref="userDetailService" 處理類-->
     <!-- data-source-ref="dataSource" 持久化儲存資料源-->
     <security:remember-me
             remember-me-parameter="remember"
             key="xiyue-li"
             token-validity-seconds="2592000"
             remember-me-cookie="xiyue-rememberme-cookies"
             data-source-ref="dataSource"
             user-service-ref="userDetailService"/>
     <security:session-management invalid-session-url="/loginPage">
         <!--并發Session管理-->
         <!--max-sessions="1" 每個賬戶并發通路量-->
         <!--error-if-maximum-exceeded="false" Session剔除模式-->
         <!--expired-url="/loginPage" Session剔除後的錯誤顯示路徑-->
         <security:concurrency-control
                 max-sessions="1"
                 error-if-maximum-exceeded="false"
                 expired-url="/loginPage"/>
     </security:session-management>

     <!--定義授權檢測失敗時的顯示頁面,一旦拒絕,将自動自動進行跳轉-->
     <security:access-denied-handler error-page="/WEB-INF/pages/error_page_403.jsp"/>
     <!--定義要攔截的路徑,可以時具體路徑,也可以使用路徑比對符設定要攔截的父路徑-->
     <!--表達式“hasRole(角色名稱)”表示擁有此角色的用可以才可以通路-->
     <security:intercept-url pattern="/pages/info/**" access="hasRole('ADMIN')"/>
     <security:intercept-url pattern="/pages/message/**" access="hasRole('USER') or hasRole('LOCAL_IP')"/>
     <!--采用http-basic模式登入 -->
     <!--
         <security:http-basic/>
     -->
     <!--登入成功後的首頁,需要在使用者已經認證後才可以顯示-->
     <security:intercept-url pattern="/welcomePage" access="isAuthenticated()"/>
     <security:intercept-url pattern="/**" access="permitAll()"/>
     <security:csrf disabled="true"/><!--關閉CSRF校驗-->
     <!--配置表單登入-->
     <!--<security:form-login
         username-parameter="mid"
         password-parameter="pwd"
         authentication-success-forward-url="/welcomePage"
         login-page="/loginPage"
         login-processing-url="/xylogin"
         authentication-failure-url="/loginPage?error=true"/>-->
     <security:logout
             logout-url="/xylogout"
             logout-success-url="/logoutPage"
             delete-cookies="JSESSIONID"/>
 </security:http>
           

配置成功後,當通過localhost或者127.0.0.1通路/pages/message/**路徑中的資訊時,将不再受到角色限制。如果是遠端通路,則依然需要按照傳統方式登入認證後才可以通路。

RoleHierarchy

為了進一步完善授權通路的級别層次,SpringSecurity提供了角色繼承概念,即使用者可定義繼承的層次關系,這樣,擁有更進階别角色的使用者可以直接通路低級别角色的資訊。

提示:關于角色繼承的描述。

假設在spring.xml中有如下兩個攔截路徑。

<security:intercept-url pattern="/pages/message/input" access="hasRole('USER')"/>
<security:intercept-url pattern="pages/message/show" access="hasRole('ADMIN')"/>
           

此時兩個通路路徑分别配置了兩種角色,按照之前的定義,假設ROLE_ADMIN是最高管理者權限,在這樣的配置下如果一個使用者隻擁有ROLE_ADMIN角色,依然無法通路ROLE_USER對應的資訊。配置了角色層次關系後,即便擁有ROLE_ADMIN的使用者沒有ROLE_USER角色,也可以通路ROLE_USER角色對應的資訊。

為了實作這一操作,需要先删除member_role表中admin使用者對應的ROLE_USER角色資訊。

在SpringSecurity中,對于角色繼承提供了org.springframework.security.access.hierarchicalroles.RoleHierarchy接口,此接口定義如下。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

此接口定義了擷取全部可用控制權限的方法,SpringSecurity架構還提供了一個基礎的實作子類RoleHierarchyImpl,下面将利用此類結合配置檔案,實作角色繼承。配置檔案中,角色繼承的配置格式為:角色1 >角色2 > … >角色n。

1.【lea-springsecurity項目】修改spring.xml配置檔案,配置角色繼承關系。

<bean id="roleHierarchy"   class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
    <property name="hierarchy"><!--定義繼承層次關系-->
        <value>ROLE_ADMIN>ROLE_USER</value><!--定義角色層次-->
    </property>
</bean>
           

2.【lea-springsecurity項目】由于所有的角色檢測操作都通過表達式進行配置,是以需要在WebExpressionVoter投票器中進行角色繼承配置,修改spring.xml配置檔案。

<bean id="accessDecisionManager"
    class="org.springframework.security.access.vote.AffirmativeBased"><!--管理器-->
    <constructor-arg name="decisionVoters"><!--投票器-->
        <list>
            <!--定義角色投票器,進行角色認證-->
            <bean class="org.springframework.security.access.vote.RoleVoter"/>
            <!--定義認證投票器,用于判斷使用者是否已經認證-->
            <bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
            <!--自定義投票器,如果是本機IP則不進行過濾-->
            <bean class="com.xiyue.leaspring.config.IPAdressVoter"/>
            <!--定義角色繼承投票器,此投票器可以在注解中使用-->
            <bean class="org.springframework.security.access.vote.RoleHierarchyVoter">
                <constructor-arg ref="roleHierarchy"/><!--引用角色繼承配置-->
            </bean>
            <!--由于需要通過SpEL定義通路繼承關系,是以要在Web表達式投票器中配置角色繼承定義-->
            <!--開啟表達式支援,這樣就可以在進行攔截時使用SpEL-->
            <bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
                <property name="expressionHandler"><!--定義表達式處理器-->
                    <bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">
                        <!--引用角色繼承配置-->
                        <property name="roleHierarchy" ref="roleHierarchy"/>
                    </bean>
                </property>
            </bean>
        </list>
    </constructor-arg>
</bean>
           

配置完成後,當使用者擁有了ROLE_ADMIN角色就可以通路ROLE_USER對應的資源。

基于Bean配置

SpringSecurity的所有核心配置,除了可以利用配置檔案實作外,也可以采用Bean配置完成。SpringSecurity中提供了WebSecurityConfigurer接口,開發者隻需要實作此接口或繼承WebSecurityConfigurerAdapter抽象類,即可實作配置。SpringSecurity的基本定義結構如下圖所示。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

自定義SpringSecurity配置類時,可以根據需要覆寫WebSecurityConfigurerAdapter抽象類中的configure方法,如下表所示。

SpringSecuritySpringSecurity概述SpringSecurity程式設計起步CSRF通路控制擴充登入和登出功能擷取認證與授權資訊使用者認證成功後基于資料庫實作使用者登入Session管理RememberMe過濾器SpringSecurity注解投票器基于Bean配置

基礎配置

通過Bean實作一個固定認證資訊的登入、登出、認證與授權控制的配置操作。需要注意的是,如果通過配置類定義SpringSecurity配置,需要滿足如下兩項。

  • 配置Bean需要設定在掃描路徑中,并且需要使用@Configuration注解聲明。
  • 由于該Bean主要負責SpringSecurity配置,是以需要在類定義中使用@EnableWeb Security注解。

    範例:定義WebSecurityConfiguration配置類,進行SpringSecurity基礎配置。

package com.xiyue.leaspring.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/19 23:48:24
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //進行使用者角色配置時不需要追加ROLE_字首,系統會自動添加
        auth.inMemoryAuthentication()//固定認證資訊
                .withUser("admin")//使用者名
                .password("{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16")//密碼
                .roles("USER","ADMIN");//角色
        auth.inMemoryAuthentication()//固定認證資訊
                .withUser("xiyue")//使用者名
                .password("{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em")//密碼
                .roles("USER");//角色
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//禁用CSRF驗證
        //配置攔截路徑的比對位址與限定授權通路
        http.authorizeRequests()//配置認證請求
            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授權通路
            .antMatchers("/welcomePage").authenticated()//認證通路
            .antMatchers("/**").permitAll();//任意通路
        //配置HTTP登入,登出與錯誤路徑
        http.formLogin()//登入配置
            .usernameParameter("mid")//使用者名參數設定
            .passwordParameter("pwd")//使用者密碼參數設定
            .successForwardUrl("/welcomePage")//登入成功路徑
            .loginPage("/loginPage")//登入表單頁面
            .loginProcessingUrl("/xylogin")//登入路徑
            .failureForwardUrl("/loginPage?error=true")//登入失敗路徑
            .and()//配置連接配接
            .logout()//登出配置
            .logoutUrl("/xylogout")//登出路徑
            .logoutSuccessUrl("/logoutPage")//登出成功路徑
            .deleteCookies("JSESSIONID")//删除Cookie
            .and()//配置連接配接
            .exceptionHandling()//認證錯誤配置
            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授權錯誤
    }
}
           

本程式實作了一個基本的SpringSecurity使用配置,主要使用了兩個configure方法。

  • configure(AuthenticationManagerBuilder auth):配置認證使用者。這裡使用auth.inMemory Authentication方法聲明了兩個使用者,并為其配置設定了相應角色。
  • configure(HttpSecurity http):配置攔截路徑、登入、登出與授權錯誤。

深入配置

SpringSecurity中除了基礎的登入認證外,還有UserDetails、Session管理、過濾器、通路注解等相關配置。這些配置也可以直接通過Bean實作管理。

範例:自定義認證處理操作。

<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
    <context:component-scan base-package="com.xiyue.leaspring">
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.action"/>
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.dao"/>
    </context:component-scan>
    <context:property-placeholder location="classpath:database.properties"/>
    <!--<bean id="dataSource"
          class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${database.driverClass}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.user}"/>
        <property name="password" value="${database.password}"/>
    </bean>-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init">
        <property name="driverClassName" value="${database.druid.driverClassName}"/><!--驅動-->
        <property name="url" value="${database.druid.url}"/><!--位址-->
        <property name="username" value="${database.druid.username}"/><!--使用者-->
        <property name="password" value="${database.druid.password}"/><!--密碼-->
        <property name="maxActive" value="${database.druid.maxActive}"/><!--最大連接配接數-->
        <property name="minIdle" value="${database.druid.minIdle}"/><!--最小連接配接池-->
        <property name="initialSize" value="${database.druid.initialSize}"/><!--初始化連接配接大小-->
        <property name="maxWait" value="${database.druid.maxWait}"/><!--最大等待時間-->
        <property name="timeBetweenEvictionRunsMillis" value="${database.druid.timeBetweenEvictionRunsMillis}"/><!--檢測空閑連接配接間隔-->
        <property name="minEvictableIdleTimeMillis" value="${database.druid.minEvictableIdleTimeMillsis}"/><!--連接配接最小生存時間-->
        <property name="validationQuery" value="${database.druid.validationQuery}"/><!--驗證-->
        <property name="testWhileIdle" value="${database.druid.testWhileIdle}"/><!--申請檢測-->
        <property name="testOnBorrow" value="${database.druid.testIOnBorrow}"/><!--有效檢測-->
        <property name="testOnReturn" value="${database.druid.testIOnReturn}"/><!--歸還檢測-->
        <property name="poolPreparedStatements" value="${database.druid.poolPreparedStatements}"/><!--是否緩存preparedStatement,也就是PSCache。PSCache能提升支援遊标的資料庫性能,如Oracle、Mysql下建議關閉-->
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${database.druid.maxpoolPreparedStatementPerConnectionSize}"/><!--啟用PSCache,必須配置大于0,當大于0時-->
        <property name="filters" value="${database.druid.filters}"/><!--驅動-->
    </bean>
    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/><!-- 資料源 -->
        <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/><!-- JPA核心配置檔案 -->
        <property name="persistenceUnitName" value="LEA_SPRING_JPA"/><!-- 持久化單元名稱 -->
        <property name="packagesToScan" value="com.xiyue.leaspring.dao"/><!-- PO類掃描包 -->
        <property name="persistenceProvider"><!-- 持久化提供類,本次為hibernate -->
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
        </property>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
        </property>
    </bean>
    <!-- 定義SpringDataJPA的資料層接口所在包,該包中的接口一定義是Repository子接口 -->
    <jpa:repositories base-package="com.xiyue.leaspring.dao"/>
    <!-- 定義事務管理的配置,必須配置PlatformTransactionManager接口子類 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <!--開啟注解事務-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <security:authentication-manager><!--定義認證管理器-->
        <security:authentication-provider><!--配置認證管理配置類-->
            <security:jdbc-user-service
                    data-source-ref="dataSource"
                    users-by-username-query="select mid as username,password,enabled from member where mid=?"
                    authorities-by-username-query="select mid as username,rid as authorities from member_role where mid=?"/>
        </security:authentication-provider>
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>

    <security:authentication-manager id="authenticationManager"><!--定義Security認證管理器-->
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>
    <bean id="accessDecisionManager"
        class="org.springframework.security.access.vote.AffirmativeBased"><!--管理器-->
        <constructor-arg name="decisionVoters"><!--投票器-->
            <list>
                <!--定義角色投票器,進行角色認證-->
                <bean class="org.springframework.security.access.vote.RoleVoter"/>
                <!--定義認證投票器,用于判斷使用者是否已經認證-->
                <bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
                <!--自定義投票器,如果是本機IP則不進行過濾-->
                <bean class="com.xiyue.leaspring.config.IPAdressVoter"/>
                <!--定義角色繼承投票器,此投票器可以在注解中使用-->
                <bean class="org.springframework.security.access.vote.RoleHierarchyVoter">
                    <constructor-arg ref="roleHierarchy"/><!--引用角色繼承配置-->
                </bean>
                <!--由于需要通過SpEL定義通路繼承關系,是以要在Web表達式投票器中配置角色繼承定義-->
                <!--開啟表達式支援,這樣就可以在進行攔截時使用SpEL-->
                <bean class="org.springframework.security.web.access.expression.WebExpressionVoter">
                    <property name="expressionHandler"><!--定義表達式處理器-->
                        <bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler">
                            <!--引用角色繼承配置-->
                            <property name="roleHierarchy" ref="roleHierarchy"/>
                        </bean>
                    </property>
                </bean>
            </list>
        </constructor-arg>
    </bean>
    <bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
        <property name="hierarchy"><!--定義繼承層次關系-->
            <value>ROLE_ADMIN>ROLE_USER</value><!--定義角色層次-->
        </property>
    </bean>
</beans>
           
package com.xiyue.leaspring.config;

import com.xiyue.leaspring.filter.ValidatorCodeUsernamePasswordAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;

import javax.sql.DataSource;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/19 23:48:24
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)//啟用注解配置
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;//資料源
    @Autowired
    private UserDetailsService userDetailsService;//使用者服務
    @Autowired
    private SavedRequestAwareAuthenticationSuccessHandler successHandler;//成功頁
    @Autowired
    private SimpleUrlAuthenticationFailureHandler failureHandler;//失敗頁
    @Autowired
    private SessionInformationExpiredStrategy sessionExpiredStrategy;//Session失效政策
    @Autowired
    private UsernamePasswordAuthenticationFilter authenticationFilter;//認證過濾器
    @Autowired
    private JdbcTokenRepositoryImpl tokenRepository;//token存儲
    @Autowired
    private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;



    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //進行使用者角色配置時不需要追加ROLE_字首,系統會自動添加
//        auth.inMemoryAuthentication()//固定認證資訊
//                .withUser("admin")//使用者名
//                .password("{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16")//密碼
//                .roles("USER","ADMIN");//角色
//        auth.inMemoryAuthentication()//固定認證資訊
//                .withUser("xiyue")//使用者名
//                .password("{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em")//密碼
//                .roles("USER");//角色
        auth.userDetailsService(this.userDetailsService);//基于資料庫認證
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();//禁用CSRF驗證
//        //配置攔截路徑的比對位址與限定授權通路
//        http.authorizeRequests()//配置認證請求
//            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授權通路
//            .antMatchers("/welcomePage").authenticated()//認證通路
//            .antMatchers("/**").permitAll();//任意通路
//        //配置HTTP登入,登出與錯誤路徑
//        http.formLogin()//登入配置
//            .usernameParameter("mid")//使用者名參數設定
//            .passwordParameter("pwd")//使用者密碼參數設定
//            .successForwardUrl("/welcomePage")//登入成功路徑
//            .loginPage("/loginPage")//登入表單頁面
//            .loginProcessingUrl("/xylogin")//登入路徑
//            .failureForwardUrl("/loginPage?error=true")//登入失敗路徑
//            .and()//配置連接配接
//            .logout()//登出配置
//            .logoutUrl("/xylogout")//登出路徑
//            .logoutSuccessUrl("/logoutPage")//登出成功路徑
//            .deleteCookies("JSESSIONID")//删除Cookie
//            .and()//配置連接配接
//            .exceptionHandling()//認證錯誤配置
//            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授權錯誤
        http.csrf().disable();//禁用CSRF驗證
        http.httpBasic().authenticationEntryPoint(this.authenticationEntryPoint);
        //進行攔截路徑的比對位址配置與授權通路限定
        http.authorizeRequests()//配置認證請求
            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授權通路
            .antMatchers("/welcomePage").authenticated();//認證通路
        //進行http登出與錯誤路徑配置,登入操作将由過濾器負責完成
        http.logout()//登出配置
            .logoutUrl("/xylogout")//登出路徑
            .logoutSuccessUrl("/logoutPage")//登出成功路徑
            .deleteCookies("JSESSIONID")//删除Cookie
            .and()//配置連接配接
            .exceptionHandling()//認證錯誤配置
            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授權錯誤頁
        http.rememberMe()//開啟RememberMe
            .rememberMeParameter("remember")//表單參數
            .key("xy-xiyue")//加密key
            .tokenValiditySeconds(2592000)//失效時間
            .rememberMeCookieName("xiyue-rememberme-cookie")//Cookie名稱
            .tokenRepository(this.tokenRepository);//持久化
        http.sessionManagement()//Session管理
            .invalidSessionUrl("/loginPage")//失效路徑
            .maximumSessions(1)//并發Session
            .expiredSessionStrategy(this.sessionExpiredStrategy);//失效政策
        http.addFilterBefore(this.authenticationFilter,UsernamePasswordAuthenticationFilter.class);//追加過濾器
    }
    @Override
    public void configure(WebSecurity web) throws Exception{
        web.ignoring().antMatchers("/index.jsp");//忽略的驗證路徑
    }

    @Bean
    public UsernamePasswordAuthenticationFilter getAuthenticationFilter() throws Exception{
        ValidatorCodeUsernamePasswordAuthenticationFilter filter = new
                ValidatorCodeUsernamePasswordAuthenticationFilter();
        filter.setAuthenticationManager(super.authenticationManager());//認證管理器
        filter.setAuthenticationSuccessHandler(this.successHandler);//登入成功頁面
        filter.setAuthenticationFailureHandler(this.failureHandler);//登入失敗頁面
        filter.setFilterProcessesUrl("/xylogin");//登入路徑
        filter.setUsernameParameter("mid");//參數名稱
        filter.setPasswordParameter("pwd");//參數名稱
        return filter;
    }

    @Bean
    public LoginUrlAuthenticationEntryPoint getAuthenticationEntryPoint() {
        return new LoginUrlAuthenticationEntryPoint("/loginPage");
    }

    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler getSuccessHandler() {//認證成功
        SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setDefaultTargetUrl("/welcomePage");//成功頁面
        return successHandler;
    }

    @Bean
    public SimpleUrlAuthenticationFailureHandler getFailureHandler() {//認證失敗處理
        SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler();
        handler.setDefaultFailureUrl("/loginPage?error=true");//失敗頁面
        return handler;
    }

    @Bean
    public JdbcTokenRepositoryImpl getTokenRepository() {//持久化Cookie
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(this.dataSource);
        return tokenRepository;
    }

    @Bean
    public SessionInformationExpiredStrategy getSessionExpiredStrategy() {
        //Session失效政策,同時配置失效後的跳轉路徑
        return new SimpleRedirectSessionInformationExpiredStrategy("/logoffPage");
    }
}
           

本程式針對之前的配置,追加了RememberMe(包括資料庫持久化)、驗證碼檢測和并發Session通路控制。考慮到Spring開發的标準型,将所有可能使用到的對象以Bean的形式進行配置,随後根據需要進行注入。

配置投票管理器

SpringSecurity配置類提供了投票管理器,按照下面的順序實作投票器的配置即可。本程式依然使用AffirmativeBased投票管理器,該投票管理器需要通過構造方法配置所有的投票器對象。

**範例:**配置投票管理器。

package com.xiyue.leaspring.config;

import com.xiyue.leaspring.filter.ValidatorCodeUsernamePasswordAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.RoleHierarchyVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;

/**
 * 描述
 *
 * @author xiyue
 * @version 1.0
 * @date 2021/04/19 23:48:24
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)//啟用注解配置
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;//資料源
    @Autowired
    private UserDetailsService userDetailsService;//使用者服務
    @Autowired
    private SavedRequestAwareAuthenticationSuccessHandler successHandler;//成功頁
    @Autowired
    private SimpleUrlAuthenticationFailureHandler failureHandler;//失敗頁
    @Autowired
    private SessionInformationExpiredStrategy sessionExpiredStrategy;//Session失效政策
    @Autowired
    private UsernamePasswordAuthenticationFilter authenticationFilter;//認證過濾器
    @Autowired
    private JdbcTokenRepositoryImpl tokenRepository;//token存儲
    @Autowired
    private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private RoleHierarchy roleHierarchy;//角色繼承

    @Autowired
    private SecurityExpressionHandler<FilterInvocation> expressionHandler;//表達式處理器

    @Autowired
    private AccessDecisionManager accessDecisionManager;//投票管理器

    @Bean
    public AccessDecisionManager getAccessDecisionManager() {
        //将所有用到的投票器設定到List集合中
        List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<>();
        decisionVoters.add(new RoleVoter());//角色投票器
        decisionVoters.add(new AuthenticatedVoter());//認證投票器
        decisionVoters.add(new RoleHierarchyVoter(this.roleHierarchy));//角色繼承投票器
        WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
        webExpressionVoter.setExpressionHandler(this.expressionHandler);
        decisionVoters.add(webExpressionVoter);//表達式解析
        AffirmativeBased accessDecisionManager = new AffirmativeBased(decisionVoters);//定義投票管理器
        return accessDecisionManager;
    }

    @Bean
    public SecurityExpressionHandler<FilterInvocation> getExpressionHandler() {//配置表達式
        DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
        expressionHandler.setRoleHierarchy(this.roleHierarchy);//設定角色繼承
        return expressionHandler;
    }

    @Bean
    public RoleHierarchy getRoleHierarchy() {//角色繼承設定
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();//角色繼承
        roleHierarchy.setHierarchy("ROLE_ADMIN>ROLE_USER");//繼承關系
        return roleHierarchy;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //進行使用者角色配置時不需要追加ROLE_字首,系統會自動添加
//        auth.inMemoryAuthentication()//固定認證資訊
//                .withUser("admin")//使用者名
//                .password("{bcrypt}$2a$10$L1/V.lk9yj2wVSfbGpxbQO8WFrqot5Xck9Yd/I5Tzy/vgCYKjEv16")//密碼
//                .roles("USER","ADMIN");//角色
//        auth.inMemoryAuthentication()//固定認證資訊
//                .withUser("xiyue")//使用者名
//                .password("{bcrypt}$2a$10$TC/ROdGr5fjmmm/OmYu13ui6LmTJWbdSTid/9yWYp9SaolZa095Em")//密碼
//                .roles("USER");//角色
        auth.userDetailsService(this.userDetailsService);//基于資料庫認證
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();//禁用CSRF驗證
//        //配置攔截路徑的比對位址與限定授權通路
//        http.authorizeRequests()//配置認證請求
//            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授權通路
//            .antMatchers("/welcomePage").authenticated()//認證通路
//            .antMatchers("/**").permitAll();//任意通路
//        //配置HTTP登入,登出與錯誤路徑
//        http.formLogin()//登入配置
//            .usernameParameter("mid")//使用者名參數設定
//            .passwordParameter("pwd")//使用者密碼參數設定
//            .successForwardUrl("/welcomePage")//登入成功路徑
//            .loginPage("/loginPage")//登入表單頁面
//            .loginProcessingUrl("/xylogin")//登入路徑
//            .failureForwardUrl("/loginPage?error=true")//登入失敗路徑
//            .and()//配置連接配接
//            .logout()//登出配置
//            .logoutUrl("/xylogout")//登出路徑
//            .logoutSuccessUrl("/logoutPage")//登出成功路徑
//            .deleteCookies("JSESSIONID")//删除Cookie
//            .and()//配置連接配接
//            .exceptionHandling()//認證錯誤配置
//            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授權錯誤
        http.csrf().disable();//禁用CSRF驗證
        http.httpBasic().authenticationEntryPoint(this.authenticationEntryPoint);
        //進行攔截路徑的比對位址配置與授權通路限定
        http.authorizeRequests()//配置認證請求
            .antMatchers("/pages/message/**").access("hasRole('ADMIN')")//授權通路
            .antMatchers("/welcomePage").authenticated();//認證通路
        //進行http登出與錯誤路徑配置,登入操作将由過濾器負責完成
        http.logout()//登出配置
            .logoutUrl("/xylogout")//登出路徑
            .logoutSuccessUrl("/logoutPage")//登出成功路徑
            .deleteCookies("JSESSIONID")//删除Cookie
            .and()//配置連接配接
            .exceptionHandling()//認證錯誤配置
            .accessDeniedPage("/WEB-INF/pages/error_page_403.jsp");//授權錯誤頁
        http.rememberMe()//開啟RememberMe
            .rememberMeParameter("remember")//表單參數
            .key("xy-xiyue")//加密key
            .tokenValiditySeconds(2592000)//失效時間
            .rememberMeCookieName("xiyue-rememberme-cookie")//Cookie名稱
            .tokenRepository(this.tokenRepository);//持久化
        http.sessionManagement()//Session管理
            .invalidSessionUrl("/loginPage")//失效路徑
            .maximumSessions(1)//并發Session
            .expiredSessionStrategy(this.sessionExpiredStrategy);//失效政策
        http.addFilterBefore(this.authenticationFilter,UsernamePasswordAuthenticationFilter.class);//追加過濾器
    }
    @Override
    public void configure(WebSecurity web) throws Exception{
        web.ignoring().antMatchers("/index.jsp");//忽略的驗證路徑
    }

    @Bean
    public UsernamePasswordAuthenticationFilter getAuthenticationFilter() throws Exception{
        ValidatorCodeUsernamePasswordAuthenticationFilter filter = new
                ValidatorCodeUsernamePasswordAuthenticationFilter();
        filter.setAuthenticationManager(super.authenticationManager());//認證管理器
        filter.setAuthenticationSuccessHandler(this.successHandler);//登入成功頁面
        filter.setAuthenticationFailureHandler(this.failureHandler);//登入失敗頁面
        filter.setFilterProcessesUrl("/xylogin");//登入路徑
        filter.setUsernameParameter("mid");//參數名稱
        filter.setPasswordParameter("pwd");//參數名稱
        return filter;
    }

    @Bean
    public LoginUrlAuthenticationEntryPoint getAuthenticationEntryPoint() {
        return new LoginUrlAuthenticationEntryPoint("/loginPage");
    }

    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler getSuccessHandler() {//認證成功
        SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setDefaultTargetUrl("/welcomePage");//成功頁面
        return successHandler;
    }

    @Bean
    public SimpleUrlAuthenticationFailureHandler getFailureHandler() {//認證失敗處理
        SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler();
        handler.setDefaultFailureUrl("/loginPage?error=true");//失敗頁面
        return handler;
    }

    @Bean
    public JdbcTokenRepositoryImpl getTokenRepository() {//持久化Cookie
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(this.dataSource);
        return tokenRepository;
    }

    @Bean
    public SessionInformationExpiredStrategy getSessionExpiredStrategy() {
        //Session失效政策,同時配置失效後的跳轉路徑
        return new SimpleRedirectSessionInformationExpiredStrategy("/logoffPage");
    }
}
           
<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
    <context:component-scan base-package="com.xiyue.leaspring">
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.action"/>
        <context:exclude-filter type="annotation" expression="com.xiyue.leaspring.dao"/>
    </context:component-scan>
    <context:property-placeholder location="classpath:database.properties"/>
    <!--<bean id="dataSource"
          class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${database.driverClass}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.user}"/>
        <property name="password" value="${database.password}"/>
    </bean>-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init">
        <property name="driverClassName" value="${database.druid.driverClassName}"/><!--驅動-->
        <property name="url" value="${database.druid.url}"/><!--位址-->
        <property name="username" value="${database.druid.username}"/><!--使用者-->
        <property name="password" value="${database.druid.password}"/><!--密碼-->
        <property name="maxActive" value="${database.druid.maxActive}"/><!--最大連接配接數-->
        <property name="minIdle" value="${database.druid.minIdle}"/><!--最小連接配接池-->
        <property name="initialSize" value="${database.druid.initialSize}"/><!--初始化連接配接大小-->
        <property name="maxWait" value="${database.druid.maxWait}"/><!--最大等待時間-->
        <property name="timeBetweenEvictionRunsMillis" value="${database.druid.timeBetweenEvictionRunsMillis}"/><!--檢測空閑連接配接間隔-->
        <property name="minEvictableIdleTimeMillis" value="${database.druid.minEvictableIdleTimeMillsis}"/><!--連接配接最小生存時間-->
        <property name="validationQuery" value="${database.druid.validationQuery}"/><!--驗證-->
        <property name="testWhileIdle" value="${database.druid.testWhileIdle}"/><!--申請檢測-->
        <property name="testOnBorrow" value="${database.druid.testIOnBorrow}"/><!--有效檢測-->
        <property name="testOnReturn" value="${database.druid.testIOnReturn}"/><!--歸還檢測-->
        <property name="poolPreparedStatements" value="${database.druid.poolPreparedStatements}"/><!--是否緩存preparedStatement,也就是PSCache。PSCache能提升支援遊标的資料庫性能,如Oracle、Mysql下建議關閉-->
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${database.druid.maxpoolPreparedStatementPerConnectionSize}"/><!--啟用PSCache,必須配置大于0,當大于0時-->
        <property name="filters" value="${database.druid.filters}"/><!--驅動-->
    </bean>
    <bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/><!-- 資料源 -->
        <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/><!-- JPA核心配置檔案 -->
        <property name="persistenceUnitName" value="LEA_SPRING_JPA"/><!-- 持久化單元名稱 -->
        <property name="packagesToScan" value="com.xiyue.leaspring.dao"/><!-- PO類掃描包 -->
        <property name="persistenceProvider"><!-- 持久化提供類,本次為hibernate -->
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"/>
        </property>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
        </property>
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
        </property>
    </bean>
    <!-- 定義SpringDataJPA的資料層接口所在包,該包中的接口一定義是Repository子接口 -->
    <jpa:repositories base-package="com.xiyue.leaspring.dao"/>
    <!-- 定義事務管理的配置,必須配置PlatformTransactionManager接口子類 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <!--開啟注解事務-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <security:authentication-manager><!--定義認證管理器-->
        <security:authentication-provider><!--配置認證管理配置類-->
            <security:jdbc-user-service
                    data-source-ref="dataSource"
                    users-by-username-query="select mid as username,password,enabled from member where mid=?"
                    authorities-by-username-query="select mid as username,rid as authorities from member_role where mid=?"/>
        </security:authentication-provider>
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>

    <security:authentication-manager id="authenticationManager"><!--定義Security認證管理器-->
        <security:authentication-provider user-service-ref="userDetailService"/>
    </security:authentication-manager>
</beans>
           

由于投票管理器中需要考慮到表達式的支援,是以在本程式建立投票管理器對象時,依然在表達式配置類中注入了角色繼承關系,最終的投票管理器需要通過HttpSecurity類對象完成配置。