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 回傳的「系統首頁」字串。
接著請在 pom.xml 檔案添加 Spring Security 的依賴,並且重新整理,將函式庫下載到程式專案中。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
即便現在我們尚未做任何設定,但 Spring Security 預設將自動保護所有 API。此時重新啟動程式,讀者再前往該網址,便會看見內建的登入畫面。
雖然在前後端分離的系統中,並不會使用這種登入畫面,然而在初學 Spring Security 時,它是用來測試認證與授權的好管道。
Spring Security 在每次程式啟動時,都會產生一個叫「user」的帳號。而密碼為隨機值,在 console 中可找到如下的訊息:
Using generated security password: 8698d55e-33dc-461c-80e4-a8e364b23a6c
這時在登入畫面輸入該組帳密,就又能看見剛剛的「系統首頁」文字了。
而 Spring Security 也有內建登出畫面,網址為「http://localhost:8080/logout」。
在本文第三節登入測試帳號後,可透過此畫面來登出,以切換不同帳號。
三、實作認證功能
(一)準備測試帳號
實務開發中,都是在資料庫儲存使用者的帳密。然而接下來的範例程式,若馬上引進資料庫,那我們就勢必得先準備資料庫的服務、設計使用者的資料表欄位,並在 Spring Boot 中串接。
為避免讀者在學習上分心,以及文章篇幅過長,筆者在本文會使用 Spring Security 提供的「in-memory user」功能,快速建立簡易的測試帳號。
待本文結束,對 Spring Security 的認證與授權有概念後,在第 17.2 課會抽換成使用資料庫來儲存帳密。
以下建立了一個配置類別,並冠上 @EnableWebSecurity 注解。有關 Spring Security 的各項設定,都能在這裡透過程式碼來自定義。
上面建立了 InMemoryUserDetailsManager 元件,它會被 Spring Security 讀取。其用途顧名思義就是在記憶體中管理帳號,建構子接收了 UserDetails 型態的物件。
至於建立帳號的方式,則是呼叫 User 類別提供的靜態方法,傳入帳號與密碼。此處建立了三個使用者:
帳號 | 密碼 |
---|---|
user1 | 111 |
user2 | 222 |
user3 | 333 |
重新啟動程式後,讀者可在瀏覽器分別用這些帳號前往「http://localhost:8080/home」。若成功,則代表這些測試帳號是有用的。
(二)密碼加密
設定密碼時,在前面加上了 {noop} 的字串。原因是配置 in-memory user 的密碼時,需指定密碼的加密演算法。以下舉例其中幾項:
前綴 | 演算法 | 密碼原文 | 參數寫法 |
---|---|---|---|
{noop} | 不加密 | 123 | {noop}123 |
{bcrypt} | BCrypt | 456 | {bcrypt}$2a$12$YowIkLKzGwPjMt6jtCkvDuCA7Vxb/81pQaJvGgtbKjMgVYtMs2DKK |
{sha256} | SHA256 | password | {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
偷懶看太快沒把後面四個方法設為True, 結果在後面的學習時一直出錯, 找了半天的錯.
回覆刪除OK,那我把這四個方法也放在文中的範例程式中,就不寫說略過了~
刪除謝謝你詳細的說明ㄇ
回覆刪除