(七) SpringBoot起飛之路-整合SpringSecurity(Mybatis、JDBC、記憶體)

BWH_Steven發表於2020-06-28

興趣的朋友可以去了解一下前五篇,你的贊就是對我最大的支援,感謝大家!

(一) SpringBoot起飛之路-HelloWorld

(二) SpringBoot起飛之路-入門原理分析

(三) SpringBoot起飛之路-YAML配置小結(入門必知必會)

(四) SpringBoot起飛之路-靜態資源處理

(五) SpringBoot起飛之路-Thymeleaf模板引擎

(六) SpringBoot起飛之路-整合JdbcTemplate-Druid-MyBatis

說明:

  • 這一篇的目的還是整合,也就是一個具體的實操體驗,原理性的沒涉及到,我本身也沒有深入研究過,就不獻醜了

  • SpringBoot 起飛之路 系列文章的原始碼,均同步上傳到 github 了,有需要的小夥伴,隨意去 down

  • 才疏學淺,就會點淺薄的知識,大家權當一篇工具文來看啦,不喜勿憤哈 ~

(一) 初識 Spring Security

(1) 引言

許可權以及安全問題,雖然並不是一個影響到程式、專案執行的必須條件,但是卻是開發中的一項重要考慮因素,例如某些資源我們不想被訪問到或者我們某些方法想要滿足指定身份才可以訪問,我們可以使用 AOP 或者過濾器來實現要求,但是實際上,如果程式碼涉及的邏輯比較多以後,程式碼是極其繁瑣,冗餘的,而有很多開發框架,例如 Spring Security,Shiro,已經為我們提供了這種功能,我們只需要知道如何正確配置以及使用它了

(2) 基本介紹

先看一下官網的介紹

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security是一個功能強大且高度可定製的身份驗證和訪問控制框架。它是保護基於spring的應用程式的實際標準。

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

Spring Security是一個框架,側重於為Java應用程式提供身份驗證和授權。與所有Spring專案一樣,Spring安全性的真正強大之處在於它很容易擴充套件以滿足定製需求

簡單的說,Spring Security 就是一個控制訪問許可權,強大且完善的框架

Web 應用的安全性包括使用者認證(Authentication)和使用者授權(Authorization)兩個部分,同時它們也是 Spring Security 提供的核心功能

使用者認證:使用者認證就是指這個使用者身份是否合法,一般我們的使用者認證就是通過校驗使用者名稱密碼,來判斷使用者身份的合法性,確定身份合法後,使用者就可以訪問該系統

使用者授權:如果不同的使用者需要有不同等級的許可權,就涉及到使用者授權,使用者授權就是對使用者能訪問的資源,所能執行的操作進行控制,根據不同使用者角色來劃分不同的許可權

(二) 靜態頁面匯入 And 環境搭建

(1) 關於靜態頁面

A:頁面介紹

頁面是我自己臨時弄得,有需要的朋友可以去我 GitHub:ideal-20 下載原始碼,簡單說明一下這個頁面

做一個靜態頁面如果嫌麻煩,也可以單純的自己建立一些簡單的頁面,寫幾個標題文字,能體現出當前是哪個頁面就好了

我程式碼中用的這些頁面,就是拿開源的前端元件框架進行了一點的美化,然後方便講解一些功能,頁面模板主要是配合 Thymeleaf

1、目錄結構

├── index.html                        // 首頁
├── images                            // 首頁圖片,僅美觀,無實際作用
├── css                               // 上線專案檔案,放在伺服器即可正常訪問
├── js                                // 專案截圖
├── views                             // 總子頁面資料夾,許可權驗證的關鍵頁面
│   ├── login.html					  // 自制登入頁面(用來替代 Spring Security 預設的 )
│   ├── L-A							  // L-A 子頁面資料夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-B							  // L-B 子頁面資料夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-C							  // L-C 子頁面資料夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html

B:匯入到專案

主要就是把基本一些連結,引入什麼的先替換成 Thymeleaf 的標籤格式,這裡語法用的不是特別多,即使對於 Thymeleaf 不是很熟悉也是很容易看懂的,當然如果仍然感覺有點吃力,可以單純的做成 html,將就一下,或者去看一下我以前的文章哈,裡面有關於 Thymeleaf 入門的講解

css、image、js 放到 resources --> static 下 ,views 和 index.html 放到 resources --> templates下

(2) 環境搭建

A:引入依賴

