SpringBoot第二十三篇:安全性之Spring Security

追夢1819發表於2019-08-14

作者:追夢1819
原文:https://www.cnblogs.com/yanfei1819/p/11350255.html
版權宣告:本文為博主原創文章,轉載請附上博文連結!

引言

  系統的安全的重要性人人皆知,其也成為評判系統的重要標準。

  Spring Security 是基於 Spring 的安全框架。傳統的 Spring Security 框架需要配置大量的 xml 檔案。而 SpringBoot 的出現,使其簡單、方便、上手快。


版本資訊

  • JDK:1.8
  • SpringBoot :2.1.6.RELEASE
  • maven:3.3.9
  • Thymelaf:2.1.4.RELEASE
  • IDEA:2019.1.1


資料庫設計

  系統的底層資料庫,設計的表格是五張:使用者表、角色表、使用者角色對應表、許可權表、角色許可權對應表。使用者與角色對應,角色與許可權對應,從而使使用者與許可權間接對應。同時考慮到了擴充套件性和健壯性。這就是底層設計的核心思想。

  上述的底層設計基本上是千篇一律的,沒啥可以講的。不是本文的重點。本文的重點是通過專案的需求來演示完整的功能實現。

搭建環境

  為了便於專案的演示,本章的例項用 SpringBoot + thymelaf 構建一個簡單的頁面。同時,由於功能點比較多,並保證能夠同時講解晚上功能,以下將分階段詳解各個功能點。

第一階段:

第一步,建立專案:

SpringBoot第二十三篇:安全性之Spring Security

對以上的專案目錄說明:

com.yanfei1819.security.config.SecurityConfig:security配置

com.yanfei1819.security.web.controller.IndexController:測試介面

com.yanfei1819.security.SecurityApplication:啟動類

src\main\resources\templates\index.html:首頁

src\main\resources\templates\springboot-1.html:同以下三個頁面都是選單的詳細頁,用來模擬選單

src\main\resources\templates\springboot-2.html:

src\main\resources\templates\work-1.html:

src\main\resources\templates\work-2.html:

src\main\resources\application.properties:主配置檔案

第二步,引入 maven 依賴:

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

注意,在引入 security 依賴後,如果沒有做配置,它會將所有的請求攔截,並跳轉到自定義的登入介面(埠號被定義為8085)。如下圖:

SpringBoot第二十三篇:安全性之Spring Security

第三步,建立配置類 SecurityConfig ,並繼承 WebSecurityConfigurerAdapter:

package com.yanfei1819.security.config;

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.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * Created by 追夢1819 on 2019-06-27.
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

        // 定製授權規則
        http.authorizeRequests().antMatchers("/").permitAll(). // 所有角色可訪問
                antMatchers("/springboot/**").hasAnyRole("admin","test"). // 只有xx角色才能訪問
                antMatchers("/work/**").hasRole("admin"); // 只有xx角色才能訪問
    }
}

定義授權規則,需要重寫 configure(HttpSecurity http) 方法。該配置類的寫法,可以參照 Spring Security官網。該方法中是定製授權規則。

hasAuthority([auth]):等同於hasRole
hasAnyAuthority([auth1,auth2]):等同於hasAnyRole
hasRole([role]):當前使用者是否擁有指定角色。
hasAnyRole([role1,role2]):多個角色是一個以逗號進行分隔的字串。如果當前使用者擁有指定角色中的任意一個則返回true
Principle:代表當前使用者的principle物件
authentication:直接從SecurityContext獲取的當前Authentication物件
permitAll():總是返回true,表示允許所有的
denyAll():總是返回false,表示拒絕所有的
isAnonymous():當前使用者是否是一個匿名使用者
isAuthenticated():表示當前使用者是否已經登入認證成功了
isRememberMe():表示當前使用者是否是通過Remember-Me自動登入的
isFullyAuthenticated():如果當前使用者既不是一個匿名使用者,同時又不是通過Remember-Me自動登入的,則返回true
hasPermission():當前使用者是否擁有指定許可權

第四步,定義介面:

package com.yanfei1819.security.web.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * Created by 追夢1819 on 2019-06-27.
 */
@Controller
public class IndexController {
    @GetMapping("/")
    public String index(){
        return "index";
    }
    @GetMapping("/springboot/{id}")
    public String springbootById(@PathVariable int id){
        return "springboot-"+id;
    }
    @GetMapping("/work/{id}")
    public String work(@PathVariable int id){
        return "work-"+id;
    }
}

