9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Java】SpringBootでJWT認証する

Last updated at Posted at 2022-02-28

まえがき

Keycloakで生成したIDトークン(JWT)をSpringBootで検証するための設定方法を、自分でOUTPUTして手順化したかったのでこの記事にまとめる。

ソースだけ見たい方向け。

SpringSecurity JWT検証フロー

① リクエストを受け取る
② リクエストのAuthorizationヘッダ内にあるJWTを取得
③ ②のJWTを下記の流れで変換して、SpringSecurity配下でJWT情報を管理する

 ■ 文字列としてのJWT
 ・HTTPリクエストで送られてきた文字列
  ↓
 ■ クラスとしてのJWT
 ・SpringSecurityで用意されているJWTクラス
 ・文字列として受け取ったJWTをオブジェクトとして管理する用のクラス
  ↓
 ■ Tokenクラス
 ・検証済のJWT情報を入れておくクラス
 ・SpringSecurityの管理対象のクラスがこれ。
 ・SpringSecurityの仕様で「Authenticationインタフェースの実装クラス」がこれに該当する。

↑の処理フローでは2回の変換処理がある。それぞれ変換処理を担うインタフェースが用意されている。

 文字列としてのJWT → クラスとしてのJWT ... JwtDecoderインタフェース
 クラスとしてのJWT → Tokenクラス ... Converterインタフェース

必要な依存関係

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.22</version>
  <scope>provided</scope>
</dependency>

用意するクラス

① SpringSecurity設定用クラス
 - SecurityConfigクラス

② 検証済のJWTの情報をSpringSecurityで管理するための入れ物クラス
 - CustomTokenクラス ※Authenticationインタフェースの実装クラス
  - CustomPrincipalクラス  // CustomTokenクラスを完成させるために必要なクラス
  - CustomRoleクラス    // CustomTokenクラスを完成させるために必要なクラス

③ ②を生成するためのクラス
 - CustomJwtConverterクラス ※Converterインタフェースの実装クラス

ソースコード

SecurityConfigクラス

・SpringSecurityの設定を行うクラス

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CustomJwtConverter customJwtConverter;
    // 本当は@Bean化してそこでインスタンス化すべき。説明用にここに定義。
    // JWTの発行者(issuer)であるKeycloakのURLをフォーマット(${ホスト名}/auth/realms/${realm名})に沿って指定する
    JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation("http://localhost:8180/auth/realms/user");

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors();
        http.csrf().disable();

        // リクエスト制御
        http.authorizeRequests(
           requests -> requests
               .antMatchers(HttpMethod.GET, "/admin/hello").hasRole("user")
               .antMatchers(HttpMethod.GET, "/user/hello").permitAll());

        // リソースサーバ関連の設定
        http.oauth2ResourceServer(
            oauth2ResourceServerCustomizer -> 
              oauth2ResourceServerCustomizer.jwt(jwtCustomizer ->
                 // 2つの変換処理で利用するクラス(ConverterIF実装クラス/JwtDecoderIF実装クラス)をここでセットする 
                 jwtCustomizer.jwtAuthenticationConverter(customJwtConverter).decoder(jwtDecoder))
        );
    }
}

CustomTokenクラス

・検証済のJWT情報を入れておくクラス
Authenticationインタフェースに沿って実装されている必要がある。
AbstractAuthenticationTokenクラスは、Authenticationインタフェースの実装クラス/Baseクラスである。
・Authenticationインタフェースの実装クラスを作る際は、BaseクラスであるAbstractAuthenticationTokenをextendsして必要なメソッドを@OverrideすればOK。

public class CustomToken extends AbstractAuthenticationToken {

    public CustomToken(Collection<? extends GrantedAuthority> authorities, CustomPrincipal principal) {
        // ここで渡したAuthority(文字列のRole)が、hasRole(),hasAuthority()のチェック対象となる
        super(authorities);
        this.principal = principal;
    }