這一部分引入也好,初始化專案的時候,勾選好自動生成也好,只要依賴正常匯入了即可

  • 引入 Spring Security 模組
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

關鍵的依賴主要就是上面這個啟動器,但是還有一些就是常規或者補充的了,例如 web、thymeleaf、devtools

thymeleaf-extras-springsecurity5 這個後面講解中會提到,是用來配合 Thymeleaf 整合 Spring Security 的

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

B:頁面跳轉 Controller

因為我們用了模板,頁面的跳轉就需要交給 Controller 了,很簡單,首先是首頁的,當然關於頁面這個就無所謂了,我隨便跳轉到了我的部落格,接著還有一個登入頁面的跳轉

有一個小 Tip 需要提一下,因為 L-A、L-B、L-C 資料夾下都有3個頁面 a.html 、b.html 、c.html,所以可以利用 @PathVariable 寫一個較為通用的跳轉方法

@Controller
public class PageController {

    @RequestMapping({"/", "index"})
    public String index() {
        return "index";
    }

    @RequestMapping("/about")
    public String toAboutPage() {
        return "redirect:http://www.ideal-20.cn";
    }

    @RequestMapping("/toLoginPage")
    public String toLoginPage() {
        return "views/login";
    }

    @RequestMapping("/levelA/{name}")
    public String toLevelAPage(@PathVariable("name") String name) {
        return "views/L-A/" + name;
    }

    @RequestMapping("/levelB/{name}")
    public String toLevelBPage(@PathVariable("name") String name) {
        return "views/L-B/" + name;
    }

    @RequestMapping("/levelC/{name}")
    public String toLevelCPage(@PathVariable("name") String name) {
        return "views/L-C/" + name;
    }
}

C:環境搭建最終效果

  • 為了貼圖方便,我把頁面拉窄了一點
  • 首頁右上角應該為登入的連結,這裡是因為,我執行的是已經寫好的程式碼,不登入頁面例如 L-A-a 等模組就顯示不出來,所以拿一個定義好的管理員身份登陸了
  • 關於如何使其自動切換顯示登陸還是登入後資訊,在後面會講解

1、首頁

2、子頁面

L-A、L-B、L-C 下的 a.html 、b.html 、c.html 都是一樣的,只是文字有一點變化

3、登陸頁面

(三) 整合 Spring Security (記憶體中)

這一部分,為了簡化一些,容易理解一些,沒有從帶資料的場景出發(因為涉及程式碼少一些,所以講解會多一點),而是直接將一些身份等等寫死了,寫到了記憶體中,方便理解,接著會在下一個標題中給出含有資料庫的寫法(講解會少一些,重點只說一些與前一種的不同點)

(1) 配置授權內容

A:原始碼瞭解使用者授權方式

可以去官網看一下,官網有提供給我們一些樣例,其中有一個關於配置類的小樣例,也就是下面這個,我們通過這個例子,展開分析

https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#jc-custom-dsls

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .apply(customDsl())
                .flag(true)
                .and()
            ...;
    }
}

1、建立 config --> SecurityConfig 配置類

  • 建立一個配置類,像官網中一樣,繼承 WebSecurityConfigurerAdapter
  • 類上新增 @EnableWebSecurity 註解,代表開啟WebSecurity模式
  • 重寫 configure(HttpSecurity http) 方法
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

既然是重寫,那麼我們可以點進去,看一下父類中關於 configure(HttpSecurity http) 方法的原始碼註釋,它有很多有用的資訊

我摘選出這麼兩小段,第一段的意思就是 ,我們想要使用 HttpSecurity ,要通過重寫,不能通過 super 呼叫,否則會有覆蓋問題,第二段就是給出了一個預設的配置方式

* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their

* configuration. The default configuration is:
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();

2、按照原始碼的註釋分析

我們先按照剛才看到的註釋寫出來,首先能看到,它是支援一個鏈式呼叫的

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and().formLogin()
            .and().httpBasic();
}
  • 通過字面意思也很好理解,authorizeRequests 是關於請求授權的,所以要涉及到關於請求授權(允許指定身份使用者訪問不同許可權的資源)的問題就需要呼叫了

  • 其次,anyRequest().authenticated() 也就是說所有HTTP請求都需要被認證

  • 接著看,通過 and() 連線了一些新的內容,例如選擇表單登入還是 HTTPBasic 的方式(這裡認證的過程就是讓你輸入使用者名稱密碼,檢測你的身份,兩種方式表單或者那種彈窗)

Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸使用者名稱和密碼到服務端進行認證,通常需要配合HTTPS來保證資訊傳輸的安全

