mike-neckのブログ

Java or Groovy or Swift or Golang

リファクタリングの手順 メソッドの抽出 -> メソッドの別クラスへの移動

IntelliJ での基本的なリファクタリングの手順です。


次のような二つのクラスを考えます。

public class WithAccessTokenSecurityContextFactory implements WithSecurityContextFactory<WithAccessToken> {

    @Override
    public SecurityContext createSecurityContext(final WithAccessToken annotation) {
        final SecurityContext context = SecurityContextHolder.createEmptyContext();
        final GivenAccessToken accessToken = new GivenAccessToken(annotation);
        final AccessTokenEntity accessTokenEntity = accessToken.accessTokenEntity(); // ここから
        final Authentication authentication = new Authentication(accessTokenEntity);
        final OAuth2Authentication oauth2 = authentication.oauth2(); // ここまでを一つにまとめたい
        context.setAuthentication(oauth2);
        return context;
    }
}
class GivenAccessToken {
    GivenAccessToken(final WithAccessToken accessToken) {
        // 省略
    }
    AccessTokenEntity accessTokenEntity() {
        // 省略
    }
}

このように7行もあるメソッドは長すぎるし、利用するクラスが実装を知りすぎているので、一つにまとめて見通しを良くしたいところです。

1. メソッドの抽出

最初に WithAccessTokenSecurityContextFactory の内部に private メソッドとして抽出します。

  • コード内で「ここから」〜「ここまで」とした範囲を選択する

f:id:mike_neck:20180215225007p:plain

  • Refactor > Extract > Method を選択する。あるいは + + M を押す

f:id:mike_neck:20180215225049p:plain

  • ダイアログでメソッドの可視性、型、メソッド名、パラメーターを確認してメソッドの抽出を実行

f:id:mike_neck:20180215225235p:plain

抽出が完了したら次のような形になります。

public class WithAccessTokenSecurityContextFactory implements WithSecurityContextFactory<WithAccessToken> {

    @Override
    public SecurityContext createSecurityContext(final WithAccessToken annotation) {
        final SecurityContext context = SecurityContextHolder.createEmptyContext();
        final GivenAccessToken accessToken = new GivenAccessToken(annotation);
        final OAuth2Authentication oauth2 = oauth2(accessToken);
        context.setAuthentication(oauth2);
        return context;
    }

    private OAuth2Authentication oauth2(final GivenAccessToken accessToken) {
        final AccessTokenEntity accessTokenEntity = accessToken.accessTokenEntity();
        final Authentication authentication = new Authentication(accessTokenEntity);
        return authentication.oauth2();
    }
}

2. メソッドの別クラスへの移動

抽出したメソッドおよび抽出されたメソッドは短くなりますが、今のままだと WithAccessTokenSecurityContextFactoryGivenAccessTokenOAuth2Authentication の内部実装を知りすぎています。このような操作は GivenAccessToken が抑えておけばよい内容です。したがって、 oauth2 メソッドを GivenAccessToken に移動します。

  1. oauth2 メソッドにカーソルをあわせる
  2. Refactor > Move を選択する。あるいは F6

f:id:mike_neck:20180215225916p:plain

  • ダイアログで移動先のクラスを選択して移動を実行

f:id:mike_neck:20180215230032p:plain

移動完了後は次のようになっている

public class WithAccessTokenSecurityContextFactory implements WithSecurityContextFactory<WithAccessToken> {

    @Override
    public SecurityContext createSecurityContext(final WithAccessToken annotation) {
        final SecurityContext context = SecurityContextHolder.createEmptyContext();
        final GivenAccessToken accessToken = new GivenAccessToken(annotation);
        final OAuth2Authentication oauth2 = accessToken.oauth2();
        context.setAuthentication(oauth2);
        return context;
    }
}
class GivenAccessToken {
    GivenAccessToken(final WithAccessToken accessToken) {
        // 省略
    }
    AccessTokenEntity accessTokenEntity() {
        // 省略
    }
    private OAuth2Authentication oauth2() {
        final AccessTokenEntity accessTokenEntity = accessTokenEntity();
        final Authentication authentication = new Authentication(accessTokenEntity);
        return authentication.oauth2();
    }
}

privateメソッドに一度抽出しているが、これを飛ばしてメソッド抽出+メソッドを別クラスへ移動することはできない(っぽい)

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

Spring Web Flux を少しさわってみただけの話

Spring 5 から Reactive Web というのになるらしく、サーバーの性能が落ちにくくなると聞いた(違うかもしれない)ので、 家で試してみることにした。

環境

  • Spring Boot: 2.0.0.RC1
  • Java: 1.8

ビルドファイル

以下の curl コマンドで取得できるもの

curl https://start.spring.io/build.gradle -d dependencies=security,webflux -d bootVersion=2.0.0.RC1

