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
を返却する動作とします。
@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
を作成します。
@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を作成します。
@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_token
をx-www-form-urlencoded
で/login
にPOSTするよう実装した状態。
パラメーター名はgoogle_id_token
としました。
SecurityConfigを修正
作成済みのSecurityConfig#configure(HttpSecurity http)
のformLogin()
の箇所を以下のようにします。
.formLogin()
.passwordParameter("google_id_token")
.successHandler(authenticationSuccessHandler)
passwordParameter("google_id_token")
を追加しただけです。
これで、デフォルトであるユーザー名とパスワードによる認証のパスワードとしてid_token
を受け取ります。
AuthenticationProvider
を実装
ここまででid_token
を受け取れるようになっているので、後は認証を実装するだけです。
@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
クラスは以下の通り。
@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に対応できました。