第五步,編寫頁面 index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>我是首頁</h1>
<di>
    <h3>追夢1819的部落格系列</h3>
    <ul>
        <li><a th:href="@{/springboot/1}">SpringBoot 第一章</a></li>
        <li><a th:href="@{/springboot/2}">SpringBoot 第二章</a></li>
        <li><a th:href="@{/work/1}">work 第一章</a></li>
        <li><a th:href="@{/work/2}">work 第二章</a></li>
    </ul>
</di>
</body>
</html>

SpringBoot-1.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>SpringBoot-1</h1>

</body>
</html>

另外的 springboot-2.html、work-1.html、work-2.html 與以上類似,此不再贅述。

第六步,啟動類是:

package com.yanfei1819.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }
}

最後,啟動專案。直接訪問 http://localhost:8085/ ,進入首頁:

SpringBoot第二十三篇:安全性之Spring Security

點選其中任意一個連結:

SpringBoot第二十三篇:安全性之Spring Security

可以看到是沒有許可權訪問的。因此,上述的 security 配置成功。


第二階段:

  開啟自動配置的登入功能,也就是在 SecurityConfig 配置類中加入以下程式碼:

        http.formLogin();

該功能的作用是,進入首頁後,點選選單,如果沒有許可權,則跳轉到登入頁。


第三階段:

下面闡述設定登入賬號和密碼。

在 SecurityConfig 配置類重寫 configure(AuthenticationManagerBuilder auth) 方法:

    // 定義認證規則
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123456").roles("admin", "test")
                .and().withUser("test").password("123456").roles("test");
    }

注意,此處會有一個問題。如以上地址認證規則,在使用配置的賬號登入時會報錯:

SpringBoot第二十三篇:安全性之Spring Security

這是由於在 Spring Security5.0 版本後,新增了加密方式,改變了密碼的格式。

官網中有描述:

The general format for a password is:


Such that `id` is an identifier used to look up which `PasswordEncoder` should be used and `encodedPassword` is the original encoded password for the selected `PasswordEncoder`. The `id` must be at the beginning of the password, start with `{` and end with `}`. If the `id` cannot be found, the `id` will be null. For example, the following might be a list of passwords encoded using different `id`. All of the original passwords are "password".

