主頁 > 後端開發 > 學習一下 Spring Security

學習一下 Spring Security

2020-11-20 13:38:17 後端開發

一、Spring Security

1、什么是 Spring Security?

(1)基本認識
  Spring Security 是基于 Spring 框架,用于解決 Web 應用安全性的 一種方案,是一款優秀的權限管理框架,
  Web 應用的安全一般關注 用戶認證(authentication) 以及 用戶授權(authorization) 這兩個部分,簡單的理解就是 Web 應用 如何確定 你是誰 以及 你能干什么,

【官網地址:】
    https://spring.io/projects/spring-security

(2)用戶認證(authentication)
  用戶認證就是 驗證某個用戶是否為系統中的合法主體,也即該用戶是否能登陸系統,通常根據用戶名以及密碼進行確認,
  簡單的理解就是 使 Web 應用確定 你是誰,

(3)用戶授權(authorization)
  用戶授權就是 驗證某個用戶是否有執行某個操作的權限,
  簡單的理解就是 使 Web 應用確定 你能干什么,

(4)記住幾個點

【@EnableWebSecurity】
    用于開啟 WebSecurity 模式,有時不需要也可以實作相應的功能,
    
【@EnableGlobalMethodSecurity】
    用于開啟注解,常見引數為:prePostEnabled、securedEnabled,

【WebSecurityConfigurerAdapter】
    用于自定義 Security 策略,

【AuthenticationManagerBuilder】
    用于自定義 認證策略,

 

2、Spring Security 與 Shiro 簡單比較一下?

(1)Spring Security
  基于 Spring 框架開發,可以與 Spring 無縫整合,
  屬于重量級的權限控制框架(依賴其他組件、引入各種依賴),提供了全面的權限控制,

(2)Shiro
  Apache 的輕量級權限控制框架,不與任何框架捆綁,
  使用起來比 Spring Security 簡單,

(3)使用
  一般來說,使用 Shiro 可以解決大部分專案的問題,且容易操作,
  而 SpringBoot 提供了自動化配置方案,通過較少的配置就可以使用 Spring Security,
  所以常見組合通常為: SSM + Shiro 或者 SpringBoot / SpringCloud + Spring Security,

3、Spring Security 初體驗(SpringBoot + Spring Security)

(1)步驟

【步驟:】
    Step1:新建一個 SpringBoot 專案,
    Step2:引入 Web 依賴、Spring Security 依賴,
    Step3:新建一個 controller 進行測驗,
注:
    此處僅匯入依賴,未進行任何配置,所以顯示的都是默認效果,

【效果:】
當 Spring Security 依賴存在時,訪問 controller 時會默認跳轉到登陸頁面,
默認用戶名為:user
密碼在控制臺上可以看到(隨機生成),

 

(2)新建一個 SpringBoot 專案,并添加 Web、Spring Security 等依賴,

 

 

 

 

 

 

【依賴:】
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

 

(3)新建一個 controller,并簡單測驗一下 Spring Security,

【controller:】
package com.lyh.demo.springsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("test")
@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "hello spring security";
    }
}

如下圖所示,未添加 SpringSecurity 依賴時,訪問 controller 沒有限制,
而 添加上依賴后,訪問 controller 會首先跳轉到登錄頁面,成功登錄后才允許訪問,

 

 

 

4、Spring Security 再次體驗(SSM + Spring Security)

(1)步驟
  使用 SSM 時,需要進行一些繁瑣的配置,沒有 SpringBoot 用起來舒服,
  此處簡單配置一下,后面介紹仍然以 SpringBoot 為主,

【步驟:】
    Step1:創建一個 maven 工程 或者 web 工程(能使用 SpringMVC 即可),可參考:https://www.cnblogs.com/l-y-h/p/12030104.html
    Step2:配置 SpringSecurity,并測驗,

 

(2)新建 maven 工程,匯入相關依賴
  此處使用 tomcat 8 版本啟動專案,tomcat 7 啟動后在登錄時可能會報錯,

【依賴】
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-core</artifactId>
  <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-web</artifactId>
  <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-config</artifactId>
  <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>5.2.8.RELEASE</version>
</dependency>
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.11</version>
  <scope>test</scope>
</dependency>

【注意:(tomcat7 版本可能會報如下的錯誤,更換 tomcat 8 以上版本即可)】
java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.changeSessionId()Ljava/lang/String;

 

(3)配置基本的 web 環境(Spring 以及 SpringMVC)

