mike-neckのブログ

Java or Groovy or Swift or Golang

Spring WebFlux Security で ユーザー認証を実装する + ハンドラーでユーザーの情報を取り出す

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

PasswordEncoderNoOpPasswordEncoder を使ってしまっているが、どこかに提供するアプリケーションでもないので、ここでは不問。プロダクションで使う場合は、適切なものを使うべき。


このまま動かしても、ユーザーが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);
}