給大家演示一下:

  • 如果不指定一種認證方式 .and().formLogin() 或者 .and().httpBasic() 訪問任何頁面都會提示 403 禁止訪問的錯誤

  • 指定 .and().formLogin() 認證,彈出一個表單頁面(自帶的,和自己建立的沒關係)

  • 指定 .and().httpBasic(); 認證,彈出一個視窗進行 HTTPBasic 認證

B:自定製使用者授權

1、先看原始碼註釋

預設配置,設定了所有 HTTP 請求 都需要進行認證,所以我們在訪問首頁等的時候也會被攔截,但是實際情況下,有一些頁面是可以被任何人訪問的,例如首頁,或者自定義的登陸的等頁面,這時候需要用自己定義一些使用者授權的規則

在 WebSecurityConfigurerAdapter 的 formLogin() 註釋附近,又看到了一個有意思的內容

注:&quot 代表引號

* 		http
* 			.authorizeRequests(authorizeRequests ->
* 				authorizeRequests
* 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
* 			)

這就是我們想要找的,自定義的配置,通過一個一個 antMatchers 進行匹配,通過 hasRole 來規定其合法的身份,也就是說只有滿足這個身份的使用者才能訪問前面規定的路徑資源

Matchers 前面的 ant 字首代表著,他可以用 ant 風格的路徑表示式(舉例的時候就能看懂了)

萬用字元 說明
? 匹配任何單字元
* 匹配0或者任意數量的字元
** 匹配0或者更多的目錄

補充: 如果想用正規表示式的方式,可以用這個方法 .regexMatchers()

當然了,有很多情況下,你想要讓任何人都可以訪問某個路徑,例如首頁,permitAll() 方法 就可以達到這種效果,在這裡補充一些常用的方法

  • permitAll() :允許任何訪問

  • denyAll():拒絕所有訪問

  • anonymous():允許匿名使用者訪問

  • authenticated() :允許認證的使用者進行訪問

  • hasRole(String) :如果使用者具備給定角色(使用者組)的話,就允許訪問/

  • hasAnyRole(String…) :如果使用者具有給定角色(使用者組)中的一個的話,允許訪問.

  • rememberMe() :如果使用者是通過Remember-me功能認證的,就允許訪問

  • fullyAuthenticated():如果使用者是完整認證的話(不是通過Remember-me功能認證的),就允許訪問

  • hasAuthority(String):如果使用者具備給定許可權的話就允許訪問

  • hasAnyAuthority(String…) :如果使用者具備給定許可權中的某一個的話,就允許訪問

  • hasIpAddress(String) :如果請求來自給定ip地址的話,就允許訪問.

  • not() :對其他訪問結果求反

說明:hasAnyAuthority("ROLE_ADMIN") 和 hasRole("ADMIN") 的區別就是,後者會自動使用 它會自動使用 “ROLE_” 字首

2、我們來定製一下使用者授權

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests()
        	.antMatchers("/").permitAll()
        	.antMatchers("/levelA/**").hasRole("vip1")
        	.antMatchers("/levelB/**").hasRole("vip2")
        	.antMatchers("/levelC/**").hasRole("vip3")
        	.and().formLogin();
}

我們上面程式碼的意思就是,當訪問 /levelA/ /levelB/ /levelC/ 這三個路徑下面的任意檔案(這裡有 a/b/c.html)都需要認證,身份分別是對應 vip1、vip2、vip3,而其他頁面,就可以隨便訪問了

很顯然,雖然說規定了授權的內容,也就是哪些許可權的使用者,可以訪問哪些資源,但是我們由於並沒有配置使用者的資訊(合法的或者非法的),所以自然,前面的登入頁面,都是會直接報錯的,下面我們來分析一下,如何進行認證

(2) 配置認證內容

A:原始碼瞭解使用者認證方式

剛才的授權部分,我們重寫了 configure(HttpSecurity http) 方法,我們繼續看看重寫方法中,有沒有可能幫助我們驗證身份,進行使用者認證的方法,我們首先來看這個方法 configure(AuthenticationManagerBuilder auth)

先去看一下原始碼的註釋(此部分的格式,我稍微修改了一下,方便觀看):

