
在上一篇文章中,我們介紹了 Spring Security 的基礎知識
現在,我們將進一步探討如何使用程式碼來自訂安全設定
移除 application.properties 裡面的 spring.security.user 相關設定
修改原本的 SecurityConfig,加上 UserDetailsService 和 PasswordEncoder 兩個 bean
@Configuration
@EnableMethodSecurity
@EnableWebSecurity
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("user123!"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin123!"))
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在 userDetailsService 方法裡面,建立了兩個使用者,一個使用者和一個管理員
實作為使用 InMemoryUserDetailsManager ,表示把使用者的資料存在記憶體裡面
我們使用了 BCryptPasswordEncoder 密碼編碼器
BCrypt 是一種強大的密碼雜湊 (hash) 函數,專門設計用於安全地存儲密碼
它自動處理加鹽(salt)並可調整運算複雜度,能有效抵禦彩虹表攻擊
為了方便說明,可以先把使用者的密碼寫到 log 來看
注意:這僅為說明目的,實際應用中絕對不要將密碼記錄在 Log 中
注意:這僅為說明目的,實際應用中絕對不要將密碼記錄在 Log 中
注意:這僅為說明目的,實際應用中絕對不要將密碼記錄在 Log 中
(很重要,要說三次)

預設產生的密碼,由$分隔成了三個部份
演算法標識
2a 這表示 BCrypt 的版本成本因子(cost factor)
10 也稱為工作因子(work factor)鹽值和雜湊值
salt (加鹽)和實際雜湊後的值使用 admin 呼叫需要 ADMIN 角色的 API,可以正常呼叫

如果用 user 呼叫,則會發生 Access Denied

user 呼叫一般 API也沒有問題

在 SecurityConfig 裡面加上 securityFilterChain 方法
而原本在 controller 的 PreAuthorize 註解,可以使用這裡的 requestMatchers 來取代
這裡 securityFilterChain 的相關寫法,網路上或是 AI 的答案,蠻常都是舊版的寫法,要小心
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 請求授權設定
http.authorizeHttpRequests((requests) -> requests
.requestMatchers("/").permitAll()
.requestMatchers("/api/todos/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/hello").hasRole("ADMIN")
.anyRequest().authenticated()
)
// HTTP Basic 設定
.httpBasic(Customizer.withDefaults())
// CSRF 設定
.csrf(AbstractHttpConfigurer::disable)
// 登入設定
.formLogin((form) -> form
.loginPage("/login")
.loginProcessingUrl("/perform_login")
.defaultSuccessUrl("/", true)
.permitAll()
)
// 登出設定
.logout((logout) -> logout
.logoutUrl("/perform_logout")
.logoutSuccessUrl("/")
.permitAll());
return http.build();
}
/ : 允許所有用戶存取/api/todos/** : 只允許 USER 或是 ADMIN 角色的使用者存取/api/hello : 只允許 ADMIN 角色的使用者存取已認證 (登入) 的使用者存取Authorization: Basic 認證/login /perform_login
/
/perform_logout /
在 requestMatchers 打 /api/ 時,會出現專案所有 /api/ 下面的端點

設定完,點選前面的 盾牌 就可以看到這個 Matchers 下面所有的端點

新增 User 的 entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String roles;
// getter and setter ...
}
新增 UserRepository interface,加一個 findByUsername 給登入使用
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
在 application.properties 裡面設定 JPA 的 ddl 操作
# 每次應用程式啟動,自動建立 table (先刪除後新增)
spring.jpa.hibernate.ddl-auto=create
修改 DatabaseInitializer,加上 User 相關的資料庫操作
@Component
public class DatabaseInitializer {
private static final Logger log = LoggerFactory.getLogger(DatabaseInitializer.class);
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public DatabaseInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@PostConstruct
@Transactional
public void initDatabase() {
if (userRepository.count() == 0) {
User user = new User();
user.setUsername("user");
user.setPassword(passwordEncoder.encode("user123!"));
user.setRoles("USER");
User admin = new User();
admin.setUsername("admin");
admin.setPassword(passwordEncoder.encode("admin123!"));
admin.setRoles("ADMIN,USER");
userRepository.saveAll(Arrays.asList(user, admin));
log.info("Default users initialized successfully");
}
}
}
也可以看到 H2 裡面有使用者的相關資料

建立自訂義的 UserDetailsService
並且 override``loadUserByUsername 這個方法,讓使用者登入的時候呼叫使用
@Service
public class CustomUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("load user by username: {}", username);
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().split(","))
.build();
}
}
log 也可以看的到,目前的 UserDetailsService 是使用 CustomUserDetailsService

這樣子 API 在使用基本認證的時候,就會呼叫 loadUserByUsername 這個方法

這裡因為範例講解的方便性,直接把使用者寫到資料庫內,所以沒有處理註冊相關的問題
實務上,還需要有使用者註冊的 API
如果使用了 Spring Security 之後,要存取 H2 console 的話
要注意下面的相關設定,這樣子才可以存取 H2 console
CSRF csrf(AbstractHttpConfigurer::disable)
允許所有人可以存取requestMatchers("/h2-console/**").permitAll()
frame 為 sameOrigin headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
透過這些進階設定,我們實現了更靈活且安全的認證系統
我們使用程式碼來設定相關的安全性,自訂義了 API 端點的存取控制,並將使用者資料儲存在資料庫中
這些改進使得我們的應用程式更加健壯和可擴展
在實際的生產環境中,你可能需要考慮更多的安全措施,如使用 HTTPS、實施更複雜的密碼策略、添加多因素認證等
Spring Security 提供了豐富的功能來滿足各種安全需求,你可以根據具體的專案需求進行進一步的自訂和優化
同步刊登於 Blog 「Spring Boot API 開發:從 0 到 1」Day 33 Spring Security 進階
我的粉絲專頁
圖片來源:AI 產生