注意: この記事はLLMによって英語から翻訳されたものです。正確性については保証いたしかねますので、あらかじめご了承ください。英語の原文はこちら。
Springは最もよく使われる(おそらく最もよく使われる)Javaフレームワークの1つです。多くの優れた機能を備えており、プロジェクトを素早く立ち上げることができますが、APIを構築する場合、認証管理の部分は十分にドキュメント化されていません。この記事では、Spring Security(v5.7.3)を活用してSpring Boot APIを保護し、ロールで権限を細かく調整する方法について説明します。以下の内容では、SpringとHibernateについてある程度の知識があることを前提としています。
認証オプション#
残念ながら、Spring Securityにはトークンを生成して認証メカニズムとして使用する機能は提供されていません(少なくともドキュメントには記載されていません)。そのため、Basic認証を使用します。
以前は、WebSecurityConfigurerAdapterクラスで認証を管理できました(インターネット上のほとんどのチュートリアルはこれを使用しています)が、今年非推奨になったため、使用しません。
コードを書こう#
デフォルトでは、Spring Securityはユーザーとロールのクラスを提供していますが、データベースパッチの適用が必要なようで柔軟性に欠けます。それらを使用する代わりに、独自のクラスを作成して適切なインターフェースを実装します。これにより、現在もUserクラスの機能を拡張したい将来も、作業が容易になります。以下のコードではLombokとJPAを使用します。
ユーザー#
UserクラスをSpring Securityと互換性を持たせるために、org.springframework.security.core.userdetails.UserDetailsを実装する必要があります。最低限、フレームワークが使用するusernameとpassword属性が必要です。また、getAuthorities(これについては後で詳しく説明します)、isLockedなどのさまざまなメソッドも実装する必要があります。これらのメソッドはSpring Securityがアクセスを許可するために使用するため、返す値には注意してください。
@Entity
@Audited
@NoArgsConstructor
@Table(name = "users")
public class User implements UserDetails {
@Id @GeneratedValue @Getter
private Long id;
@Getter @Setter
private String email;
@Getter @Setter
@Column(unique = true)
private String username;
@Getter @Setter
private String password;
@Getter @Setter
private boolean enabled;
@Getter @Setter
private boolean locked;
@Getter @Setter
@Column(name = "expiration_date")
private Date expirationDate;
@Setter @Getter
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
private Set<UserRole> roles = new HashSet<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public boolean isAccountNonExpired() {
return expirationDate == null || expirationDate.after(new Date());
}
@Override
public boolean isAccountNonLocked() {
return !isLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}@Auditedアノテーションに注目してください。これはorg.hibernate:hibernate-enversによって提供されます。このアノテーションの機能は、データベースに変更がプッシュされるたびにオブジェクトの古いバージョンを保持することで、ユーザー管理において便利です。
ロールの実装#
基本的なUserクラスができたので、ロールを設定しましょう。実装は非常にシンプルで、GrantedAuthorityを実装してメソッドgetAuthorityをオーバーライドするだけです。
ここではenumを使用して2つのロールを定義していますが、異なるロールを使用して別の方法で行うことも可能です。ロール名はROLE_[...]の形式にしてください。
@Embeddable
public class UserRole implements GrantedAuthority {
@Setter
@Enumerated(EnumType.STRING)
@Column(name = "role_name")
private RoleName roleName;
@Override
public String getAuthority() {
return roleName.name();
}
public enum RoleName {
ROLE_ADMIN,
ROLE_USER
}
}データベースアクセス#
次のステップは、Spring Securityがデータベースからユーザーの詳細を取得できるようにすることです。これには2つのクラスを実装する必要があります。まず、データベースへのクエリを可能にするJpaRepositoryです。
@Configuration
public interface UserRepository extends JpaRepository<User, Long> {
User findUserByUsername(String username);
}次に、Spring Securityがユーザーを検索するために使用するUserDetailsServiceを実装したクラスです。
@Component
public class SecurityUserDetailService implements UserDetailsService {
private final UserRepository _userRepository;
public SecurityUserDetailService(UserRepository userRepository) {
this._userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return _userRepository.findUserByUsername(username);
}
}セキュリティ設定#
最後に、Spring Securityの動作を定義する設定クラスを作成する必要があります。CSRF保護を無効にし、セッション管理をステートレスに設定していることに注目してください。これはREST APIで使用するためです。WebUIを構築する場合は、このようにすべきではありません。
また、パスワードのハッシュ方法を定義するpasswordEncoderメソッドにも注目してください。ここでは、10ラウンドのbcryptを使用しています。
@Configuration
@EnableMethodSecurity
public class SecurityConfiguration
{
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
@Bean
public AuthenticationManager authManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, SecurityUserDetailService securityUserDetailService) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(securityUserDetailService)
.passwordEncoder(bCryptPasswordEncoder)
.and()
.build();
}
}特定のロールによるメソッドアクセスの制御#
ここまでの設定を適用すると、アプリケーションはすべてのエンドポイントにアクセスするためにユーザー認証を要求するようになります。次に、誰が何にアクセスできるかを細かく調整する方法を見てみましょう。
まず、設定クラスから管理できます。例えば、filterChainメソッドを以下のように変更して、認証なしで誰でも/api/publicにクエリでき、/api/adminへのアクセスにはADMINロールが必要になるようにできます。
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/api/public").permitAll()
.antMatchers("/api/admin").hasRole("ADMIN")
.anyRequest().authenticated().and().httpBasic();もう1つのオプションは、コントローラーに直接アノテーションを付けることです。ここでは、/api/adminにアクセスするためにADMINロールを要求しています。
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
...最後に、コントローラー内のメソッドに直接アノテーションを付けることもできます。メソッドが@AuthenticationPrincipalを使用していることに注目してください。これにより、リクエストを行っている認証済みユーザーを取得でき、完全にオプションです。
@GetMapping("/user/{userId}")
@PreAuthorize("hasRole('ADMIN')")
ResponseEntity<?> getUser(@AuthenticationPrincipal User user, @PathVariable Long userId) {
...まとめ#
この記事では、Spring BootでREST APIを作成する際に、Spring-Securityを活用して基本的な認証メカニズムを実装し、認証と認可を提供する方法を学びました。これは始めるには十分ですが、Spring Securityの機能はこの記事に記載されたものに限りません。例えば、複数のグループを認可したり、@PostAuthorizeなどの他のアノテーションを使用することもできます。
ソースとクレジット#
ソース#
クレジット#
- カバー写真: Clement Helardot(Unsplash)