這是其中他局舉的一個例子,其實這個就是我們想要的,看註釋也可以看出來,他就是用來在記憶體中啟用基於使用者名稱的身份驗證的

* protected void configure(AuthenticationManagerBuilder auth) {
*  auth
*  // enable in memory based authentication with a user named
*  // &quot;user&quot; and &quot;admin&quot;
*  		.inMemoryAuthentication()
*   		.withUser(&quot;user&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;).and()
*        	.withUser(&quot;admin&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;, &quot;ADMIN&quot;);
* }

照貓畫虎,我們也先這麼做

B:自定製使用者認證

程式碼如下:

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("admin")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2", "vip3")
            .and()
            .withUser("ideal-20")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2")
            .and()
            .withUser("jack")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1");
}

我們就是照著例子打的,但是,其中我們又加入了編碼的問題,它要求必須進行編碼,否則會報錯,官方推薦的是bcrypt加密方式,我們這裡就用這種,當然自己用常見的 MD5 等等都是可以的,可以自己寫一個工具類

到這裡,測試一下,實際上就可以按照身份的不同,從而擁有訪問不同路徑資源你的許可權了,主要的功能已經實現了,下面補充一些,更加友好的功能,例如登入登出按鈕的顯示,以及記住密碼等等

(3) 登出問題

1、登出配置

當然了,前面因為已經有很多配置了,所以可以通過 .and() 進行連線,例如 .and().xxx,或者像下面給出的,單獨再寫一個 http.xxx

@Override
protected void configure(HttpSecurity http) throws Exception {
   ......
    // 登出配置
	http.logout().logoutSuccessUrl("/")
}

上面短短一句的程式碼, logout() 代表開啟了登出的配置,logoutSuccessUrl("/"),代表登出成功後,返回的頁面,我們令其登出後回到首頁

前臺的頁面中,我已經給出了登出的按鈕的程式碼,當然這不是固定的,不同的 ui 框架,不同的模板引擎都是不一樣的,但是路徑是 /logout

<a class="item" th:href="@{/logout}">
  <i class="address card icon"></i> 登出
</a>

(4) 根據身份許可權顯示元件

A:登入、登出的顯示

還有這樣一種問題,右上角,未登入的時候,應該顯示登陸按鈕,登入後,應該顯示使用者資訊,以及登出等等,這一部分,主要是頁面這邊的問題

顯示的條件其實很簡單,就是判斷是否認證了,認證了就取出一些值,沒認證就顯示登陸

1、這時,我們就需要引入一個 Thymeleaf 配合 Spring Security 的一個依賴 (當然瞭如果是別的技術,就不一樣了)

地址如下:

https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

2、匯入名稱空間

引入這個檔案的目的,就是為了在頁面寫許可權判斷等相關的內容的時候可以有提示

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

3、修改導航欄邏輯

<!--登入登出-->
<div class="right menu">

  <!--如果未登入-->
  <div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLoginPage}">
      <i class="address card icon"></i> 登入
    </a>
  </div>

  <!--如果已登入-->
  <div sec:authorize="isAuthenticated()">
    <a class="item">
      <i class="address card icon"></i>
      使用者名稱:<span sec:authentication="principal.username"></span>
      <!--角色:<span sec:authentication="principal.authorities"></span>-->
    </a>
  </div>

  <div sec:authorize="isAuthenticated()">
    <a class="item" th:href="@{/logout}">
      <i class="address card icon"></i> 登出
    </a>
  </div>
</div>

B:元件皮膚的顯示

上面的程式碼,解決了導航欄的問題,但是例如我們首頁中,一些板塊,對於不同的使用者的顯示也是不同的嗎

正如上面的例子,沒有登入的使用者,是不能訪問了 /levelA/、 /levelB/、 /levelC/ 下面的任何檔案的,只有登入的使用者,根據許可權的大小,才能訪問某一個,或者所有

而我們首頁部分的三個皮膚就是用來顯示這三塊的連結,對於沒有足夠身份的人,實際上顯示這個皮膚就已經是多餘了,當然,你可以選擇顯示,但是如果想要根據身份顯示皮膚怎麼做呢?

關鍵就是在 div 中新增了這樣一句許可權的程式碼,沒有這個指定的身份,這個皮膚就不會顯示sec:authorize="hasRole('vip1')"

<div class="column" sec:authorize="hasRole('vip1')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelA/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/b}">L-A-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/c}">L-A-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelB/a}">L-B-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/b}">L-B-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/c}">L-B-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelC/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/b}">L-C-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/c}">L-C-c</a>
    </div>
  </div>
