LoginSignup
9
14

More than 5 years have passed since last update.

Spring Boot + Spring Security な REST API で Google Sign-In してみた

Posted at

Spring Bootで作ったREST APIサーバに、Google Sign-Inでログインできるよう対応したので、やったことを書いておきます。

使っているSpring Bootのバージョンは1.5.7です。
なお、以下でやっていることのほとんどは@EnableOAuth2Ssoで実現できてしまうかも知れないですが、それでは面白くないというか、アレなので。

準備(認証のREST API化)

Google Sign-Inの対応の前に、Spring Securityのデフォルト動作をRESTな感じに変更します。

参考サイト:http://www.baeldung.com/securing-a-restful-web-service-with-spring-security

エントリーポイント

標準的なWebアプリでは、未認証状態でセキュアなリソースへアクセスすると自動的に認証を促す動作となりますが、RESTサービスでは明示的に認証を行うため、このような場合には単に401を返却する動作とします。

RestAuthenticationEntryPoint.java
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
  }
}

ログイン成功時に301ではなく200を返す

デフォルトではログイン成功時に301を返してログイン後の画面へ誘導する動作となっていますが、RESTでは単に200を返すのみとします。
SavedRequestAwareAuthenticationSuccessHandlerを参考にリダイレクトしないAuthenticationSuccessHandlerを作成します。

RestSavedRequestAwareAuthenticationSuccessHandler.java
@Component
public class RestSavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

  private RequestCache requestCache = new HttpSessionRequestCache();

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request,
                                      HttpServletResponse response, Authentication authentication) {
    SavedRequest savedRequest = requestCache.getRequest(request, response);

    if (savedRequest == null) {
      clearAuthenticationAttributes(request);
      return;
    }
    String targetUrlParameter = getTargetUrlParameter();
    if (isAlwaysUseDefaultTargetUrl()
      || (targetUrlParameter != null && StringUtils.hasText(request
      .getParameter(targetUrlParameter)))) {
      requestCache.removeRequest(request, response);
      clearAuthenticationAttributes(request);
      return;
    }

    clearAuthenticationAttributes(request);
  }
}

ログイン失敗時に302ではなく401を返す

同様に、ログイン失敗時は単に401を返すようにします。
こちらはSpringのSimpleUrlAuthenticationFailureHandlerがそのまま使えます。

ログアウト時も200を返すだけ

同じく、ログアウト時もリダイレクトせず200を返すのみとします。
こちらも、SpringのSimpleUrlLogoutSuccessHandlerが使えます。

SecurityConfig

ここまでの状態でConfigurationを作成します。

SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private RestAuthenticationEntryPoint authenticationEntryPoint;

  @Autowired
  private RestSavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler;

  @Autowired
  private SimpleUrlAuthenticationFailureHandler authenticationFailureHandler;

  @Autowired
  private SimpleUrlLogoutSuccessHandler logoutSuccessHandler;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .csrf().disable()
      .authorizeRequests()
        .antMatchers("/api/**").authenticated()
        .and()
      .exceptionHandling()
        .authenticationEntryPoint(authenticationEntryPoint)
        .and()
      .formLogin()
        .successHandler(authenticationSuccessHandler)
        .failureHandler(authenticationFailureHandler)
        .and()
      .logout()
        .logoutSuccessHandler(logoutSuccessHandler)
    ;
  }

  @Bean
  public SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() {
    return new SimpleUrlAuthenticationFailureHandler();
  }

  @Bean
  public SimpleUrlLogoutSuccessHandler simpleUrlLogoutSuccessHandler() {
    return new SimpleUrlLogoutSuccessHandler();
  }
}

Google Sign-Inを実装する

いよいよ本題。
ココココを参考に。
Webアプリ(フロント側)からは、サインイン時に取得できるid_tokenx-www-form-urlencoded/loginにPOSTするよう実装した状態。
パラメーター名はgoogle_id_tokenとしました。

SecurityConfigを修正

作成済みのSecurityConfig#configure(HttpSecurity http)formLogin()の箇所を以下のようにします。

SecurityConfig.java
      .formLogin()
        .passwordParameter("google_id_token")
        .successHandler(authenticationSuccessHandler)

passwordParameter("google_id_token")を追加しただけです。
これで、デフォルトであるユーザー名とパスワードによる認証のパスワードとしてid_tokenを受け取ります。

AuthenticationProviderを実装

ここまででid_tokenを受け取れるようになっているので、後は認証を実装するだけです。

GoogleIdAuthenticationProvider.java
@Component
public class GoogleIdAuthenticationProvider implements AuthenticationProvider {

  private final String clientId = "XXX.apps.googleusercontent.com";

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    String tokenString = (String) authentication.getCredentials();
    GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(UrlFetchTransport.getDefaultInstance(), JacksonFactory.getDefaultInstance())
      .setAudience(singletonList(clientId))
      .build();
    GoogleIdToken idToken;
    try {
      idToken = verifier.verify(tokenString);
      if (idToken == null) {
        throw new BadCredentialsException("Failed to verify token");
      }
    } catch (GeneralSecurityException|IOException e) {
      throw new AuthenticationServiceException("Failed to verify token", e);
    }

    GoogleIdToken.Payload payload = idToken.getPayload();
    String userId = payload.getSubject();
    String name = (String) payload.get("name");
    GoogleUser user = new GoogleUser(userId, name);
    List<GrantedAuthority> authorities = singletonList(new SimpleGrantedAuthority("ROLE_USER"));
    return new UsernamePasswordAuthenticationToken(user, tokenString, authorities);
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return UsernamePasswordAuthenticationToken.class.equals(authentication);
  }
}

特定ドメインに制限したい場合は、payload.getHostedDomain()をチェックすることで実現できます。

GoogleUserクラスは以下の通り。

GoogleUser.java
@Data
@RequiredArgsConstructor
@AllArgsConstructor
public class GoogleUser implements UserDetails {

  @NotNull
  @Setter(AccessLevel.NONE)
  private String userId;

  private String username;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return null;
  }

  @Override
  public String getPassword() {
    return null;
  }

  @Override
  public boolean isAccountNonExpired() {
    return false;
  }

  @Override
  public boolean isAccountNonLocked() {
    return false;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return false;
  }

  @Override
  public boolean isEnabled() {
    return false;
  }
}

これでGoogle Sign-Inに対応できました。

9
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
14