在上一篇文章中,我們介紹了 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 產生