【Spring Boot】第17.1課-初探 Spring Security 的認證與授權 #2024 年更新


https://unsplash.com/photos/LmyPLbbUWhA

Spring Security 是一套框架,它能幫助我們開發有關認證與授權等有關安全管理的功能。

本文會進行 Spring Security 的初始配置。首先準備簡單的 RESTful API,並搭配 in-memory 的測試帳號,在瀏覽器登入存取 API。藉此讓讀者建立認證的概念。

至於授權的部份,則會設計帳號的權限,並定義 API 要開放給哪些權限。

一、前言

Spring Security 是一套可以保護 Spring Boot 應用程式的框架。它扮演著如同保全的角色,能夠管控人員的進出,以及誰可以去什麼地方。

那麼要保護什麼呢?假設我們有一個校務系統,已知有「學生」和「老師」兩種角色。使用情境中,老師可以建立課程資料、給學生打分數;而學生可以選課、給予課程回饋。當然,這些操作都要先登入系統才能進行。

基於這個情境,對學生而言,課程和分數的資料就只能查看,不能建立、修改或刪除。對老師而言,課程回饋只能查看,不能修改。

所以說,Spring Security 這套安全管理的框架,就是要保護服務、資料等各項資源,不會被任意存取。

Spring Security 提供了兩大功能,分別是認證(authentication)與授權(authorization)。認證就相當於前面提到的登入,向系統表示自己是個擁有帳號的使用者。而授權則是系統允許該使用者存取某服務,也就是存取 API。


二、程式專案概觀

本系列文章的範例程式,使用的 Spring Boot 版本為 3.2.4。

請建立 Controller,準備一支簡單的 API,做為測試用途。

啟動程式後,讀者可在瀏覽器上前往「http://localhost:8080/home」,確認有出現該 API 回傳的「系統首頁」字串。

spring-security-call-api-in-browser

接著請在 pom.xml 檔案添加 Spring Security 的依賴,並且重新整理,將函式庫下載到程式專案中。

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

即便現在我們尚未做任何設定,但 Spring Security 預設將自動保護所有 API。此時重新啟動程式,讀者再前往該網址,便會看見內建的登入畫面。

spring-security-login-page

雖然在前後端分離的系統中,並不會使用這種登入畫面,然而在初學 Spring Security 時,它是用來測試認證與授權的好管道。

Spring Security 在每次程式啟動時,都會產生一個叫「user」的帳號。而密碼為隨機值,在 console 中可找到如下的訊息:

Using generated security password: 8698d55e-33dc-461c-80e4-a8e364b23a6c

這時在登入畫面輸入該組帳密,就又能看見剛剛的「系統首頁」文字了。

而 Spring Security 也有內建登出畫面,網址為「http://localhost:8080/logout」。

spring-security-logout-page

在本文第三節登入測試帳號後,可透過此畫面來登出,以切換不同帳號。

三、實作認證功能

(一)準備測試帳號

實務開發中,都是在資料庫儲存使用者的帳密。然而接下來的範例程式,若馬上引進資料庫,那我們就勢必得先準備資料庫的服務、設計使用者的資料表欄位,並在 Spring Boot 中串接。

為避免讀者在學習上分心,以及文章篇幅過長,筆者在本文會使用 Spring Security 提供的「in-memory user」功能,快速建立簡易的測試帳號。

待本文結束,對 Spring Security 的認證與授權有概念後,在第 17.2 課會抽換成使用資料庫來儲存帳密。

以下建立了一個配置類別,並冠上 @EnableWebSecurity 注解。有關 Spring Security 的各項設定,都能在這裡透過程式碼來自定義。

上面建立了 InMemoryUserDetailsManager 元件,它會被 Spring Security 讀取。其用途顧名思義就是在記憶體中管理帳號,建構子接收了 UserDetails 型態的物件。

至於建立帳號的方式,則是呼叫 User 類別提供的靜態方法,傳入帳號與密碼。此處建立了三個使用者:

帳號密碼
user1111
user2222
user3333

重新啟動程式後,讀者可在瀏覽器分別用這些帳號前往「http://localhost:8080/home」。若成功,則代表這些測試帳號是有用的。

(二)密碼加密

設定密碼時,在前面加上了 {noop} 的字串。原因是配置 in-memory user 的密碼時,需指定密碼的加密演算法。以下舉例其中幾項:

