🔒JWT認証フィルター付きのAPI(Spring)をローカル動作できるようにする。

2023/01/19に公開

まえがき

JWT認証フィルター付きのJavaAPIをローカル動作させるために必要な手順をまとめておく。
・SpringBootでJWT認証フィルターの作成
・ローカル動作で利用するトークンの生成
 - トークンの生成 → https://jwt.io/
 - 公開鍵をPEMからJWKに変換 → https://irrte.ch/jwt-js-decode/pem2jwk.html
・公開鍵を配布するJWKSサーバ構築 by NodeJS

1. SpringでJWT認証フィルター付きのAPIを作成する。

SpringBootプロジェクト作成

以下を参考にSpringBootプロジェクトを作成する。

https://zenn.dev/crsc1206/books/d8166194fd58f2/viewer/f40920

・APIを作成したい -> spring-boot-starter-web
・SwaggerUIを使ってAPI動作検証したい -> swagger-core, springdoc-openapi-ui

pom.xml
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

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

  <!-- Run this application as API server -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-core</artifactId>
    <version>1.6.6</version>
    <scope>compile</scope>
  </dependency>

  <!-- This will automatically deploy swagger-ui to a spring-boot application: -->
  <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.6</version>
    <scope>compile</scope>
  </dependency>

  <!-- jwt -->
 <dependency>
   <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
   <version>4.2.2</version>
 </dependency>	
	
</dependencies>

HelloController作成

HelloController.java
package oidc.practice.oidcpractice.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "HELLO WORLD";
    }

}

SwaggerUI経由でAPIコールできるようにする。

アプリ起動
mvn clean spring-boot:run

SwaggerUI(http://localhost:8080/swagger-ui/index.html )を開き、/helloをコールできることを確認👍

トークンをAuthorizationヘッダにセットできるようにする。

SwaggerUiConfig.java
package oidc.practice.oidcpractice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;

@Configuration
public class SwaggerUiConfig {

    @Bean
    public OpenAPI createRestApiWithSecurityScheme() {
        return new OpenAPI().components(new Components()
                // Add security scheme for Authorization Bearer token
                .addSecuritySchemes("basicScheme", new SecurityScheme()
                        .type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("jwt")))
                .addSecurityItem(new SecurityRequirement().addList("basicScheme"));
    }
}

この設定が適用された状態でSwaggerUIを開いて、↓のように「Authorize🔒」があればOK👍

JWT認証用のFilter作成

JwtFilter.java
package oidc.practice.oidcpractice.filter;

import java.io.IOException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.JwkProviderBuilder;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

public class JwtFilter implements Filter {

    public JwtFilter() {
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("########## Initiating Jwtfilter ##########");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String authorizaionHeader = request.getHeader("Authorization");
        if (authorizaionHeader != null) {
            Boolean bToken = false;
            String sMessage = "";
            try {
                String token = authorizaionHeader.replaceFirst("Bearer ", "");
                DecodedJWT jwt = JWT.decode(token);
                String jwksUrl = "http://localhost:3001/jwks";
                JwkProvider provider = new JwkProviderBuilder(new URL(jwksUrl)).build();
                // トークン内に格納されてるKeyIDに紐づく公開鍵を取得する。
                Jwk jwk = provider.get(jwt.getKeyId());
                // 署名アルゴリズムは「RS256」を選択。
                Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
                // 検証 -> NGであれば SignatureVerificationException が投げられる。
                algorithm.verify(jwt);
                bToken = true;
            } catch (Exception e) {
                sMessage = e.getMessage();
                bToken = false;
            }

            if (bToken) {
                // call next filter in the filter chain
                filterChain.doFilter(request, response);
            } else {
                response.sendError(401, "not authorized -> " + sMessage);
                return;
            }
        } else {
            response.sendError(401, "not authorized -> " + "No authorization header found.");
            return;
        }
    }
}

JWT認証用のFilterをFilterとして登録

FilterRegistrationBeanクラスを利用して、先ほど作成したJwtFilterクラスをFilterとして登録する。

ApplicationConfig.java
package oidc.practice.oidcpractice.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import oidc.practice.oidcpractice.filter.JwtFilter;

@Configuration
public class ApplicationConfig {

    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        JwtFilter jwtFilter = new JwtFilter();
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(jwtFilter);
        registrationBean.addUrlPatterns("/hello/*");

        return registrationBean;
    }
}