【web.xml】
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

  <!-- step1: 配置全域的引數,啟動Spring容器 -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <!-- 若沒有提供值,默認會去找/WEB-INF/applicationContext.xml, -->
    <param-value>classpath:applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!-- step2: 配置SpringMVC的前端控制器,用于攔截所有的請求  -->
  <servlet>
    <servlet-name>springmvcDispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <init-param>
      <param-name>contextConfigLocation</param-name>
      <!-- 若沒有提供值,默認會去找WEB-INF/*-servlet.xml, -->
      <param-value>classpath:dispatcher-servlet.xml</param-value>
    </init-param>
    <!-- 啟動優先級,數值越小優先級越大 -->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>springmvcDispatcherServlet</servlet-name>
    <!-- 將DispatcherServlet請求映射配置為"/",則Spring MVC將捕獲Web容器所有的請求,包括靜態資源的請求 -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <!-- step3: characterEncodingFilter字符編碼過濾器,放在所有過濾器的前面 -->
  <filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <!--要使用的字符集,一般我們使用UTF-8(保險起見UTF-8最好)-->
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <!--是否強制設定request的編碼為encoding,默認false,不建議更改-->
      <param-name>forceRequestEncoding</param-name>
      <param-value>false</param-value>
    </init-param>
    <init-param>
      <!--是否強制設定response的編碼為encoding,建議設定為true-->
      <param-name>forceResponseEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <!--這里不能留慷訓者直接寫 ' / ' ,否則可能不起作用-->
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- step4: 配置過濾器,將post請求轉為delete,put -->
  <filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

【applicationContext.xml】
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
       http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

    <!-- step1: 配置包掃描方式,掃描所有包,但是排除Controller層 -->
    <context:component-scan base-package="com.lyh.demo">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
</beans>

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

    <!-- step1: 配置Controller掃描方式 -->
    <!-- 使用組件掃描的方式可以一次掃描多個Controller,只需指定包路徑即可 -->
    <context:component-scan base-package="com.lyh.demo" use-default-filters="false">
        <!-- 一般在SpringMVC的配置里,只掃描Controller層,Spring配置中掃描所有包,但是排除Controller層,
        context:include-filter要注意,如果base-package掃描的不是最終包,那么其他包還是會掃描、加載,如果在SpringMVC的配置中這么做,會導致Spring不能處理事務,
        所以此時需要在<context:component-scan>標簽上,增加use-default-filters="false",就是真的只掃描context:include-filter包括的內容-->
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

    <!-- step2: 配置視圖決議器 -->
    <bean id="defaultViewResolver" >
        <property name="prefix" value="https://www.cnblogs.com/WEB-INF/"/><!--設定JSP檔案的目錄位置-->
        <property name="suffix" value="https://www.cnblogs.com/l-y-h/p/.jsp"/>
    </bean>

    <!-- step3: 標準配置 -->
    <!-- 將springmvc不能處理的請求交給 spring 容器處理 -->
    <mvc:default-servlet-handler/>
    <!-- 簡化注解配置,并提供更高級的功能 -->
    <mvc:annotation-driven />
</beans>

 

(4)配置 SpringSecurity,并新建一個 controller 進行測驗

【web.xml 中配置核心過濾器鏈 springSecurityFilterChain】
<filter>
  <filter-name>springSecurityFilterChain</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
  <filter-name>springSecurityFilterChain</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>


【新建一個 spring-security.xml 用于進行 Spring Security 相關配置】
<?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:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

    <!--
        配置 Spring-Security.
        auto-config="true" 表示使用框架默認提供的登錄界面
        use-expressions="true" 表示使用 Spring 的 EL 運算式
    -->
    <security:http auto-config="true" use-expressions="true">
        <!--
            配置攔截請求,
            pattern="/**" 表示攔截所有請求
            access="hasAnyRole('ROLE_USER')" 表示只有角色為 ROLE_USER 的用戶才能訪問并登陸系統
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
    </security:http>

    <!--
        配置用戶資訊(用戶管理)
        密碼默認是加密的,若不想密碼加密,則可以在 密碼前面添加 {noop}
    -->
    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <security:user name="tom" password="{noop}123456" authorities="ROLE_USER" />
                <security:user name="jarry" password="{noop}123456" authorities="ROLE_ADMIN" />
                <security:user name="jack" password="123456" authorities="ROLE_USER" />
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
</beans>


【在 web.xml 中匯入 spring-security.xml 檔案(與匯入 applicationContext.xml 類似,也可以在 applicationContext.xml 中通過 <import> 標簽引入 spring-security.xml)】
<context-param>
  <param-name>contextConfigLocation</param-name>
  <!-- 若沒有提供值,默認會去找/WEB-INF/applicationContext.xml, -->
  <param-value>
    classpath:applicationContext.xml
    classpath:spring-security.xml
  </param-value>
</context-param>

【完整 web.xml 如下:】
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

  <!-- step1: 配置全域的引數,啟動Spring容器 -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <!-- 若沒有提供值,默認會去找/WEB-INF/applicationContext.xml, -->
    <param-value>
      classpath:applicationContext.xml
      classpath:spring-security.xml
    </param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!-- step2: 配置SpringMVC的前端控制器,用于攔截所有的請求  -->
  <servlet>
    <servlet-name>springmvcDispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <init-param>
      <param-name>contextConfigLocation</param-name>
      <!-- 若沒有提供值,默認會去找WEB-INF/*-servlet.xml, -->
      <param-value>classpath:dispatcher-servlet.xml</param-value>
    </init-param>
    <!-- 啟動優先級,數值越小優先級越大 -->
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>springmvcDispatcherServlet</servlet-name>
    <!-- 將DispatcherServlet請求映射配置為"/",則Spring MVC將捕獲Web容器所有的請求,包括靜態資源的請求 -->
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <!-- step3: characterEncodingFilter字符編碼過濾器,放在所有過濾器的前面 -->
  <filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <!--要使用的字符集,一般我們使用UTF-8(保險起見UTF-8最好)-->
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <!--是否強制設定request的編碼為encoding,默認false,不建議更改-->
      <param-name>forceRequestEncoding</param-name>
      <param-value>false</param-value>
    </init-param>
    <init-param>
      <!--是否強制設定response的編碼為encoding,建議設定為true-->
      <param-name>forceResponseEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <!--這里不能留慷訓者直接寫 ' / ' ,否則可能不起作用-->
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- step4: 配置過濾器,將post請求轉為delete,put -->
  <filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- Step5:配置 SpringSecurity 核心過濾器鏈 -->
  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

【新建一個 TestController.java 進行測驗】
package com.lyh.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("test")
@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "hello spring security";
    }
}

如下圖所示,未配置 springSecurityFilterChain 時,等同于普通的系統登錄,
配置 springSecurityFilterChain 后,在 spring-security.xml 中可以看到,
配置了如下內容:
  攔截所有請求,并只允許擁有 ROLE_USER 這個角色的用戶才可以登錄,
  設定了三個用戶,tom 為 ROLE_USER 角色,且密碼未加密,所以可以正常登陸,
  jarry 為 ROLE_ADMIN 角色,沒有權限,所以不能正常登陸,
  jack 為 ROLE_USER 角色,但密碼被加密,所以不能正常登陸,

 

 

 

5、Spring Security 過濾器鏈

(1)本質
  Spring Security 基于 Servlet 過濾器實作的,
  默認由 15 個過濾器組成過濾器鏈(可以通過配置添加、移除過濾器),通過過濾器攔截請求并進行相關操作,

 

 

 

(2)簡單了解幾個過濾器

【org.springframework.security.web.context.SecurityContextPersistenceFilter】
    此過濾器主要是在 SecurityContextRepository 中 保存或者更新 SecurityContext,并交給后續的過濾器操作,
    而 SecurityContext 中保存了當前用戶認證、權限等資訊,
    
【org.springframework.security.web.csrf.CsrfFilter】
    此過濾器用于防止 CSRF 攻擊,Spring Security 4.0 開始,默認開啟 CSRF 防護,針對 PUT、POST、DELETE 等請求進行防護,
注:
    CSRF 指的是 Cross Site Request Forgery,即 跨站請求偽造,
    簡單理解為:攻擊者冒用用戶身份去執行操作,
   舉例:
       用戶打開瀏覽器并成功登陸某個網站 A,
       此時用戶 未登出網站 A,且在同一瀏覽器中新增一個 Tab 頁并訪問 網站 B,
       而瀏覽器接收到網站 B 回傳的惡意代碼后,在用戶不知情的情況下攜帶 cookie 等用戶資訊向 A 網站發送請求,
       網站 A 處理該請求,從而導致網站 B 的惡意代碼被執行,
   簡單理解就是:用戶登錄一個網站 A,并打開了另一個網站 B,B 網站攜帶惡意代碼 且使用用戶身份去訪問 網站 A,

    XSS 指的是 Cross Site Scripting,即 跨站腳本,
    簡單理解:攻擊者將惡意代碼嵌入網站,當用戶訪問網站時導致 惡意代碼被執行,
    
【org.springframework.security.web.authentication.logout.LogoutFilter】
    匹配 URL(默認為 /logout),用于實作用戶退出并清除認證資訊,   
    
【org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter】
    匹配 URL(默認為 /login),用于實作用戶登錄認證操作(必須為 POST 請求),
    
【org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter】
    若沒有指定登錄認證界面,此過濾器會提供一個默認的界面,
    
【org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter】
    若沒有指定登出界面,此過濾器會提供一個默認的界面,
    
【org.springframework.security.web.authentication.AnonymousAuthenticationFilter】
    創建一個匿名身份,用于系統的訪問,(兼容游客登錄模式)
    
【org.springframework.security.web.access.ExceptionTranslationFilter】
    位于整個 springSecurityFilterChain 過濾鏈后方,用于處理鏈路中的例外(跳轉到指定頁面或者回傳錯誤資訊),
    
【org.springframework.security.web.access.intercept.FilterSecurityInterceptor】
    獲取資源訪問的授權資訊,根據 SecurityContext 中存盤的用戶資訊來決定操作是否有權限,

 

(3)這些過濾器是如何加載進來的?
  通過前面 SSM + Spring Security 可以看到,在 web.xml 中配置了名為 springSecurityFilterChain 的過濾器,可以 Debug 看下 DelegatingFilterProxy 加載的流程,

【基本流程:】
Step1:
    通過 DelegatingFilterProxy 過濾器的 doFilter() 獲取到 FilterChainProxy 過濾器并執行,
Step2:
    通過 FilterChainProxy 過濾器的 doFilter() 呼叫 doFilterInternal() 加載到 過濾器鏈,
Step3:
    doFilterInternal() 內部通過 SecurityFilterChain 介面獲取到 過濾器鏈,
Step4:
    SecurityFilterChain 介面實作類為 DefaultSecurityFilterChain,

 

二、SpringBoot + SpringSecurity 相關操作

1、三種認證方式(設定用戶名、密碼)

  不進行任何 SpringSecurity 配置時,系統默認提供用戶名以及密碼,但是這種情況肯定不適用于作業場景,那么如何進行 認證呢?

(1)方式一:
  通過組態檔 application.properties 或者 application.yml 中直接定義,
  不太適用于實際作業場景,

【在 application.properties 中直接定義 用戶名、密碼】
spring.security.user.name=tom
spring.security.user.password=123456

 

 

 

(2)方式二:
  通過配置類的形式,(需要繼承 WebSecurityConfigurerAdapter 抽象類)
  不太適用于實際作業場景,

【通過配置類的形式:】
package com.lyh.demo.springsecurity.config;

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.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 配置 Spring Security
 */
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        auth.inMemoryAuthentication()
            .withUser("jack")