{bcrypt}$2a\(10\)dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 1
{noop}password 2
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 3
{scrypt}$e0801\(8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==\)OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= 4
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 5
```

1 The first password would have a PasswordEncoder id of bcrypt and encodedPassword of $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG. When matching it would delegate to BCryptPasswordEncoder
2 The second password would have a PasswordEncoder id of noop and encodedPassword of password. When matching it would delegate to NoOpPasswordEncoder
3 The third password would have a PasswordEncoder id of pbkdf2 and encodedPassword of 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc. When matching it would delegate to Pbkdf2PasswordEncoder
4 The fourth password would have a PasswordEncoder id of scrypt and encodedPassword of $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=When matching it would delegate to SCryptPasswordEncoder
5 The final password would have a PasswordEncoder id of sha256 and encodedPassword of 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0. When matching it would delegate to StandardPasswordEncoder

上面這段話的解釋了為什麼會報錯:There is no PasswordEncoder mapped for the id "null",同時給出瞭解決方案。也就是 configure(AuthenticationManagerBuilder auth) 方法修改為:

    // 定義認證規則
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("admin","test")
                .and().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("test").password(new BCryptPasswordEncoder().encode("123456")).roles("test");
    }

修改後重啟專案,登入可正常訪問:

SpringBoot第二十三篇:安全性之Spring Security

訪問結果是:賬號 admin/123456 可以訪問所有選單:SpringBoot 第一章、SpringBoot 第二章、work 第一章、work 第二章,賬號 test/123456 只能訪問 SpringBoot 第一章、SpringBoot 第二章。


第四階段:

  開啟自動配置的登出功能,並清除 session,在配置類 SecurityConfig 中的 configure(HttpSecurity http) 方法中新增:

http.logout();

然後在首頁 index.html 中新增一個登出按鈕:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>我是首頁</h1>
<di>
    <h3>追夢1819的部落格系列</h3>
    <ul>
        <li><a th:href="@{/springboot/1}">SpringBoot 第一章</a></li>
        <li><a th:href="@{/springboot/2}">SpringBoot 第二章</a></li>
        <li><a th:href="@{/work/1}">work 第一章</a></li>
        <li><a th:href="@{/work/2}">work 第二章</a></li>
    </ul>
</di>
<div>
    <form method="post" th:action="@{/logout}"> 
        <input type="submit" value="logout">
    </form>
</div>
</body>
</html>

啟動專案,進入首頁,點選 【logout】,會跳轉到登入介面,同時連結中帶了引數 ?logout

SpringBoot第二十三篇:安全性之Spring Security

當然,也可以跳轉到定製的頁面,只要將屬性修改為:

        http.logout()  // 退出並清除session
                .logoutSuccessUrl("/");


第五階段:

  以上的功能基本都滿足了我們專案中的需求。不過只講述了功能點。下面我們將闡述如何在頁面展示以上功能。

  首先,我們必須引入以下依賴,以便使用 sec:authentication和sec:authorize 屬性。

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>

注意: 此處有版本衝突問題,以上的演示的 SpringBoot 用的版本都是 2.1.6.RELEASE。但是在此如果繼續使用該版本,則無法使用以上依賴中的 sec:authentication和sec:authorize 屬性。作者在做此演示時,對 SpringBoot 版本作了降級處理,版本為 2.1.4.RELEASE。而舊的版本有很多不同的地方,例如舊版本的登入介面是:

SpringBoot第二十三篇:安全性之Spring Security

此處需要特別注意!


引入上述依賴後,我們將首頁進行改造:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>我是首頁</h1>
<!--沒有登入-->
<div sec:authorize="!isAuthenticated()">
    <a th:href="@{/login}">login</a>
</div>
<!--已登入-->
<div sec:authorize="isAuthenticated()">
    <div>
        <form method="post" th:action="@{/logout}">
            <input type="submit" value="logout">
        </form>
    </div>
    登陸者:<span sec:authentication="name"></span>
    登陸者角色:<span sec:authentication="principal.authorities"></span>
</div>
<div>
    <h3>追夢1819的部落格系列</h3>
    <ul>
        <!-- 通過角色判斷是否展示-->
        <div sec:authorize="hasRole('admin')">
            <li><a th:href="@{/springboot/1}">SpringBoot 第一章</a></li>
            <li><a th:href="@{/springboot/2}">SpringBoot 第二章</a></li>
        </div>
        <div sec:authorize="hasRole('test')">
            <li><a th:href="@{/work/1}">work 第一章</a></li>
            <li><a th:href="@{/work/2}">work 第二章</a></li>
        </div>
    </ul>
</div>
</body>
</html>

啟動專案,分別用不登入、 admin/123456、test/123456 登入,檢視效果:

SpringBoot第二十三篇:安全性之Spring Security

SpringBoot第二十三篇:安全性之Spring Security

SpringBoot第二十三篇:安全性之Spring Security


第六階段:

  最後我們講解一個常用的功能,就是登陸的記住功能,配置很簡單,在配置類 SecurityConfig 中的 configure(HttpSecurity http) 方法中新增即可:

        http.rememberMe() // 記住功能
                .rememberMeParameter("remember") //自定義rememberMe的name值,預設remember-Me
                .tokenValiditySeconds(10); // 記住時間

進入登陸介面:

SpringBoot第二十三篇:安全性之Spring Security

新增該方法後,登入頁會出現記住功能的核取方塊。


總結

  還有很多詳細的功能。由於篇幅所限,本章中不做一一細解。如果想了解更多,作者給讀者的建議是,可以多看看 WebSecurityConfigurerAdapterHttpSecurityAuthenticationManagerBuilder 等類的原始碼,比較簡單,很容易上手。另外就是其文件非常的詳細、清晰(文件詳細是Spring的一個特色)。可以讓大家先感受一下 Spring 原始碼文件的強大:

SpringBoot第二十三篇:安全性之Spring Security

功能描述、示例一應俱全。


結語

  其實對以上功能的瞭解,不算很難。但是這篇部落格前後寫了六七個小時。作者看了翻閱了不少的資料,通讀對應的官方文件,聽了一些比較好的課程,然後自己一一校驗,思考,排版,解決版本衝突等。最終是希望讓讀者能夠看到一篇準確、美觀、較詳細的資料,不至於陷入網上的亂七八糟的資料中無法自拔。


參考

  1. Spring Security Reference
  2. Hello Spring Security with Boot
  3. WebSecurityConfigurerAdapterHttpSecurityAuthenticationManagerBuilder 等類的原始碼



SpringBoot第二十三篇:安全性之Spring Security

相關文章