まとめ
UsernamePasswordAuthenticaitonFilter
が FormData から認証情報を読み込むので、 それを仕様にしてしまったほうが実装が楽になる。UsernamePasswordAuthenticaitonFilter
を使えないなら、 JSON から認証情報を読み込んでも実装コストとコード量はそんなに増えない。- やっぱり DSL で設定を記述したい。
本文
Spring Security 標準の UsernamePasswordAuthenticationFilter は ログインフォームから入力されたユーザー名とパスワードを処理する仕様になっている。 REST API では認証情報を JSON 形式で送信したくなるが、その場合は JSON 形式のリクエストボディから認証情報を取り出す Filter を実装する必要がある。
class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } Map<String, String> content; try { content = objectMapper.readValue(request.getInputStream(), new TypeReference<>() { }); } catch (IOException e) { throw new RuntimeException(e); } String username = content.getOrDefault(getUsernameParameter(), ""); String password = content.getOrDefault(getPasswordParameter(), ""); UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
そのうえ、 HttpSecurity#formLogin
で設定している項目を Filter にセッターで設定する必要がある。
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, AuthenticationManager authenticationManager) throws Exception { JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(); filter.setFilterProcessesUrl("/api/login"); filter.setAuthenticationSuccessHandler((request, response, authentication) -> { response.setStatus(HttpServletResponse.SC_OK); }); filter.setAuthenticationFailureHandler((request, response, exception) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); }); filter.setAuthenticationManager(authenticationManager); return httpSecurity .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class) .csrf(csrf -> csrf .ignoringRequestMatchers("/api/login") .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ) .build(); } @Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setPasswordEncoder(passwordEncoder); authenticationProvider.setUserDetailsService(userDetailsService); return new ProviderManager(authenticationProvider); } @Bean public InMemoryUserDetailsManager inMemoryUserDetailsManager(PasswordEncoder passwordEncoder) { InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); inMemoryUserDetailsManager.createUser(User.builder().username("user").password(passwordEncoder.encode("password")).roles("USER").build()); return inMemoryUserDetailsManager; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
この方法は以下の理由で嫌だと感じた。
UsernamePasswordAuthenticationFilter
の実装とHttpSecurity#formLogin
のデフォルトの振る舞いを活用できないので、記述量が増える。- もともと DSL で設定できていたものをセッターでダラダラ設定するのが嫌。
そもそも認証フィルタをカスタマイズしないと要件を満たせないなら、 JSON から読み込ように設定することによる記述量の増加は気にならないとは思う。 それはそれとして、 DSL による設定のしやすさは捨てがたいので、 認証フィルタをカスタマイズするなら DSL まで実装したいと思った。