Spring Cloud AlibabaにおけるJWTトークンのカスタマイズ方法

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トークンにcreatorplatformの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()を使用してもユーザー名しか取得できません。

これは、デフォルトのJwtAccessTokenConverterDefaultUserAuthenticationConverterextractAuthenticationメソッドを呼び出してトークンからユーザー情報を取得するためです。デフォルト実装を見てみましょう。

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つのアプローチがあります:

  1. リソースサーバーにUserDetailServiceを注入する(推奨されない - 認証サーバーとリソースサーバーが分離されている場合、不必要な結合が生じる)
  2. 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;
    }
}

タグ: Spring Cloud JWT OAuth2 マイクロサービス セキュリティ

7月11日 00:06 投稿