//            .password("{noop}" + "123456") // 未配置 PasswordEncoder 時,可以在 密碼前拼接上 {noop},防止出錯
            .password(bCryptPasswordEncoder.encode("123456"))
            .roles("admin");
    }

    /**
     * 配置加密類,若不配置,則 bCryptPasswordEncoder.encode() 進行加密時會出錯,
     * java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
     *
     * 若不想配置,可以在 設定 password 時,在密碼前添加上 {noop}
     * @return 加密類
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

 

 

(3)方式三
  通過配置類 以及 自定義實作類(實作 UserDetailsService 介面)實作,
  適用于作業場景(從資料庫中查詢出用戶資訊并認證),

【步驟一:在配置類中 指定使用 UserDetailsService 介面,并注入其 實作類】
package com.lyh.demo.springsecurity.config;

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.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 配置 Spring Security
 */
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

 

 

【步驟二:撰寫自定義實作類(實作 UserDetailsService 介面)】
package com.lyh.demo.springsecurity.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 設定用戶權限,若有多個權限可以使用 逗號分隔
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        return new User("jarry", new BCryptPasswordEncoder().encode("123456"), auths);
    }
}

 

 

 

2、SpringSecurity 中登錄認證程序中 的密碼加密(BCryptPasswordEncoder)