前綴演算法密碼原文參數寫法
{noop}不加密123{noop}123
{bcrypt}BCrypt456{bcrypt}$2a$12$YowIkLKzGwPjMt6jtCkvDuCA7Vxb/81pQaJvGgtbKjMgVYtMs2DKK
{sha256}SHA256password{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

在實務上,使用資料庫儲存密碼時,我們會先將密碼的原文加密後才儲存。其用意是為了保護客戶的資料,避免資料庫內容外洩,或者員工監守自盜。在範例程式中,指定密碼的加密演算法,便是為了模擬出這個情境。

為了方便示範,將以未加密的密碼來儲存。其中「noop」是「No Operation」的意思。若讀者想試用其他加密演算法,這裡提供 BCrypt 演算法的轉換與驗證工具。

四、實作授權功能

(一)準備測試用 API

上一節,我們做到建立測試帳號,並通過登入畫面的「認證」。不過 Spring Security 還有另一個環節,那就是「授權」。

授權指的是系統允許使用者存取某項服務。這背後意味著,一個人即便能夠登入,也不代表能使用所有的功能。正如同本文第一節所舉的例子,學生不能編輯課程資料,老師也不能選課。

在 Controller 中,請讀者準備其他 API。連同先前的「系統首頁」,現在共有 5 支 API。

在這個例子中,我們希望「註冊畫面」不需登入即可存取;「系統首頁」需登入才能存取;「修課清單」只有學生能存取;「課程回饋」只有老師能存取;「使用者列表」只有管理員能存取。

(二)添加權限

接著回到 Spring Security 的配置類別,為測試帳號設計「權限」(authority)。

呼叫 authorities 方法,可以為 in-memory user 添加一至多個權限。至於權限的名稱,則是由我們自己取名,包含「學生」、「老師」與「管理員」。其中「user3」這個帳號同時具有老師與管理員權限。

(三)授權規則

設計好帳號的權限後,接下來要對 Controller 的 API 進行保護,定義它們要開放給具有哪些權限的人存取。

請在 Spring Security 的配置類別,建立 SecurityFilterChain 元件。

Spring Security 會將 HttpSecurity 物件注入到建立元件的方法中。透過該物件的一系列方法呼叫,我們能站在安全管理的角度,自定義 request 到達後端時的應對方式。

一開始的 formLogin 方法,是啟用先前的登入畫面,便於我們繼續進行測試。

接下來的 authorizeHttpRequests 方法,其用途是設定要如何進行授權。定義時,需提供「API」與「授權規則」這兩個部份。

呼叫 requestMatchers 方法,可傳入 API 路徑與 HTTP 方法;呼叫 anyRequests 方法,代表要對「其餘」的 API 做設定。這是有先後順序之分的,就像 Java 語言的「if → else if → else」,是由上而下逐一判斷。

提供完 API 後,接著要定義授權規則。以下舉例幾個可用的方法:

方法名稱意義
permitAll不必登入就能存取。
hasAuthority需具備某一個權限才能存取。
hasAnyAuthority只要具備任一個權限就能存取。
authenticated需登入才能存取。

除了上表列出的方法,讀者也可呼叫 access 方法,搭配 Spring 表達式(Spring Expression Language,SpEL)實現複雜的規則。下面的例子,是只有兼具管理員與老師權限的帳號才能存取 API,用到了「AND」邏輯的語法。

.access(new WebExpressionAuthorizationManager("hasAuthority('ADMIN') AND hasAuthority('TEACHER')"))

撰寫完設定後,讀者可重新啟動程式,在瀏覽器分別使用學生、老師與管理員的帳號存取這 5 支 API,確認是否符合規則。

最後補充,定義 API 授權規則時,路徑的部份除了精確地逐一寫出來,也能透過萬用字元來「模糊匹配」。下表是用法與範例:

萬用字元意義範例寫法適用不適用
*0 到多個字元/courses/*/courses、/courses/123/courses/123/draft
**0 到多個階層/courses/**任何「/courses」開頭的路徑-
?1 個字元/courses/?/courses/1/courses/123、/courses
?* 或 *?1 到多個字元/courses/?*/courses/1、/courses/123/courses

到目前為止,測試帳號都是使用 in-memory user。下一篇將特別實作自定義的認證方式,抽換成使用資料庫來儲存帳號、密碼與權限。

本文的完成專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch17-1-v2

上一篇:【Spring Boot】第16課-使用 Filter 擷取請求與回應

下一篇:【Spring Boot】第17.2課-在 Spring Security 整合資料庫進行認證

留言

  1. 偷懶看太快沒把後面四個方法設為True, 結果在後面的學習時一直出錯, 找了半天的錯.

    回覆刪除
    回覆
    1. OK,那我把這四個方法也放在文中的範例程式中,就不寫說略過了~

      刪除

張貼留言