JWTトークンへの追加情報実装
本記事では、Spring Cloud Alibaba環境でJWTトークンにカスタム情報を追加し、リソースサーバーでこれらのデータを取得する方法について解説します。
主に以下の3つの技術ポイントをカバーします:
- JWTトークンへのカスタムデータ追加方法
- ユーザーIDや携帯番号などの追加情報をJWTに含める方法
- リソースサーバーでカスタムデータを取得する方法
JWTトークンへのカスタムデータ追加
JWTトークンにカスタムデータを追加するには、以下の手順を実行します。
まず、カスタムトークンエンハンサーを作成します。
package com.example.auth.security;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.HashMap;
import java.util.Map;
/**
* カスタムJWTトークンコンバーター
*/
public class EnhancedJwtTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Object principal = authentication.getUserAuthentication().getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>(4);
additionalInfo.put("creator", "java-tech-blog");
additionalInfo.put("platform", "spring-cloud");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return super.enhance(accessToken, authentication);
}
}
次に、認証サーバーの設定でこのカスタムトークンエンハンサーを設定します。
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
// カスタムJWT出力用コンバーター
JwtAccessTokenConverter converter = new EnhancedJwtTokenConverter();
// 対称署名の設定
converter.setSigningKey("secret-key-123");
return converter;
}
この設定により、生成されるJWTトークンにcreatorとplatformの2つの属性が含まれるようになります。
この機能が動作する理由は、DefaultTokenServicesクラスのcreateAccessTokenメソッドにあります:
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
システムでaccessTokenEnhancerが設定されている場合、トークン生成時にenhance()メソッドが呼び出され、JWTトークンに追加情報が付加されます。
JWTへのユーザー追加情報実装
ユーザー固有の情報(IDや携帯番号など)をJWTに追加するには、EnhancedJwtTokenConverterを拡張する必要があります。これを実現するには、カスタムUserDetailsオブジェクトを作成し、ユーザー情報を含める必要があります。
まず、カスタムUserDetailsを実装します。
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
* カスタムユーザー情報
*/
@Getter
public class CustomUserDetails extends User {
private Integer userId;
private String phoneNumber;
public CustomUserDetails(Integer userId, String phoneNumber,
String username, String password,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.userId = userId;
this.phoneNumber = phoneNumber;
}
}
次に、UserDetailsServiceでカスタムオブジェクトを返すように変更します。
private UserDetails buildUserDetails(AppUser appUser) {
Set<String> authSet = new HashSet<>();
List<String> roles = appUser.getRoles();
if(!CollectionUtils.isEmpty(roles)){
roles.forEach(role -> authSet.add("ROLE_" + role));
authSet.addAll(appUser.getPermissions());
}
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(authSet.toArray(new String[0]));
return new CustomUserDetails(
appUser.getId(),
appUser.getPhoneNumber(),
appUser.getUsername(),
appUser.getPassword(),
authorityList
);
}
最後に、エンハンサーメソッドで現在のユーザー情報を取得し設定します。
public class EnhancedJwtTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getUserAuthentication().getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>(4);
additionalInfo.put("userId", userDetails.getUserId());
additionalInfo.put("phoneNumber", userDetails.getPhoneNumber());
// 他のカスタム情報も追加可能
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return super.enhance(accessToken, authentication);
}
}
リソースサーバーでのカスタム情報取得
上記の設定でJWTトークンにユーザーデータを追加できますが、リソースサーバーではSecurityContextHolder.getContext().getAuthentication().getPrincipal()を使用してもユーザー名しか取得できません。
これは、デフォルトのJwtAccessTokenConverterがDefaultUserAuthenticationConverterのextractAuthenticationメソッドを呼び出してトークンからユーザー情報を取得するためです。デフォルト実装を見てみましょう。
public class DefaultUserAuthenticationConverter implements UserAuthenticationConverter {
// ...
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}
// ...
}
UserDetailServiceが注入されていない場合、OAuth2はユーザー名のみを取得します。UserDetailServiceが注入されている場合、すべてのユーザー情報を返すことができます。
この問題を解決するには、2つのアプローチがあります:
- リソースサーバーに
UserDetailServiceを注入する(推奨されない - 認証サーバーとリソースサーバーが分離されている場合、不必要な結合が生じる) DefaultUserAuthenticationConverterを拡張し、extractAuthenticationメソッドをオーバーライドして追加データを手動で取得する
ここでは2番目のアプローチを実装します。
まず、カスタムトークンパーサーを作成します。
package com.example.common.security.component;
import com.example.common.security.user.CustomUserDetails;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.Map;
public class CustomUserAuthConverter extends DefaultUserAuthenticationConverter {
/**
* ユーザーデータ抽出メソッドのオーバーライド
*/
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
String username = (String) map.get(USERNAME);
Integer userId = (Integer) map.get("userId");
String phoneNumber = (String) map.get("phoneNumber");
CustomUserDetails user = new CustomUserDetails(userId, phoneNumber, username, "N/A", authorities);
return new UsernamePasswordAuthenticationToken(user, "N/A", authorities);
}
return null;
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
Object authorities = map.get(AUTHORITIES);
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
}
if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.collectionToCommaDelimitedString((Collection<?>) authorities));
}
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
次に、カスタムトークンコンバーターを作成し、カスタムパーサーを注入します。
public class CustomTokenConverter extends DefaultAccessTokenConverter {
public CustomTokenConverter() {
super.setUserTokenConverter(new CustomUserAuthConverter());
}
}
最後に、リソースサーバーの設定クラスでカスタムトークンコンバーターを設定します。
@Bean
public JwtAccessTokenConverter jwtTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("secret-key-123");
converter.setAccessTokenConverter(new CustomTokenConverter());
return converter;
}
以上の設定により、SecurityContextHolder.getContext().getAuthentication().getPrincipal()メソッドを呼び出すと、ユーザーの追加情報を取得できるようになります。
さらに、コンテキストから直接ユーザー情報を取得するためのユーティリティクラスを作成することもできます。
public class AuthUtils {
/**
* 認証情報の取得
*/
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
public static CustomUserDetails getCurrentUser() {
Authentication authentication = getAuthentication();
if (authentication == null) {
return null;
}
return getUser(authentication);
}
/**
* 現在のユーザーの取得
* @param authentication 認証情報
* @return 現在のユーザー
*/
private static CustomUserDetails getUser(Authentication authentication) {
Object principal = authentication.getPrincipal();
if (principal instanceof CustomUserDetails) {
return (CustomUserDetails) principal;
}
return null;
}
}