Spring Security の WebFlux Security Configuration という公式のドキュメントにあるセクションにも書いてあるのだが、ユーザー認証の実装はこれまでの Spring Security にある UserDetails
の Reactive 版である ReactiveUserDetails
を実装したオブジェクトをBean登録すれば実現できる。
サンプルとして、Cassandraに保存してあるユーザーの情報を以てユーザー認証をおこなうアプリケーションを作ってみた。
環境
- Spring Boot: 2.0.0.RC1
- Java: 8
- Cassandra: 3.11.1
ユーザーのテーブル
ユーザーをあらわすクラスを作る
@Table("users") @Value public class User { @PrimaryKey private final String id; @Indexed private final String username; @JsonIgnore private final String password; @JsonIgnore private final Set<UserRole> roles; private final LocalDateTime created; public static User createNew(final String username, final String password) { final String id = UUID.randomUUID().toString(); return new User(id, username, password, UserRole.forNormalUser(), LocalDateTime.now()); } }
ユーザー名(username
)に @Indexed
をつけておかないと、Cassandraでユーザー名をキーにユーザーを取得できないので注意。
ユーザーのロールをあらわすクラスを作る
public enum UserRole implements GrantedAuthority { USER, ADMIN; @Override public String getAuthority() { return name(); } }
User
の Cassandra の Repository
インターフェースを作る
@Repository public interface UserRepository extends ReactiveCrudRepository<User, String> { Mono<User> findByUsername(final String username); }
戻り値が Mono<User>
にするあたりが Web Flux っぽい(よくわかってない)
これまでの Spring Security のように UserDetails
を実装したクラスをつくるのだが、 org.springframework.security.core.userdetails.User
と名前を被らせてしまったので、 AuthenticatedUser
というクラスを作る
public class AuthenticatedUser implements UserDetails { private final User user; // <- 先程作った User の方 public AuthenticatedUser(final User user) { this.user = user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return user.getRoles(); } // その他メソッドは割愛 }
この AuthenticatedUser
を ユーザー名から取り出すサービスクラス ReactiveUserDetailsService
をBean登録する
@Configuration @EnableWebFluxSecurity public class SecurityConfig { private final UserRepository userRepository; // コンストラクターは割愛 @Bean ReactiveUserDetailsService userDetailsService() { return username -> userRepository.findByUsername(username) .map(AuthenticatedUser::new); } @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } }
PasswordEncoder
に NoOpPasswordEncoder
を使ってしまっているが、どこかに提供するアプリケーションでもないので、ここでは不問。プロダクションで使う場合は、適切なものを使うべき。
このまま動かしても、ユーザーがCassandraにないので、当然401 UnAuthorized が返されまくる。そこで、アプリケーション起動時にCassandraにユーザーを登録する。
@SpringBootApplication(exclude = { CassandraDataAutoConfiguration.class }) @EnableWebFlux @Slf4j public class App { private final UserRepository userRepository; // コンストラクター割愛 @Bean CommandLineRunner commandLineRunner() { return args -> { final User user = saveUser(User.createNew("foo", "bar")) .block(); log.info("created user: {}", user); } @Transactional Mono<User> saveUser(final User user) { return userRepository.save(user); } }
これでユーザー認証が実現できた
さらに認証したユーザーの情報をそのまま返すurlを作ってみる。
認証の情報は ServerRequest#principal()
というメソッドを実行すると UsernamePasswordAuthenticationToken
というクラスのインスタンス(Mono
に包まれている)が返されるので、 UsernamePasswordAuthenticationToken#getPrincipal
というメソッドで取得できる。
Mono<ServerResponse> me(final ServerRequest request) { final Mono<User> user = request.principal() .cast(UsernamePasswordAuthenticationToken.class) .map(UsernamePasswordAuthenticationToken::getPrincipal) .cast(AuthenticatedUser.class) .map(AuthenticatedUser::getUser); return ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .body(user, User.class); } @Bean RouterFunction<ServerResponse> userRouterFunction() { return route(GET("/users/me"), this::me); }