簡介
Spring Security 是 Java 生態系統中最強大的安全框架。預設情況下,只需新增依賴項,Spring Boot 就會自動保護所有端點。本文介紹如何設定身份驗證、授權並了解安全過濾器鏈。
1.Spring安全架構
1.1 安全過濾器鏈
HTTP Request
│
▼
┌─────────────────────────────────┐
│ SecurityFilterChain │
│ │
│ ┌───────────────────────────┐ │
│ │ CorsFilter │ │
│ ├───────────────────────────┤ │
│ │ CsrfFilter │ │
│ ├───────────────────────────┤ │
│ │ UsernamePasswordAuth... │ │
│ ├───────────────────────────┤ │
│ │ BasicAuthenticationFilter │ │
│ ├───────────────────────────┤ │
│ │ BearerTokenAuth... (JWT) │ │
│ ├───────────────────────────┤ │
│ │ AuthorizationFilter │ │
│ ├───────────────────────────┤ │
│ │ ExceptionTranslation... │ │
│ └───────────────────────────┘ │
│ │
└─────────────────────────────────┘
│
▼
DispatcherServlet → Controller
1.2 核心概念
| 概念 | 描述 |
|---|---|
| 身份驗證 | 驗證:「你是誰?」 |
| 授權 | 去中心化:「你能做什麼?」 |
| 校長 | 目前已通過身分驗證的使用者 |
| 授予權限 | 使用者權限/角色 |
| 安全情境 | 儲存驗證資訊 |
2. 基本配置
2.1 依賴關係
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 安全過濾鏈配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable cho REST API
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.3 授權規則
// Patterns
.requestMatchers("/public/**").permitAll() // Ai cũng truy cập được
.requestMatchers("/admin/**").hasRole("ADMIN") // Chỉ ADMIN
.requestMatchers("/api/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/**").hasAuthority("WRITE_ARTICLES")
.anyRequest().authenticated() // Phải đăng nhập
.anyRequest().denyAll() // Chặn tất cả
3. UserDetailsService — 自訂使用者存儲
3.1 實體與儲存庫
@Entity
@Table(name = "users")
public class User extends BaseEntity {
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password; // BCrypt hash
@Column(nullable = false)
private boolean enabled = true;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
3.2 自訂使用者詳細資訊服務
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(String[]::new))
.disabled(!user.isEnabled())
.build();
}
}
3.3 用戶註冊
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public AuthService(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public UserResponse register(RegisterRequest request) {
if (userRepository.existsByUsername(request.username())) {
throw new DuplicateResourceException("User", "username", request.username());
}
if (userRepository.existsByEmail(request.email())) {
throw new DuplicateResourceException("User", "email", request.email());
}
User user = new User();
user.setUsername(request.username());
user.setEmail(request.email());
user.setPassword(passwordEncoder.encode(request.password()));
user.setRoles(Set.of("USER"));
user.setEnabled(true);
User saved = userRepository.save(user);
return UserResponse.from(saved);
}
}
4. 身份驗證管理器
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public AuthenticationProvider authenticationProvider(
CustomUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
}
5.取得目前使用者訊息
@RestController
@RequestMapping("/api/users")
public class UserController {
// Cách 1: @AuthenticationPrincipal
@GetMapping("/me")
public UserResponse getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return userService.findByUsername(userDetails.getUsername());
}
// Cách 2: SecurityContextHolder
@GetMapping("/profile")
public UserResponse getProfile() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
return userService.findByUsername(username);
}
}
6. 密碼編碼最佳實踐
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt: salt tự động, strength mặc định = 10
return new BCryptPasswordEncoder();
// Hoặc DelegatingPasswordEncoder cho multiple encoders
// return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// Verify password
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
重要:切勿以純文字形式儲存密碼。始終使用 BCrypt 或 Argon2。
總結
- Spring Security 使用 SecurityFilterChain 透過一系列過濾器來處理身份驗證和授權
- UserDetailsService 允許從資料庫自訂使用者存儲,結合 BCryptPasswordEncoder 來保護密碼
- 透過 HttpSecurity 設定的授權規則:permitAll()、hasRole()、hasAuthority()、authentiated()
練習
- 為 REST API 設定 SecurityFilterChain:公共端點 (GET /api/products)、受保護端點 (/api/users)、管理端點 (/api/admin)
- 實作 CustomUserDetailsService 從資料庫載入用戶,使用 BCrypt 編碼的密碼註冊新用戶
- 建立端點
/api/auth/register和/api/users/me,使用 HTTP Basic 測試驗證流程