</div>

演示一下:

(5) 記住使用者

如果重啟瀏覽器後,就需要重新登入,對於一部分使用者來說,他們認為是麻煩的,所以很多網站登入時都提供記住使用者這種選項

1、一個簡單的配置就可以達到目的,這種情況下,預設的登陸頁面,就會多出一個記住使用者的單選框

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//記住使用者
    http.rememberMe();
}

2、但是如果,登陸頁面是自定義(下面講)的怎麼辦呢?,其實只要修改為如下配置即可,

//定製記住我的引數!
http.rememberMe().rememberMeParameter("remember");

上面的 remember 對應 input 中的 name 屬性值

<input type="checkbox" name="remember"/>
<label>記住密碼</label>

3、它做了哪些事情呢?

可以開啟頁面的控制檯看一下,實際上配置後,使用者選擇記住密碼後,會自動幫我們增加一個 cookie 叫做 remember-me,過期時間為 14 天,當登出的時候,這個 cookie 就會被刪除了

(6) 定製登入頁面

1、配置

自帶的登陸頁面確實,還是比較醜的,版本更低一些的,更是不美觀,如果想要使用自己定製的登陸頁面,可以加入下面的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	// 登陸表單提交請求
    http.formLogin()
	.usernameParameter("username")
	.passwordParameter("password")
	.loginPage("/toLoginPage")
	.loginProcessingUrl("/login")
}
  • .loginPage("/toLoginPage") 就是說,當你訪問一些需要使用者許可權認證的頁面時,就會發起這個請求,到你的登入頁面
  • .loginProcessingUrl("/login") 就是表單中,真正要提交請求的一個路徑
  • 其餘兩個就是關於使用者名稱和密碼的一個獲取,其值和頁面中使用者名稱密碼的 name 屬性值一致

2、頁面跳轉

前面我們就提過這個,回顧一下

@RequestMapping("/toLoginPage")
public String toLoginPage() {
    return "views/login";
}

3、自定義登入頁面的表單提交 action 設定

<form id="login" class="ui fluid form segment" th:action="@{/login}" method="post">
	......
</form>

(7) 關閉csrf

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//關閉csrf功能:跨站請求偽造,預設只能通過post方式提交logout請求
	http.csrf().disable();
}

(四) 整合 Spring Security (JDBC)

因為配置記憶體中的使用者還是相對簡單一些的,所以一些細節也都說了一下,基於上面的基礎,來看一下 如何用 JDBC 實現上面的功能,當然了這部分只能算補充,基本不會這麼用的,下面的整合 MyBatis 才是常用的()

(1) 建立表以及資料

這裡建立了三個欄位,使用者名稱,密碼,還有角色,插入資料的時候密碼是使用了 md5 加密(自己寫了一個工具類)

這裡更合理了一些,我把許可權定義為了普通使用者、普通管理員、超級管理員(自己設計都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '使用者名稱',
  `password` varchar(255) DEFAULT NULL COMMENT '密碼',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');

(2) 建立實體

我使用了 lombok,不過自己寫 get set 構造方法 也是一樣的

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer uid;
    private String username;
    private String password;
    private String roles;
}

(3) 配置授權內容

這部分沒什麼區別

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陸表單提交請求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //登出
            .and().logout().logoutSuccessUrl("/")
            //記住我
            .and().rememberMe().rememberMeParameter("remember")
            //關閉csrf功能:跨站請求偽造,預設只能通過post方式提交logout請求
            .and().csrf().disable();
}

(4) 配置認證內容

A:配置資料庫

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

server:
  port: 8082

B:具體配置

以幾個注意的地方:

  • 查詢語句都是通過 username 查詢

  • usersByUsernameQuery()方法裡的引數一定要有一個 true 的查詢結果,所以我直接在查詢語句中寫了一個 true

  • MD5 工具類,是我以前一個專案中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候簡單點

  • DataSource dataSource 要在前面注入進來(選擇 sql 的)

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery("select username,password,true from user where username = ?")
            .authoritiesByUsernameQuery("select username,roles from user where username = ?")
            .passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence rawPassword) {
                    return MD5Util.MD5EncodeUtf8((String) rawPassword);
                }

                @Override
                public boolean matches(CharSequence rawPassword, String encodedPassword) {
                    return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
                }
            });
}

C:MD5工具類

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具類
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大寫MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

