🔒JWT認証フィルター付きのAPI(Spring)をローカル動作できるようにする。
まえがき
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プロジェクトを作成する。
・APIを作成したい -> spring-boot-starter-web
・SwaggerUIを使ってAPI動作検証したい -> swagger-core, springdoc-openapi-ui
<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作成
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ヘッダにセットできるようにする。
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作成
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として登録する。
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;
}
}
https://jwt.io/
2. トークンを生成トークンの署名アルゴリズムとして良く採用される以下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
の中から対象の公開鍵を取得する。
{
"keys": [
{
"kid": "test",
"kty": "RSA",
"n": "u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0_IzW7yWR7QkrmBL7jTKEn5u-qKhbwKfBstIs-bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW_VDL5AaWTg0nLVkjRo9z-40RQzuVaE8AkAFmxZzow3x-VJYKdjykkJ0iT9wCS0DRTXu269V264Vf_3jvredZiKRkgwlL9xNAwxXFg0x_XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC-9aGVd-Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw",
"e": "AQAB"
}
]
}
expressで/jwks
エンドポイントから↑のJSON(JWKS)を返すように設定する。
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