2. トークンを生成 https://jwt.io/

トークンの署名アルゴリズムとして良く採用される以下2つの内、公開鍵をJWKS_URLで配布するRS256を採用する。

署名アルゴリズム 内容
HS256 共通鍵を用いて署名を作成。共通鍵を用いて署名を検証。
RS256 秘密鍵を用いて署名を作成。公開鍵を用いて署名を検証。

「RS256」を選択。

以下の3パートでトークンは構成される。

Part Description
HEADER/ヘッダ部 👉'alg'に署名アルゴリズム🔒を書いておく必要がある。
👉公開鍵を取得する際にkidが必要なので記載しておくこと。
PAYLOAD/ペイロード部 トークンに詰め込みたい情報はここに格納する。
SIGNATURE/署名 "ヘッダ部をBASE64URLエンコードした文字列" + "." + "ペイロード部をBASE64URLエンコードした文字列"をINPUTに、秘密鍵を利用して作成した署名

赤枠のところが署名を検証するための公開鍵なので、この公開鍵をJWKS_URLから取得できるようにする👍

公開鍵をPEM形式 → JWKS形式に変換する。

先ほどhttps://jwt.io/ で生成したトークンの公開鍵(PEM形式)をPEM to JWKでJWK形式に変換する。

3. 公開鍵を配布するJWKSサーバ構築

JWKSをexpressでAPIで取得できるようにする。

mkdir jwks-provider
cd jwks-provider
npm init
npm install express

✅先ほどPEM→JWKに変換したものをkeysの配列内に定義。
✅このとき"kid": "test"を追加する → 👉公開鍵を取得する人はこのkidをキーにkeysの中から対象の公開鍵を取得する。

keyPairs.json
{
    "keys": [
        {
            "kid": "test",
            "kty": "RSA",
            "n": "u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0_IzW7yWR7QkrmBL7jTKEn5u-qKhbwKfBstIs-bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW_VDL5AaWTg0nLVkjRo9z-40RQzuVaE8AkAFmxZzow3x-VJYKdjykkJ0iT9wCS0DRTXu269V264Vf_3jvredZiKRkgwlL9xNAwxXFg0x_XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC-9aGVd-Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw",
            "e": "AQAB"
        }
    ]
}

expressで/jwksエンドポイントから↑のJSON(JWKS)を返すように設定する。

index.js
const express = require('express');
const keyList = require('./keyPairs.json');

const app = express();

app.get('/jwks', (req, res) => {
    res.send(keyList);
});

app.listen(3001, () => {
    console.log('jwks server listening on 3001');
});

node index.jsで起動。http://localhost:3001/jwks で下記JSONを取得できればOK👍

C:\Users\daisu>curl http://localhost:3001/jwks
{
	"keys": [
		{
			"kid": "test",
			"kty": "RSA",
			"n": "u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0_IzW7yWR7QkrmBL7jTKEn5u-qKhbwKfBstIs-bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW_VDL5AaWTg0nLVkjRo9z-40RQzuVaE8AkAFmxZzow3x-VJYKdjykkJ0iT9wCS0DRTXu269V264Vf_3jvredZiKRkgwlL9xNAwxXFg0x_XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC-9aGVd-Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw",
			"e": "AQAB"
		}
	]
}

4. 動作確認

SpringBootアプリ起動。

mvn clean spring-boot:run

JWKSサーバ起動。

node index.js

SwaggerUIを開き、🔒Authorizeに先ほどのトークンを設定する。


あとはAPIをコールすれば、JWT認証Filterにより署名検証が行われた上でAPIがコールされたことになる👍

Discussion