(1)為什么要了解密碼加密?
  Spring Security 5.0 以上版本 對于密碼處理需要特別注意一下,前面也介紹了,Spring Security 認證時會對密碼進行加密,采用 {encodingId}password 的形式設定加密方式,
  如果不想密碼加密,可以在配置密碼時在 密碼前拼接上 {noop}, 即 ({noop}password),
  而實際場景中,資料庫存盤的密碼都是非明文存盤(即存盤的都是加密后的密碼),所以有必要了解一下 SpringSecurity 加密相關內容,

【相關的 encodingId 與其 對應的 物體類 如下:】
public static PasswordEncoder createDelegatingPasswordEncoder() {
    String encodingId = "bcrypt";
    Map<String, PasswordEncoder> encoders = new HashMap();
    encoders.put(encodingId, new BCryptPasswordEncoder());
    encoders.put("ldap", new LdapShaPasswordEncoder());
    encoders.put("MD4", new Md4PasswordEncoder());
    encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
    encoders.put("noop", NoOpPasswordEncoder.getInstance());
    encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    encoders.put("scrypt", new SCryptPasswordEncoder());
    encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
    encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
    encoders.put("sha256", new StandardPasswordEncoder());
    encoders.put("argon2", new Argon2PasswordEncoder());
    return new DelegatingPasswordEncoder(encodingId, encoders);
}

 

 

 

(2)PasswordEncoder
  SpringSecurity 默認需要在容器中存在 PasswordEncoder 實體物件,用于進行密碼加密,所以配置 SpringSecurity 時,需要在容器中配置一個 PasswordEncoder Bean 物件(一般使用 BCryptPasswordEncoder 實體物件),

【在配置類中通過 @Bean 配置一個 PasswordEncoder 的 Bean 物件:】
@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

【PasswordEncoder 常用方法:】
String encode(CharSequence rawPassword);   // 用于密碼加密
boolean matches(CharSequence rawPassword, String encodedPassword); // 用于密碼解密,rawPassword 表示待匹配的密碼,encodedPassword 表示加密后的密碼,

 

 

 

(3)BCryptPasswordEncoder
  是最常用的一種密碼決議器,其通過 哈希演算法 并加上 隨機鹽(salt)的方式進行密碼加密,
  密碼解密時,根據加密后的資料 A 得到鹽值(salt),將待比較資料根據鹽值進行一次加密得到 B,如果 B 與 A 是相同的結果,則說明密碼是正確的,
  密碼加密、解密的關鍵點在于 鹽值的計算,

密碼加密相關代碼如下所示:

【 encode() 加密:】
加密代碼如下所示,首先呼叫 BCrypt.gensalt() 方法計算出 鹽值(salt),
然后呼叫 BCrypt.hashpw() 方法,根據 鹽值(salt)進行密碼(password)加密,

而在 BCrypt 中的 hashpw() 中,會通過 salt.substring() 截取并得到真實的鹽值(real_salt),
通過 B.crypt_raw() 求得一個哈希陣列(hashed),通過 encode_base64() 進行加密,

public String encode(CharSequence rawPassword) {
   if (rawPassword == null) {
      throw new IllegalArgumentException("rawPassword cannot be null");
   }

   String salt;
   if (random != null) {
      salt = BCrypt.gensalt(version.getVersion(), strength, random);
   } else {
      salt = BCrypt.gensalt(version.getVersion(), strength);
   }
   return BCrypt.hashpw(rawPassword.toString(), salt);
}

【BCrypt】
public static String hashpw(String password, String salt) {
   byte passwordb[];

   passwordb = password.getBytes(StandardCharsets.UTF_8);

   return hashpw(passwordb, salt);
}

public static String hashpw(byte passwordb[], String salt) {
   BCrypt B;
   String real_salt;
   byte saltb[], hashed[];
   char minor = (char) 0;
   int rounds, off;
   StringBuilder rs = new StringBuilder();

   if (salt == null) {
      throw new IllegalArgumentException("salt cannot be null");
   }

   int saltLength = salt.length();

   if (saltLength < 28) {
      throw new IllegalArgumentException("Invalid salt");
   }

   if (salt.charAt(0) != '$' || salt.charAt(1) != '2')
      throw new IllegalArgumentException ("Invalid salt version");
   if (salt.charAt(2) == '$')
      off = 3;
   else {
      minor = salt.charAt(2);
      if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b')
            || salt.charAt(3) != '$')
         throw new IllegalArgumentException ("Invalid salt revision");
      off = 4;
   }

   // Extract number of rounds
   if (salt.charAt(off + 2) > '$')
      throw new IllegalArgumentException ("Missing salt rounds");

   if (off == 4 && saltLength < 29) {
      throw new IllegalArgumentException("Invalid salt");
   }
   rounds = Integer.parseInt(salt.substring(off, off + 2));

   real_salt = salt.substring(off + 3, off + 25);
   saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);

   if (minor >= 'a') // add null terminator
      passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);

   B = new BCrypt();
   hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0);

   rs.append("$2");
   if (minor >= 'a')
      rs.append(minor);
   rs.append("$");
   if (rounds < 10)
      rs.append("0");
   rs.append(rounds);
   rs.append("$");
   encode_base64(saltb, saltb.length, rs);
   encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
   return rs.toString();
}

密碼解密相關代碼如下所示:

【matches() 解密:】
解密代碼如下所示,首先確保 encodedPassword 是加密后的代碼,
然后呼叫 BCrypt.checkpw() 進行密碼匹配,

