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;
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);
}