【Java】SpringBootでJWT認証する

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

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

https://github.com/daisuketakakuwa/keycloak-practice

## SpringSecurity JWT検証フロー

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

　■ **文字列としてのJWT**
　・HTTPリクエストで送られてきた文字列
　　↓
　■ **クラスとしてのJWT**
　・[SpringSecurityで用意されているJWTクラス](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/oauth2/jwt/Jwt.html)
　・文字列として受け取ったJWTをオブジェクトとして管理する用のクラス
　　↓
　■ **Tokenクラス**
　・**検証済**のJWT情報を入れておくクラス
　・SpringSecurityの管理対象のクラスがこれ。
　・SpringSecurityの仕様で「[Authenticationインタフェース](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/core/Authentication.html)の実装クラス」がこれに該当する。

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

　文字列としてのJWT → クラスとしてのJWT ... [JwtDecoderインタフェース](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/oauth2/jwt/JwtDecoder.html)
　クラスとしてのJWT → Tokenクラス ... [Converterインタフェース](https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html)

## 必要な依存関係

```xml
<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インタフェース](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/core/Authentication.html)の実装クラス
　　- CustomPrincipalクラス 　// CustomTokenクラスを完成させるために必要なクラス
　　- CustomRoleクラス　 　　// CustomTokenクラスを完成させるために必要なクラス

③ ②を生成するためのクラス
　- CustomJwtConverterクラス ※[Converterインタフェース](https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html)の実装クラス


## ソースコード

### SecurityConfigクラス
・SpringSecurityの設定を行うクラス

```java
@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 ->
                 // ２つの変換処理で利用するクラス(ConverterIF実装クラス/JwtDecoderIF実装クラス)をここでセットする 
                 jwtCustomizer.jwtAuthenticationConverter(customJwtConverter).decoder(jwtDecoder))
        );
    }
}
```

### CustomTokenクラス
・検証済のJWT情報を入れておくクラス
・[Authenticationインタフェース](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/core/Authentication.html)に沿って実装されている必要がある。
・`AbstractAuthenticationToken`クラスは、[Authenticationインタフェース](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/core/Authentication.html)の実装クラス/Baseクラスである。
・Authenticationインタフェースの実装クラスを作る際は、BaseクラスであるAbstractAuthenticationTokenをextendsして必要なメソッドを@OverrideすればOK。

```java
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インタフェース](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/core/Authentication.html)のgetPrincipal()で返す入れ物クラスとして利用される
・@AuthenticationPrincipalで参照される
```java
@Builder
@Data
public class CustomPrincipal implements Serializable {
    private String id;
    private String name;
}
```

### CustomRoleクラス
・[Authenticationインタフェース](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/core/Authentication.html)のgetAuthorities()で返す入れ物クラスとして利用される。
・getAuthorities()の戻り値型が`Collection<? extends GrantedAuthority>`であるため「GrantedAuthorityの実装クラス」としてCustomRoleクラスを実装する必要がある
・Keycloakが生成したJWT内の格納されているロール情報はここに格納する。


```java
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クラス](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/oauth2/jwt/Jwt.html) → AuthenticationIFを実装したCustomTokenクラス へ変換する。
・[Converterインタフェース](https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html)を`implements Converter<Jwt, CustomToken>`で型指定＆IFとして提供されているconvert()を@Overrideして実装する。

```java
@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を用意
```java
@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();
    }
}

```

・アクセス制御の設定
```java
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/8cae1660-0e5f-0252-3259-ebc874aaa497.png)


　/admin/hello
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/342864d9-5147-54d7-90e4-00141a191ee0.png)

・AuthorizationヘッダにはJWTが格納されている
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/372e64d8-cb15-8032-29a7-41081ff31652.png)


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

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

・hasRole, hasAuthorityの違い