はじめに
JWTって聞いたことあるけど、詳しく知らないと思い、調べてみることにしました。
基本的なことを調べ、SpringSecurityとauth0:java-jwtを使い実際に簡単な実装をしてみたいと思います。
目次
JWTって何?
JWTとはJsonWebTokenの略称であり、以下のようなJsonをBase64エンコードなどでURLセーフにしたものです
あくまでJsonの表示形式を決めただけのもので、これ自体が認証するわけではないありません
{
"iss": "jwt-demo-server",
"sub": "1",
"jti": "89ce044c-6fbe-4e99-b465-089e0d5c2aba",
"iat": 1722736918,
"exp": 1722740518
}
このJsonのキーの説明
キー名 | 説明 |
---|---|
iss | JWTの発行者 |
sub | ユーザの識別子など |
jti | JWTの一意な識別子 |
iat | JWTの発行日時 |
exp | JWTの有効期限 |
どう認証するのか?
JWS(JsonWebSignature)というJWTから作る検証シグネチャによって認証を行います
JWSは以下のように作成されます
まずは暗号アルゴリズムとペイロード(JWT)の種別を表すヘッダーを用意します
{
"alg": "HS256",
"typ": "JWT"
}
次にURLエンコードしたヘッダーとJWTを暗号キーで暗号化したシグネチャを作成します
シグネチャ = 「{ヘッダと言われるJsonをURLエンコードしたもの} . {JWT}」を暗号化キーで暗号化
それぞれ作成したものを「.」で繋いだものがJWS
JWS = {ヘッダと言われるJsonをURLエンコードしたもの} . {JWT} . {シグネチャをURLエンコードしたもの}
簡単に実装してみた
実装
今回はauth0:java-jwtとSpring Securityを使って認証機能を作っていきます
以下認証までの流れです
1. /auth/login にemailとpasswordでリクエストを送る
2. LoginControllerでリクエストを受け取り、emailとpasswordで認証。ユーザIDを含んだJWSを作成し、返却する。
3. /hello のHeaderに発行したJWSをつけ、リクエストを送る
4. filterでJWSで認証する
5. "Hello, World!"が返される
build.gradleにライブラリを追加
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.auth0:java-jwt:4.4.0'
JwtUtilsを作成
ここでJWSの作成・認証を行います
本来なら暗号化キーはもっと厳重に管理した方が良いですが、今回は簡単な実装なためコードにベタ書しています。
public class JwtUtils {
private static final Long EXPIRATION_TIME = 1000L * 60L * 60L;
private static final String SECRET_KEY = "secret";
public static String build(Integer userId) {
Date issuedAt = new Date();
Date expiresAt = new Date(issuedAt.getTime() + EXPIRATION_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
return JWT.create()
.withIssuer("jwt-demo-server")
.withSubject(userId.toString())
.withJWTId(UUID.randomUUID().toString())
.withIssuedAt(issuedAt)
.withExpiresAt(expiresAt)
.sign(algorithm);
}
public static DecodedJWT verify(String token) {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("jwt-demo-server")
.build();
return verifier.verify(token);
}
}
LoginControllerを作成
受け取ったemailとpasswordで認証し、ユーザIDを取得します。
そのユーザIDを利用し、JWSを作成します。
@RestController
@RequestMapping("/auth")
public class LoginController {
@Resource
AuthService authService;
@PostMapping("/login")
public ResponseEntity login(@RequestBody LoginRequest request) {
// emailとpasswordで認証し、ユーザID(1)を返す
Integer userId = authService.auth(request.getEmail(), request.getPassword());
String token = JwtUtils.build(userId);
return ResponseEntity.ok(token);
}
}
SecurityConfigを作成
今回はpostmanなどでAPIを直接叩いたので、csrfの設定を無効にしておくきます。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}
AuthorizeFilterを作成
OncePerRequestFilterというリクエストごとに毎回呼び出すクラスを継承し作成します。
matcherに合致しないリクエストのみを認証対象とします。
JWSがない or 認証できなければ401が返されます。
@Component
public class AuthorizeFilter extends OncePerRequestFilter {
private final AntPathRequestMatcher matcher = new AntPathRequestMatcher("/auth/login");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!matcher.matches(request)) {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.replace("Bearer ", "");
try {
DecodedJWT decodedJWT = JwtUtils.verify(token);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
filterChain.doFilter(request, response);
}
}
HelloControllerを作成
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("")
public String hello() {
return "Hello, World!";
}
}
検証
まずは/auth/login にリクエストを送り、JWSを取得します
次に/hello へJWSをつけてリクエストを送ると、Hello, World!と返ってきます
まとめ
今回はJWTについて調べ、それを使って簡単に認証機能を作ってみました。
JWTとして送る情報によって、認可処理とかも入れることができるようなので、次は入れてみたいと思います。