iT邦幫忙

2024 iThome 鐵人賽

0
Modern Web

Spring Boot API 開發:從 0 到 1系列 第 34

Day 33 Spring Security 進階

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240913/20121948AwJdEJKXDN.png

在上一篇文章中,我們介紹了 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 中
(很重要,要說三次)

https://ithelp.ithome.com.tw/upload/images/20240913/20121948P0PmG26dbb.png

預設產生的密碼,由$分隔成了三個部份

  • 演算法標識

    • 2a  這表示 BCrypt 的版本
  • 成本因子(cost factor)

    • 10  也稱為工作因子(work factor)
    • 它決定了雜湊的迭代次數,這裡是 2^10 = 1024 次
    • 數字越大,計算雜湊所需的時間就越長,安全性更高,但同時也會增加系統負擔
  • 鹽值和雜湊值

    • 剩餘部分包含了 salt (加鹽)和實際雜湊後的值

測試

使用 admin 呼叫需要 ADMIN 角色的 API,可以正常呼叫

https://ithelp.ithome.com.tw/upload/images/20240913/20121948pvsZLwiRy0.png

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

https://ithelp.ithome.com.tw/upload/images/20240913/20121948jBKYz0G8mJ.png

user 呼叫一般 API也沒有問題

https://ithelp.ithome.com.tw/upload/images/20240913/20121948A1zyF6pg1x.png

自訂 HttpSecurity 

在 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 角色的使用者存取
  • 其他所有請求 : 只允許已認證 (登入) 的使用者存取

HTTP Basic 設定

  • 使用預設值,也就是 API 使用的 Authorization: Basic 認證

CSRF 設定

  • 對於無狀態的 API 來說通常不需要,禁用它

登入設定

  • 自訂登入頁面路徑為 /login 
  • 處理登入請求的 URL 為 /perform_login
  • 登入成功後預設導向到 /
  • 允許所有使用者存取登入功能

登出設定

  • 登出請求的 URL 為 /perform_logout 
  • 登出成功後導向到 /
  • 允許所有使用者存取登出功能

IDE 方便的功能

在 requestMatchers 打 /api/ 時,會出現專案所有 /api/ 下面的端點

https://ithelp.ithome.com.tw/upload/images/20240913/20121948167hfR9wkF.png

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

https://ithelp.ithome.com.tw/upload/images/20240913/20121948PrFk6FkKDw.png

將使用者資料儲存到H2資料庫

新增 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 裡面有使用者的相關資料

https://ithelp.ithome.com.tw/upload/images/20240913/20121948lLfIiJjet9.png

建立自訂義的 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

https://ithelp.ithome.com.tw/upload/images/20240913/20121948ATSlReQkD1.png

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

https://ithelp.ithome.com.tw/upload/images/20240913/201219485oWJTY46fE.png

這裡因為範例講解的方便性,直接把使用者寫到資料庫內,所以沒有處理註冊相關的問題
實務上,還需要有使用者註冊的 API 

同場加映:H2 console 的安全存取

如果使用了 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 產生

參考連結


上一篇
Day 32.5 Spring Boot 測試進化:三種優雅的 JSON 驗證方法
下一篇
Day 34 Spring Security 測試
系列文
Spring Boot API 開發:從 0 到 139
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言