Chuyển đến nội dung chính

レッスン 9: Spring セキュリティの基礎 — 認証と認可

SecurityFilterChain、HttpSecurityの構成。 UserDetailsS​​ervice、PasswordEncoder、ログインフォーム、HTTP Basic。 @Secured、@PreAuthorize を使用したロールベースのアクセス制御 (RBAC)。

💻 プログラミング — レッスン 8 レッスン 9: Spring セキュリティの基礎 — 認証と認可

Spring Boot 4: 基本から上級まで

パート 3: アプリケーションのセキュリティ

xdev.asia

はじめに

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 SecurityFilterChain の構成

@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 カスタム UserDetailsService

@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()、authenticated()

演習

  1. REST API 用に SecurityFilterChain を構成します: パブリック エンドポイント (GET /api/products)、保護されたエンドポイント (/api/users)、管理エンドポイント (/api/admin)
  2. CustomUserDetailsService を実装してデータベースからユーザーをロードし、BCrypt でエンコードされたパスワードを使用して新しいユーザーを登録します
  3. エンドポイントの作成 /api/auth/register そして /api/users/me、HTTP Basic を使用した認証フローのテスト