而 BCrypt 的 checkpw() 中,可以看到其會將待比較的密碼 重新進行一次 hashpw() 密碼加密,
而此時傳入的鹽值是 加密的代碼,在 hashpw() 方法中會截取出相應的 鹽值(real_salt)并用于加密,
加密完成后,再去比較新加密的密碼 與 原來加密的密碼 是否相同即可,

所以如果待比較的密碼 與 加密的密碼是相同的,也即相當于 根據相同的 鹽值 再加密了一次,加密結果是相同的,


public boolean matches(CharSequence rawPassword, String encodedPassword) {
   if (rawPassword == null) {
      throw new IllegalArgumentException("rawPassword cannot be null");
   }

   if (encodedPassword == null || encodedPassword.length() == 0) {
      logger.warn("Empty encoded password");
      return false;
   }

   if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
      logger.warn("Encoded password does not look like BCrypt");
      return false;
   }

   return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}

【BCrypt】
public static boolean checkpw(String plaintext, String hashed) {
   return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}

static boolean equalsNoEarlyReturn(String a, String b) {
   return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
}

 

3、從資料庫中查詢用戶資訊并認證(MyBatis-Plus + MySQL 8 )

(1)建表(SQL)
  MyBatis-Plus 使用可以參考:https://www.cnblogs.com/l-y-h/p/12859477.html

  配置 SpringSecurity 時需要配置 使用密碼加密,
  若不使用加密,則需在設定密碼時在密碼前拼接上 {noop},
  若使用加密,則使用 BCryptPasswordEncoder 的 encode() 方法對其進行加密,
  若資料庫存盤的已經是 BCryptPasswordEncoder 加密后的資料,不用再次加密,

此處為了方便理解,存盤密碼時均使用 明文存盤,

【建表 SQL :】
DROP DATABASE IF EXISTS testSpringSecurity;

CREATE DATABASE testSpringSecurity;

USE testSpringSecurity;

DROP TABLE IF EXISTS users;

CREATE TABLE users
(
    id BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '主鍵ID',
    name VARCHAR(30) NOT NULL COMMENT '姓名',
    password VARCHAR(64) NOT NULL COMMENT '密碼',
    role VARCHAR(20) NOT NULL COMMENT '角色'
);

INSERT INTO users (name, password, role) VALUES
('tom', '123456', 'user'),
('jarry', '123456', 'admin'),
('jack', '123456', 'ROLE_USER');

 

 

 

(2)引入 MyBatis-Plus 與 MySQL 相關依賴

【依賴:】
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1.tmp</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.18</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
</dependency>

 

 

 

(3)配置 MyBatis-Plus 以及 MySQL 資料源資訊

【資料源資訊】
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/testSpringSecurity?useUnicode=true&characterEncoding=utf8

 

(4)撰寫 資料表對應的 物體類,以及相應的 mapper 或者 service(用于操作資料庫)

【物體類:】
package com.lyh.demo.springsecurity.entity;

import lombok.Data;

@Data
public class Users {
    private Long id;
    private String name;
    private String password;
    private String role;
}

【Mapper:】
package com.lyh.demo.springsecurity.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lyh.demo.springsecurity.entity.Users;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Service;

@Mapper
@Service
public interface UsersMapper extends BaseMapper<Users> {
}

 

 

 

(5)結合 SpringSecurity 進行安全驗證,
  通過前面分析,添加上 SpirngSecurity 配置類后,會執行 loadUserByUsername() 方法將需要認證的用戶資訊加載到當前認證系統中,所以在此添加 查詢資料庫的邏輯即可,
  首先根據用戶名 在資料庫中 查詢出相應的 用戶、密碼 并封裝到 物體類中,并將此時的用戶、密碼、角色等加入到 當前認證系統中,然后再根據 輸入的用戶名、密碼 進行驗證,

【修改 MyUserDetailsService 中 loadUserByUsername() 代碼:(改為從資料庫中獲取用戶)】
package com.lyh.demo.springsecurity.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lyh.demo.springsecurity.entity.Users;
import com.lyh.demo.springsecurity.mapper.UsersMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UsersMapper usersMapper;

    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        // 定義查詢條件,根據用戶名 從資料庫查詢 對應的 用戶、密碼、角色
        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", name);
        Users user = usersMapper.selectOne(queryWrapper);

        // 用戶不存在時,直接拋例外
        if (user == null) {
            throw new UsernameNotFoundException("用戶不存在");
        }

        // 用戶存在時,把 用戶、密碼、角色 加入到當前認證系統中
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole());
        // 將資料庫中的密碼進行 加密
        return new User(user.getName(), new BCryptPasswordEncoder().encode(user.getPassword()), auths);
//        return new User(user.getName(), user.getPassword(), auths); // 若資料庫密碼已經加密過,直接使用即可
    }
}

 

 

 

4、自定義頁面(不使用默認頁面)以及 頁面跳轉、頁面訪問權限控制

(1)自定義頁面
  在前面與 資料庫 互動的基礎上,添加如下代碼,

【登錄頁面:(login.html)】
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<div>
    <form method="post" action="/login">
        <h2>Please sign in</h2>
        <p>
            <label for="username">Username</label>
            <input type="text" id="username" name="username" placeholder="Username" required="" autofocus="">
        </p>
        <p>
            <label for="password" class="sr-only">Password</label>
            <input type="password" id="password" name="password" placeholder="Password" required="">
        </p>
        <button type="submit">Sign in</button>
    </form>
</div>
</body>
</html>403 頁面:】
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>403</title>
</head>
<body>
<h1>403</h1>
</body>
</html>

【在 TestController 中添加一個 處理錯誤的邏輯:】
package com.lyh.demo.springsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("test")
@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "hello spring security";
    }

    @PostMapping("/error")
    public String error() {
        return "login error";
    }
}

 

 

 

(2)撰寫配置類,配置頁面跳轉規則
  在配置類中,重寫 configure() 方法,并通過 formLogin() 方法設定相關頁面,

