Spring Security 是一個框架,它能幫助我們開發有關認證(authentication)與授權(authorization)等有關安全管理的功能。Day 22 ~ 26 為一個小系列,而本文將認識 Spring Security 的初始設置。
首先會在範例專案中準備使用者的資料 model、REST API,以及簡易業務邏輯。接著再設置授權規則,決定開放 API 的方式。透過適當地設置規則,可以達到保護 API,不被使用者任意存取的目的。
此篇在 2024 年於「【Spring Boot】第17.1課-初探 Spring Security 的認證與授權」文章更新。
為了實作人的權限管控,系統中理所當然要有「使用者」這項元素。本節主要是準備好使用者相關的程式碼,在後面這幾篇文章,都會以此為基礎。筆者不一定會仔細說明前面文章已實作過的東西。
以下簡單地設計了名為 AppUser
的類別,用來描述使用者。
public class AppUser {
private String id;
private String email;
private String password;
private UserAuthority authority;
public static AppUser getTestAdminUser() {
var user = new AppUser();
user.id = "100";
user.email = "vincent@gmail.com";
user.password = "123456";
user.authority = UserAuthority.ADMIN;
return user;
}
public static AppUser getTestNormalUser() {
var user = new AppUser();
user.id = "101";
user.email = "dora@gmail.com";
user.password = "654321";
user.authority = UserAuthority.NORMAL;
return user;
}
public static AppUser getTestGuestUser() {
var user = new AppUser();
user.id = "000";
user.email = "ivy@gmail.com";
user.password = "000000";
user.authority = UserAuthority.GUEST;
return user;
}
// getter, setter ...
}
其中 UserAuthority
這個列舉類別,代表使用者的身份。在此分為「管理員」、「一般」與「訪客」。
public enum UserAuthority {
ADMIN, NORMAL, GUEST;
}
AppUser
類別提供三個範例資料。
||帳號||密碼||身份||
|-|-|-|
|vincent@gmail.com|123456|管理員|
|dora@gmail.com|654321|一般|
|ivy@gmail.com|000000|訪客|
以下的 controller 提供了三個 API,用途分別如下。
POST /users
:建立使用者GET /users
:列出所有使用者GET /users/{id}
:取得一筆使用者資料@RestController
public class DemoController {
@Autowired
private UserRepository userRepository;
@PostMapping("/users")
public ResponseEntity<Void> createUser(@RequestBody AppUser user) {
userRepository.insert(user);
return ResponseEntity.ok().build();
}
@GetMapping("/users")
public ResponseEntity<List<AppUser>> getAllUsers() {
var users = userRepository.findAll();
return ResponseEntity.ok(users);
}
@GetMapping("/users/{id}")
public ResponseEntity<AppUser> getUser(@PathVariable String id) {
var user = userRepository.findById(id);
return user != null
? ResponseEntity.ok(user)
: ResponseEntity.notFound().build();
}
}
由於本文的範例專案沒有串接真實的資料庫,故以 HashMap
代替,並用自定義的 UserRepository
類別封裝起來。此外還預先添加數筆使用者資料。
@Repository
public class UserRepository {
private final Map<String, AppUser> idToUserMap = new HashMap<>();
@PostConstruct
private void init() {
var adminUser = AppUser.getTestAdminUser();
var normalUser = AppUser.getTestNormalUser();
var guestUser = AppUser.getTestGuestUser();
idToUserMap.put(adminUser.getId(), adminUser);
idToUserMap.put(normalUser.getId(), normalUser);
idToUserMap.put(guestUser.getId(), guestUser);
}
public AppUser findById(String id) {
return idToUserMap.get(id);
}
public AppUser findByEmail(String email) {
return idToUserMap.values().stream()
.filter(u -> u.getEmail().equals(email))
.findFirst()
.orElse(null);
}
public List<AppUser> findAll() {
return new ArrayList<>(idToUserMap.values());
}
public void insert(AppUser user) {
idToUserMap.put(user.getId(), user);
}
}
讀者可自行先試用看看這些 API。
為了使用 Spring Security 的 library,請在 pom.xml 檔案添加依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加依賴後,讀者會發現所有的 API 突然不能用了,使用時都得到 HTTP 401 的狀態碼(Unauthorized)。這是因為 Spring Security 預設會將所有的 API 保護起來。
添加 library 後,請建立一個名為 SecurityConfig
的 config 類別,並冠上 @EnableWebSecurity
標記。我們將在此配置 API 的授權規則。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// TODO
return http.build();
}
}
Spring Boot 3 版本的配置程式寫法,與以前版本稍微不同。上面建立了 SecurityFilterChain
的元件,它會被 Spring Security 讀取,套用裡面的設定。而接下來就是要做「設定」這件事。
在建立 SecurityFilterChain
的過程中,透過呼叫 HttpSecurity
的方法,可幫助我們配置 API 的授權規則。
筆者將規則設計為:
以下的配置程式,會在 HttpSecurity.authorizeHttpRequests
方法中進行 functional programming。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(registry ->
registry
.requestMatchers(HttpMethod.POST, "/users").permitAll()
.requestMatchers(HttpMethod.GET, "/users/?*").permitAll()
.anyRequest().authenticated()
)
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}
以上的範例程式,有幾點需要說明。
設置一組授權規則時,會使用 requestMatcher
方法,傳入 HTTP 方法與 API 路徑。
接著搭配 permitAll
方法,可將授權方式設置為「允許所有人」。若搭配 authenticated
方法,則設置為「需通過身份認證」。本文只使用這兩種,下一篇文章會介紹第三種。
另外這邊還停用了有關 CSRF 的保護機制。若維持預設啟用的狀態,當存取 API 時,除了 HTTP 的 GET 方法,其他如 POST 都會被阻擋。
關於
/users/?*
的?
與*
符號,會在下面說明。
在上面的範例程式中,我們想設置 GET /users/{id}
這個 API 的授權規則,因而在 requestMatcher
方法的參數寫了 ?*
。其實這是一種「萬用字元」,該方法除了傳入精確的路徑,也能傳入格式(pattern)來做模糊匹配。
以下是萬用字元的用法與範例。
||符號||意義||範例格式||適用||不適用||
|-|-|-|-|-|
|*
|0 到多個字元|/posts/*
|/posts
、/posts/123
|/posts/123/draft
|
|**
|0 到多個階層|/posts/**
|任何 /posts
開頭的路徑|-|
|?
|1 個字元|/posts/?
|/posts/1
|/posts/123
、/posts
|
若 pattern 為
/posts/?*
,則/posts/123
就有符合了。
第一節的 AppUser
範例使用者,其密碼是以明文(plain text)來儲存的。然而這並不是適當的做法,應該以密文(cipher text)來儲存,避免開發人員盜用的疑慮。
為此,讓我們先將 Spring Security 內建的密碼加密工具 BCryptPasswordEncoder
,建立成元件。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// ...
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
接著將其注入到 UserRepository
,於儲存使用者的程式碼中,呼叫 encode
方法來加密。
@Repository
public class UserRepository {
private final Map<String, AppUser> idToUserMap = new HashMap<>();
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
private void init() {
var adminUser = AppUser.getTestAdminUser();
var normalUser = AppUser.getTestNormalUser();
var guestUser = AppUser.getTestGuestUser();
Stream.of(adminUser, normalUser, guestUser)
.forEach(u -> {
var encodedPwd = passwordEncoder.encode(u.getPassword());
u.setPassword(encodedPwd);
});
idToUserMap.put(adminUser.getId(), adminUser);
idToUserMap.put(normalUser.getId(), normalUser);
idToUserMap.put(guestUser.getId(), guestUser);
}
public void insert(AppUser user) {
var encodedPwd = passwordEncoder.encode(user.getPassword());
user.setPassword(encodedPwd);
idToUserMap.put(user.getId(), user);
}
// ...
}
前面已經配置好這三支 API 的授權規則了,讓我們試著用 Postman 存取看看。
首先是 GET /users/{id}
,匹配到的規則為 /users/?*
。授權方式為 permitAll
,所以可以正常存取。
接著是 POST /users
,授權方式為 permitAll
,同樣也可以正常存取。
最後是 GET /users
,由於需要通過身份認證,目前無法存取。得到 HTTP 403(Forbidden)的狀態碼。
那麼要如何處理身份認證的問題呢?筆者會在明天的文章進行實作。
本文的完成專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch17-1
Ref:Securing a Web Application(官方文件)
今日文章到此結束!
最後推廣一下自己的部落格,我是「新手工程師的程式教室」的作者,請多指教