    // @AuthenticationPrincipalで参照されるPrincipalとなる
    private CustomPrincipal principal;

    // Authenticationインタフェースで用意されているgetPrincipal()を@Override
    @Override
    public CustomPrincipal getPrincipal() {
        return principal;
    }

    // Authenticationインタフェースで用意されているgetCredentials()を@Override
    @Override
    public Object getCredentials() {
        return null;
    }

}

CustomPrincipalクラス

AuthenticationインタフェースのgetPrincipal()で返す入れ物クラスとして利用される
@AuthenticationPrincipalで参照される

@Builder
@Data
public class CustomPrincipal implements Serializable {
    private String id;
    private String name;
}

CustomRoleクラス

AuthenticationインタフェースのgetAuthorities()で返す入れ物クラスとして利用される。
・getAuthorities()の戻り値型がCollection<? extends GrantedAuthority>であるため「GrantedAuthorityの実装クラス」としてCustomRoleクラスを実装する必要がある
・Keycloakが生成したJWT内の格納されているロール情報はここに格納する。

public class CustomRole implements GrantedAuthority {

    private String role;

    public CustomRole(String role) {
        this.role = role;
    }

    @Override
    public String getAuthority() {
        // hasRole()でチェックするときに「ROLE_」をprefixとして付与されるように。
        return "ROLE_" + role;
    }

}

CustomJwtConverterクラス

JWTクラス → AuthenticationIFを実装したCustomTokenクラス へ変換する。
Converterインタフェースimplements Converter<Jwt, CustomToken>で型指定&IFとして提供されているconvert()を@Overrideして実装する。

@Component
public class CustomJwtConverter implements Converter<Jwt, CustomToken> {
    private static final String CLAIM_KEY_ID = "sub";
    private static final String CLAIM_KEY_USERNAME = "preferred_username";
    private static final String CLAIM_KEY_ROLES = "roles";

    @Override
    public CustomToken convert(Jwt token) {
        // 1. principal(id,username)の作成
        String id = token.getClaimAsString(CLAIM_KEY_ID);
        String name = token.getClaimAsString(CLAIM_KEY_USERNAME);
        CustomPrincipal principal = CustomPrincipal.builder().id(id).name(name).build();

        // 2. authoritiesの作成 AS CustomRoleクラス
        Collection<CustomRole> authorities = Collections.emptyList();
        if (token.getClaims().containsKey(CLAIM_KEY_ROLES)) {
            authorities = token.getClaimAsStringList(CLAIM_KEY_ROLES).stream().map(CustomRole::new)
                    .toList();
        }

        // CustomToken生成
        CustomToken customToken = new CustomToken(authorities, principal);
        // AuthenticationIFで用意されているsetAuthenticated()で更新
        // これをtrueにしないと401エラーで怒られる
        customToken.setAuthenticated(true);
        return customToken;
    }

}

動作確認

・動作確認用のControllerを用意

@CrossOrigin
@RestController
public class SampleController {

    @GetMapping("/user/hello")
    public String userHello() {
        return "Any user can call this api.";
    }

    @GetMapping("/admin/hello")
    public String adminHello(@AuthenticationPrincipal CustomPrincipal principal) {
        return "HELLO " + principal.getName();
    }
}

・アクセス制御の設定

http.authorizeRequests(requests -> requests
    .antMatchers(HttpMethod.GET, "/admin/hello").hasRole("user")  // Authentication.getPrincipal()で返されるAuthorityの中に「user」があること
    .antMatchers(HttpMethod.GET, "/user/hello").permitAll() // 認証無でアクセス可能
);

・クライアントアプリからアクセス

 /user/hello
image.png

 /admin/hello
image.png

・AuthorizationヘッダにはJWTが格納されている
image.png

SpringSecurity仕様関連のMEMO ※後で書き足していく

・Jwtクラスらへんの継承関係

・hasRole, hasAuthorityの違い

9
6
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
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?