D:修改頁面

到這裡,JDBC 的整合方式就成功了,至於前面的頁面只需要根據我們自己設計的許可權進行修改,別的地方和前面記憶體中的方式是一樣的

<div class="ui stackable three column grid">
  <div class="column" sec:authorize="hasAnyRole('USER','ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelA/a}">L-A-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/b}">L-A-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/c}">L-A-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasAnyRole('ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelB/a}">L-B-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/b}">L-B-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/c}">L-B-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasRole('SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelC/a}">L-C-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/b}">L-C-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/c}">L-C-c</a>
      </div>
    </div>
  </div>
  <!-- <div class="column"></div> -->
</div>

(五) 整合 Spring Security (MyBatis)

因為這部分內容是比較常用的,所以,我儘可能給的完善一些

(1) 新增依賴

像 lombok、commons-lang3 都不是必須的,都是可以使用原生的一些手段替代的,寫到那裡我會提的

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

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

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</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-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

(2) 建立表

和 JDBC 部分用同樣的表

三個欄位,使用者名稱,密碼,還有角色,插入資料的時候密碼是使用了 md5 加密(自己寫了一個工具類)

這裡更合理了一些,我把許可權定義為了普通使用者、普通管理員、超級管理員(自己設計都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '使用者名稱',
  `password` varchar(255) DEFAULT NULL COMMENT '密碼',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');

(3) 整合 MyBatis

在進行 Spring Security 的配置前,最好先把 MyBatis 先整合好,這樣等會只考慮 Spring Security 的問題就可以了

說明:這部分我儘可能簡化了,例如連線池就用預設的,如果這部分感覺還是有點問題,可以參考一下我前幾篇,關於整合 MyBatis 的文章

A:配置資料庫

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: cn.ideal.pojo

server:
  port: 8081

B:配置 Mapper 以及 XML

UserMapper

@Mapper
public interface UserMapper {
    User queryUserByUserName(String username);
}

mapper/UserMapper.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.ideal.mapper.UserMapper">
    <select id="queryUserByUserName" parameterType="String" resultType="cn.ideal.pojo.User">
         select * from user where username = #{username}
    </select>
</mapper>

這裡就不演示測試了,是沒有問題的

(4) 配置授權內容

這部分沒什麼好說的,和前面的都一樣,解釋在記憶體中配置使用者時已經詳細說過了

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陸表單提交請求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //登出
            .and().logout().logoutSuccessUrl("/")
            //記住我
            .and().rememberMe().rememberMeParameter("remember")
            //關閉csrf功能:跨站請求偽造,預設只能通過post方式提交logout請求
            .and().csrf().disable();
}

(5) 配置認證內容

A:建立 UserService

建立一個類,實現 UserDetailsService,其實主要就是為了 loadUserByname 方法,在這個類中,我們可以注入 mapper 等等,去查使用者,如果查不到,就還留在這個頁面,如果查到了,做出一定邏輯後(例如判空等等),就會把使用者資訊封裝到 Spring Security 自己的的 User類中去,Spring Security 拿前臺的資料和它比較,做出操作,例如認證成功或者錯誤

注意:

  • StringUtils 是 commons.lang3 下的,使用需要導包,我們用了一個判空功能,不想用的話,用原生的是一個道理,這不是重點
  • 注意區分自己的 User 和 Spring Security 的 User
@Service
public class UserService<T extends User> implements UserDetailsService{

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.queryUserByUserName(username);
        if (username == null){
            throw  new UsernameNotFoundException("使用者名稱不存在");
        }

        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        String role = user.getRoles();
        if (StringUtils.isNotBlank(role)){
            authorityList.add(new SimpleGrantedAuthority(role.trim()));
        }
        return new org.springframework.security.core.userdetails
            .User(user.getUsername(),user.getPassword(),authorityList);
    }
}

B:修改配置類

這裡也很熟悉,我們呼叫就可以呼叫 userDetailsService 了,同樣還需要指定編碼相關的內容 例項化 PasswordEncoder,就需要重寫 encode、 matches

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
        @Override
        public String encode(CharSequence rawPassword) {
            return MD5Util.MD5EncodeUtf8((String) rawPassword);
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
        }
    });
}

C:MD5 工具類補充

其實上面已經給出了,但是怕大家看起來不方便,這裡再貼一下

MD5 工具類,是我以前一個專案中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候可以簡單點

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具類
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大寫MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

相關文章