【配置類中重寫 configure() 方法:】
package com.lyh.demo.springsecurity.config;

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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 配置 Spring Security
 */
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        /**
         * csrf() 表示開啟 csrf 防護,
         * disable() 表示關閉 csrf 防護,
         */
        http.csrf().disable();

        /**
         * formLogin() 用于自定義表單登錄,
         * loginPage() 用于自定義登錄頁面,
         * defaultSuccessUrl() 登錄成功后 跳轉的路徑,
         * loginProcessingUrl() 表單提交的 action 地址(默認為 /login,修改后,對應的表單 action 也要修改),由系統提供 UsernamePasswordAuthenticationFilter 過濾器攔截并處理,
         * usernameParameter() 用于自定義表單提交的用戶引數名,默認為 username,修改后,對應的表單引數也要修改,
         * passwordParameter() 用于自定義表單提交的用戶密碼名,默認為 password,修改后,對應的表單引數也要修改,
         * failureForwardUrl() 用于自定義表單提交失敗后 重定向地址,可用于前后端分離中,指向某個 controller,注意使用 POST 處理,
         */
        http.formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/login")
            .defaultSuccessUrl("/test/hello")
            //.usernameParameter("name")
            //.passwordParameter("pwd")
            .failureForwardUrl("/test/error")
        ;

        /**
         * authorizeRequests()  用于 開啟認證,基于 HttpServletRequest 對 url 進行身份控制并授權訪問,
         * antMatchers() 用于匹配 url,
         * permitAll() 用于允許任何人訪問該 url,
         * hasAuthority() 用于指定 具有某種權限的 人才能訪問 url,
         * hasAnyAuthority() 用于指定 多個權限 進行訪問,多個權限間使用逗號分隔,
         *
         * hasRole() 寫法與 hasAuthority() 類似,但是其會在 角色前 拼接上 ROLE_,使用時需要注意,
         * hasAnyRole() 寫法與 hasAnyAuthority() 類似,同樣會在 角色前 拼接上 ROLE_,
         *
         * 使用時 hasAuthority()、hasAnyAuthority() 或者 hasAnyRole()、hasAnyAuthority() 任選一對即可,同時使用四種可能會出現問題,
         */
        http.authorizeRequests()
                .antMatchers("/test/hello").hasAuthority("user")
                //.antMatchers("/test/hello").hasAnyRole("USER,GOD")
                //.antMatchers("/test/hello").hasRole("GOD")
                .antMatchers("/test/hello").hasAnyAuthority("user,admin")
                .antMatchers("/login", "/test/error").permitAll();

        /**
         * 自定義 403 頁面
         */
        http.exceptionHandling().accessDeniedPage("/403.html");
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

  如下圖所示,tom 的角色為 user、jarry 的角色為 admin,jack 的角色為 ROLE_USER,
  只允許 user、admin 角色能夠訪問 /test/hello,即 tom、jarry 可以成功訪問系統,而 jack 訪問時會跳轉到 403 頁面,若 用戶名 或者 密碼輸入錯誤時,將會跳轉到 /test/error 畫面,

 

 

 

5、了解幾個注解

  為了簡化開發,可以使用注解進行相關操作(操作不太靈活,慎用),
(1)@Secured
  添加在 方法上,并可以指定用戶角色,作用是只允許指定的用戶角色去訪問 該方法,

【使用步驟一:】
    在配置類上,通過 @EnableGlobalMethodSecurity(securedEnabled = true) 開啟注解,
    
【使用步驟二:】
    在方法上添加注解 @Secured,并指定 角色,角色前綴要為 ROLE_,
    
@GetMapping("/testSecured")
@Secured({"ROLE_USER"})
public String testSecured() {
    return "success";
}

注:
    由于 角色需要使用 ROLE_ 為前綴,所以資料庫存盤的 角色需要以 ROLE_ 為前綴 或者 設定權限時手動加上 ROLE_,

 

 

(2)@PreAuthorize
  添加在 方法上,并可以指定用戶角色,作用是只允許指定的用戶角色去訪問 該方法,
  在進入方法之前 會進行 校驗,校驗通過后才能執行方法,

【使用步驟一:】
    在配置類上,通過 @EnableGlobalMethodSecurity(prePostEnabled = true) 開啟注解,
    
【使用步驟二:】
    在方法上添加 @PreAuthorize 注解,并指定角色,角色的指定可以使用 Spring 運算式,
    
@GetMapping("/testSecured")
@PreAuthorize("hasAnyAuthority('user', 'ROLE_USER')")
public String testSecured() {
    return "success";
}

 

 

(3)@PostAuthorize
  添加在 方法上,并可以指定用戶角色,作用是只允許指定的用戶角色去訪問 該方法,
  在進入方法之后 會進行 校驗,不管有沒有權限,都會執行方法,適合帶有回傳值的校驗,

【使用步驟一:】
    在配置類上,通過 @EnableGlobalMethodSecurity(prePostEnabled = true) 開啟注解,

【使用步驟二:】
    在方法上添加 @PostAuthorize 注解,并指定角色,角色的指定可以使用 Spring 運算式,

@GetMapping("/testSecured")
@PostAuthorize("hasAuthority('user')")
public String testSecured() {
    System.out.println("不管有沒有權限,我都會執行");
    return "success";
}

 

 

6、用戶注銷操作

(1)自定義一個登錄成功頁面,并添加一個 退出鏈接,

【登錄成功頁面 success.html】
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>success</title>
</head>
<body>
<h1>Success</h1>
<a href="https://www.cnblogs.com/logout">注銷</a>
</body>
</html>

 

 

(2)撰寫配置類,修改頁面退出規則,
  此處為了跳轉到 success.html 頁面,還需要 通過 http.formLogin().defaultSuccessUrl() 去指定頁面,