なお、依存ライブラリーは次のようになっている

dependencies {
  compile('org.springframework.boot:spring-boot-starter-security')
  compile('org.springframework.boot:spring-boot-starter-webflux')
  testCompile('org.springframework.boot:spring-boot-starter-test')
  testCompile('io.projectreactor:reactor-test')
  testCompile('org.springframework.security:spring-security-test')
}

ハンドラーファンクション

リクエストをさばくために、 org.springframework.web.reactive.function.server.ServerRequest を受け取って、 reactor.core.publisher.Mono または reactor.core.publisher.Flux を返す関数、または メソッドを作る

// クラス名は MyHandler
Mono<ServerResponse> hello(final ServerRequest serverRequest) {
  return ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .body(
          Mono.just(new Message("hello", OffsetDateTime.now(ZoneId.of("Z")))),
          Message.class);
}

なお、jsonマッピングするクラスは次のようなクラス

@Value
static class Message {
  final String text;
  final OffsetDateTime time;
}

ルーターファンクション

ハンドラーファンクションに対して、URLをマッピングする、ルーターファンクション(org.springframework.web.reactive.function.server.RouterFunction)をBean登録する

// クラス名は MyConfig
@Bean
RouterFunction<ServerResponse> routerFunction(
    final SpringWebfluxDemoApplication springWebfluxDemoApplication) {
  return route(GET("/hello"), myHandler::hello);
}

なお、このメソッドのある MyConfig クラスには @org.springframework.web.reactive.config.EnableWebFlux アノテーションをつける

@EnableWebFlux
@Configuration
public class MyConfig {
  // 省略
}

セキュリティ

元々セキュリティをつけようとは思っていなかったのだが、間違えてつけてしまったので、その設定もしてみる

Reactive web ではないアプリケーションの場合は UserDetailsService を Bean 登録していたが、 Reactive web であるアプリケーションの場合には ReactiveUserDetailsService を Bean 登録する

ドキュメントを読んでいると、 MapReactiveUserDetailsService なるクラスがあるので、そちらを利用する

@Bean
ReactiveUserDetailsService userDetailsService() {
  final UserDetails userDetails = User.withDefaultPasswordEncoder()
      .username("foo")
      .password("bar")
      .roles("BAZ", "QUX")
      .build();
  return new MapReactiveUserDetailsService(userDetails);
}

いつもの

あとはいつものとおり、 @SpringBootApplication アノテーションをつけたクラスと、 main メソッドを作る

@SpringBootApplication
public class App {
  public static void main(String... args) {
    SpringApplication.run(App.class, args);
  }
}

動かしてみた

アプリケーションを起動して curl でアクセスしてみる

f:id:mike_neck:20180208030819p:plain

$ curl -v http://localhost:8080/hello -H "Authorization: Basic $(echo -n 'foo:bar' | openssl base64)"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Authorization: Basic Zm9vOmJhcg==
> 
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Content-Type: application/json
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< 
* Connection #0 to host localhost left intact
{"text":"hello","time":1518024505.503000000}

まあ、なんか、動いた


OffsetDateTime のところがタイムスタンプになっていたので Jackson のカスタマイズ方法をドキュメントから探して CodecCustomizer を Bean 登録すればよいことがわかったのだが、これはうまくいかなかった…

@Bean
CodecCustomizer addJacksonJsr310Support() {
  return configurer -> configurer.customCodecs()
      .encoder(new Jackson2JsonEncoder(objectMapper(), MimeTypeUtils.APPLICATION_JSON));
}

おわり

同一のURLに同じHTTPメソッドで異なるパラメーターを送った時に異なるメソッドを割り当てる

同じURL(/foo/bar)に異なるメソッド(baz(Baz baz)qux(Qux qux) の二つのメソッド)を割り当てる方法

@Canonical
class Text {
    String text
}

@Canonical
class Bidirecional {
    String question
    String answer1
    String answer2
}

@Canonical
class Result<T> {
    T content
    int id
}

@RestController
@RequestMapping(path = '/app')
class AppController {

    def map = [:]

    def index = new AtomicInteger(1)

    @PostMapping(consumes = 'application/json', produces = 'application/json', headers = 'Type=text')
    ResponseEntity<Result<Text>> postText(@RequestBody Text text) {
        def i = index.getAndIncrement()
        map[i] = text
        println text
        ResponseEntity.ok(new Result(id: i, content: text))
    }

    @PostMapping(consumes = 'application/json', produces = 'application/json', headers = 'Type=bidirectional')
    ResponseEntity<Result<Bidirecional>> postBidirectional(@RequestBody Bidirecional item) {
        def i = index.getAndIncrement()
        map[i] = item
        println item
        ResponseEntity.ok(new Result(id: i, content: item))
    }
}

ヘッダーを変えると異なるメソッドでも同一のURLに割り当てられるらしい