Spring Security 是 Spring 生態最強大的安全框架。本文從基礎表單登錄進階到 RBAC 權限模型與 JWT 無狀態認證,構建生產級安全體系。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 啟用 @PreAuthorize 等註解
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/login", "/register", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.csrf(csrf -> csrf.disable()); // API 模式通常關閉 CSRF,改用 JWT
return http.build();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
// 內存用戶 (生產環境替換為資料庫實現)
UserDetails user = User.withUsername("user")
.password(encoder.encode("password"))
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password(encoder.encode("admin123"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<body>
<div th:if="${param.error}" class="alert alert-danger">用戶名或密碼錯誤</div>
<div th:if="${param.logout}" class="alert alert-success">已成功登出</div>
<form th:action="@{/login}" method="post">
<input type="text" name="username" placeholder="用戶名" required/>
<input type="password" name="password" placeholder="密碼" required/>
<button type="submit">登錄</button>
</form>
</body>
</html>
@Entity
public class SysUser implements UserDetails {
@Id @GeneratedValue
private Long id;
private String username;
private String password;
private Boolean enabled = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<SysRole> roles = new HashSet<>();
// UserDetails 方法實現
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.flatMap(r -> r.getPermissions().stream())
.map(p -> new SimpleGrantedAuthority(p.getCode()))
.collect(Collectors.toSet());
}
// 其他方法略
}
@Entity
public class SysRole {
@Id @GeneratedValue
private Long id;
private String name; // ROLE_ADMIN
private String code; // ADMIN
@ManyToMany
@JoinTable(name = "sys_role_permission",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<SysPermission> permissions = new HashSet<>();
}
@Entity
public class SysPermission {
@Id @GeneratedValue
private Long id;
private String name; // 用戶管理
private String code; // USER:READ, USER:WRITE
private String type; // MENU, BUTTON, API
}
new UsernameNotFoundException("用戶不存在: " + username));
}
}
階段三:JWT 無狀態認證 (前後端分離標配)
JWT 工具類
(); claims.put("roles", userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority).collect(Collectors.toList())); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expirationMs)) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } public String getUsername(String token) { return Jwts.parserBuilder().setSigningKey(getSigningKey()).build() .parseClaimsJws(token).getBody().getSubject(); } public List<String> getRoles(String token) { return (List<String>) Jwts.parserBuilder().setSigningKey(getSigningKey()).build() .parseClaimsJws(token).getBody().get("roles"); } }JWT 認證過濾器
整合到 Security 鏈
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }登錄接口簽發 Token
前端調用示例
// 登錄獲取 Token const login = async (username, password) => { const res = await fetch('/api/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username, password}) }); const {token} = await res.json(); localStorage.setItem('jwt_token', token); return token; }; // 攜帶 Token 請求受保護資源 const fetchUsers = async () => { const token = localStorage.getItem('jwt_token'); const res = await fetch('/api/users', { headers: {'Authorization': `Bearer ${token}`} }); return res.json(); };生產環境安全加固清單
Spring Security 從「繁瑣配置」進化為「註解驅動 + 函數式 DSL」,配合 JWT 實現無狀態認證,完美適配前後端分離架構。核心遵循:認證 確認身份 → 授權 判定權限 → 審計 記錄軌跡。掌握 SecurityFilterChain、UserDetailsService、@PreAuthorize、JWT 過濾器鏈四大核心組件,即可構建企業級安全防線。