【添加退出規則:】
http.formLogin()
    .loginPage("/login.html")
    .loginProcessingUrl("/login")
    .defaultSuccessUrl("/success.html")
    //.usernameParameter("name")
    //.passwordParameter("pwd")
    .failureForwardUrl("/test/error")
;

/**
 * logout() 用于自定義退出邏輯,
 * logoutUrl() 用于攔截退出請求,默認為 /logout,
 * logoutSuccessUrl() 用于自定義退出成功后,跳轉的頁面,
 */
http.logout()
    .logoutUrl("/logout")
    .logoutSuccessUrl("/login.html")
;

 

 

  如下圖所示,tom、jarry 可以訪問 /test/hello,jack 不可以訪問,所以當使用 tom、jarry 登錄時可以成功登陸,jack 會顯示 403,一旦點擊注銷后,需要再次進行登錄才能繼續訪問 /test/hello,

 

 

7、記住我

(1)作業流程
  記住我 功能上指的是 用戶通過瀏覽器登錄一次網站后,關閉瀏覽器并再次訪問網站時,可以不用再次登錄而直接進行相關操作,

【作業流程:】
第一次通過瀏覽器登錄系統時:
    首先 用戶名、密碼 會被 UsernamePasswordAuthenticationFilter 過濾器攔截,并進行認證,
    認證通過后,會呼叫 RememberMeServices 生成 token,并將 token 寫入資料庫 以及 瀏覽器 cookie 中,
    
第二次通過瀏覽器登錄系統時:
    直接攜帶 cookie 訪問,會被 RememberMeAuthenticationFilter 過濾器攔截,根據 cookie 讀取出 token 資訊,
    從資料庫中查找出 對應的 token 并比較,若相同,則可以登錄系統,否則跳轉到登錄頁面,  

作業流程見下圖(圖片來源于網路):

 

 

(2)基本實作:
  由于 token 需要存盤在 資料庫中,所以需要配置資料源資訊,并操作,而 SpringSecurity 中已經提供了相關操作類,只需在配置類中配置即可,

【配置如下:(注入 DataSource,并配置 PersistentTokenRepository 交給 Spring 管理)】
@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    // 設定資料源
    jdbcTokenRepository.setDataSource(dataSource);
    // 自動建表
    // jdbcTokenRepository.setCreateTableOnStartup(true);
    return jdbcTokenRepository;
}


【完整配置類:(通過  http.rememberMe() 配置)】
package com.lyh.demo.springsecurity.config;

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.web.builders.HttpSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * 配置 Spring Security
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Autowired
    private DataSource dataSource;

    /**
     * 默認使用 PersistentTokenRepository 的子類  InMemoryTokenRepositoryImpl 將 token 放在記憶體中,
     * 可以使用子類 JdbcTokenRepositoryImpl 將 token 持久化到 資料庫中,
     * 注:
     *
     *  jdbcTokenRepository.setCreateTableOnStartup(true); 等同于下面 SQL,
     *  若不手動創建,可以使用代碼自動創建,但是執行一次后需要將其注釋掉,
     *
     *  create table persistent_logins (
     *         username varchar(64) not null,
     *         series varchar(64) primary key,
     *         token varchar(64) not null,
     *         last_used timestamp not null
     *  )
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 設定資料源
        jdbcTokenRepository.setDataSource(dataSource);
        // 自動建表
        // jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * rememberMe() 用于實作記住我功能,
         * tokenRepository() 設定資料訪問層,
         * userDetailsService() 設定 userDetailsService,
         * tokenValiditySeconds() 設定過期時間,
         * rememberMeParameter() 自定義引數名,默認為 remember-me
         */
        http.rememberMe()
                .tokenRepository(persistentTokenRepository())
                .userDetailsService(userDetailsService)
                //.rememberMeParameter("remember")
                .tokenValiditySeconds(24 * 60 * 60);

        /**
         * csrf() 表示開啟 csrf 防護,
         * disable() 表示關閉 csrf 防護,
         */
        http.csrf().disable();

        /**
         * formLogin() 用于自定義表單登錄,
         * loginPage() 用于自定義登錄頁面,
         * defaultSuccessUrl() 登錄成功后 跳轉的路徑,
         * loginProcessingUrl() 表單提交的 action 地址(默認為 /login,修改后,對應的表單 action 也要修改),由系統提供 UsernamePasswordAuthenticationFilter 過濾器攔截并處理,
         * usernameParameter() 用于自定義表單提交的用戶引數名,默認為 username,修改后,對應的表單引數也要修改,
         * passwordParameter() 用于自定義表單提交的用戶密碼名,默認為 password,修改后,對應的表單引數也要修改,
         * failureForwardUrl() 用于自定義表單提交失敗后 重定向地址,可用于前后端分離中,指向某個 controller,
         */
        http.formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/login")
            .defaultSuccessUrl("/success.html")
            //.usernameParameter("name")
            //.passwordParameter("pwd")
            .failureForwardUrl("/test/error")
        ;

        /**
         * logout() 用于自定義退出邏輯,
         * logoutUrl() 用于攔截退出請求,默認為 /logout,
         * logoutSuccessUrl() 用于自定義退出成功后,跳轉的頁面,
         */
        http.logout()
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login.html")
        ;

        /**
         * authorizeRequests()  用于 開啟認證,基于 HttpServletRequest 對 url 進行身份控制并授權訪問,
         * antMatchers() 用于匹配 url,
         * permitAll() 用于允許任何人訪問該 url,
         * hasAuthority() 用于指定 具有某種權限的 人才能訪問 url,
         * hasAnyAuthority() 用于指定 多個權限 進行訪問,多個權限間使用逗號分隔,
         *
         * hasRole() 寫法與 hasAuthority() 類似,但是其會在 角色前 拼接上 ROLE_,使用時需要注意,
         * hasAnyRole() 寫法與 hasAnyAuthority() 類似,同樣會在 角色前 拼接上 ROLE_,
         *
         * 使用時 hasAuthority()、hasAnyAuthority() 或者 hasAnyRole()、hasAnyAuthority() 任選一對即可,同時使用四種可能會出現問題,
         */
        http.authorizeRequests()
                .antMatchers("/test/hello").hasAuthority("user")
                //.antMatchers("/test/hello").hasAnyRole("USER,GOD")
                //.antMatchers("/test/hello").hasRole("GOD")
                .antMatchers("/test/hello").hasAnyAuthority("user,admin")
                .antMatchers("/login", "/test/error").permitAll();

        /**
         * 自定義 403 頁面
         */
        http.exceptionHandling().accessDeniedPage("/403.html");
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

 

  如下圖所示,點擊 記住我,并登陸后,會在瀏覽器 cookie 以及 資料庫中 各存放一份 token,關閉瀏覽器并再次登錄時,無需重新登錄,會自動檢測 cookie 中的 token 值是否正確,若相同則可以正常登陸,注銷時,瀏覽器 token 以及 資料庫的 token 會一起注銷,

 

 

(3)原始碼分析:
Step1:
  第一次通過瀏覽器登錄系統時,首先會被 UsernamePasswordAuthenticationFilter 過濾器攔截,認證通過后,會在 AbstractAuthenticationProcessingFilter 抽象類的 successfulAuthentication() 方法中 進行 token 的處理,

【UsernamePasswordAuthenticationFilter 繼承 AbstractAuthenticationProcessingFilter:】
    public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {}

【AbstractAuthenticationProcessingFilter 的 doFilter() 中 呼叫了 successfulAuthentication() 方法:】
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
      implements ApplicationEventPublisherAware, MessageSourceAware {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
      ...
      successfulAuthentication(request, response, chain, authResult);
  }
}

 

 

Step2:
  AbstractAuthenticationProcessingFilter 中定義了 RememberMeServices 介面,在 successfulAuthentication() 方法中 會呼叫 RememberMeServices 介面的 loginSuccess() 方法,

【呼叫 RememberMeServices 的 loginSuccess() 方法:】
private RememberMeServices rememberMeServices = new NullRememberMeServices();
protected void successfulAuthentication(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain, Authentication authResult)
      throws IOException, ServletException {
      ...
      rememberMeServices.loginSuccess(request, response, authResult);
}

 

 

Step3:
  RememberMeServices 介面的 loginSuccess() 方法 由子類 AbstractRememberMeServices 實作,loginSuccess() 會先檢測是否存在 記住我 的功能,默認引數名為 remember-me,若表單中不存在 或者 為 false 時,會直接回傳,為 true 時,會執行 onLoginSuccess() 方法,

【呼叫 AbstractRememberMeServices 的 loginSuccess() 方法:】
public final void loginSuccess(HttpServletRequest request,
      HttpServletResponse response, Authentication successfulAuthentication) {

   if (!rememberMeRequested(request, parameter)) {
      logger.debug("Remember-me login not requested.");
      return;
   }

   onLoginSuccess(request, response, successfulAuthentication);
}

 

 

Step4:
  onLoginSuccess() 方法由 AbstractRememberMeServices 的子類 PersistentTokenBasedRememberMeServices 去實作,向資料庫中 添加 token 以及 向 cookie 中添加 token,

【呼叫 PersistentTokenBasedRememberMeServices 的 onLoginSuccess() 方法:】
protected void onLoginSuccess(HttpServletRequest request,
      HttpServletResponse response, Authentication successfulAuthentication) {
   String username = successfulAuthentication.getName();

   logger.debug("Creating new persistent login for user " + username);

   PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
         username, generateSeriesData(), generateTokenData(), new Date());
   try {
      tokenRepository.createNewToken(persistentToken);
      addCookie(persistentToken, request, response);
   }
   catch (Exception e) {
      logger.error("Failed to save persistent token ", e);
   }
}

 

 

Step5:
  關閉瀏覽器,再次登錄時,由 RememberMeAuthenticationFilter 過濾器攔截請求,在其 doFilter() 方法中 呼叫 RememberMeServices 介面的 autoLogin() 方法進行處理,

【RememberMeAuthenticationFilter 的 】
public class RememberMeAuthenticationFilter extends GenericFilterBean implements
      ApplicationEventPublisherAware {
    private RememberMeServices rememberMeServices;
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
          ...
          Authentication rememberMeAuth = rememberMeServices.autoLogin(request,response);
          ...
      }
}

 

 

Step6:
  RememberMeServices 介面的 autoLogin() 方法由 AbstractRememberMeServices 子類實作,其根據 cookie 值決議出相應的 token,并根據 token 從資料庫中查詢用戶,并驗證用戶是否合法,

public final Authentication autoLogin(HttpServletRequest request,
      HttpServletResponse response) {
    String rememberMeCookie = extractRememberMeCookie(request);
    ...
    String[] cookieTokens = decodeCookie(rememberMeCookie);
    user = processAutoLoginCookie(cookieTokens, request, response);
    userDetailsChecker.check(user);
}

 

 

Step7:
  呼叫 processAutoLoginCookie() 方法根據 token 從資料庫中查詢出用戶,

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
      HttpServletRequest request, HttpServletResponse response) {
    final String presentedSeries = cookieTokens[0];
    final String presentedToken = cookieTokens[1];

    PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);
    if (!presentedToken.equals(token.getTokenValue())) {
    }
    ...
    return getUserDetailsService().loadUserByUsername(token.getUsername());    
}

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/225467.html

標籤:Java

上一篇:Java基礎(一)

下一篇:shiro利用過期時間,解決用戶凍結踢出問題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more