JwtProvider.java

package com.hwhub.backend.security;

import com.hwhub.backend.config.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
 * JWT(JSON Web Token)の生成・検証・解析を行うプロバイダ。
 *
 * <p>本コンポーネントはアプリケーションの認証基盤として機能し、 ログイン成功時のアクセストークン発行および API リクエスト時のトークン検証に利用される。
 *
 * <p>署名方式:
 *
 * <ul>
 *   <li>HMAC-SHA256(HS256)
 * </ul>
 *
 * <p>トークン構造:
 *
 * <pre>
 * Header:
 *   alg: HS256
 *
 * Payload:
 *   sub   : userId
 *   name  : displayName
 *   iat   : 発行時刻
 *   exp   : 有効期限
 * </pre>
 *
 * <p>注意:
 *
 * <ul>
 *   <li>秘密鍵は JwtProperties.secret から取得
 *   <li>トークン失効管理(passwordChangedAt 等)は別レイヤで実施
 * </ul>
 */
@Component
@RequiredArgsConstructor
public class JwtProvider {

  private final JwtProperties jwtProperties;

  private Key getSigningKey() {
    // secret を元に署名キー作成
    return Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8));
  }

  /**
   * JWT アクセストークンを生成する。
   *
   * <p>主な用途:
   *
   * <ul>
   *   <li>ログイン成功時
   *   <li>OAuth ログイン成功時
   * </ul>
   *
   * <p>格納されるクレーム:
   *
   * <ul>
   *   <li>sub : ユーザーID
   *   <li>name : 表示名
   *   <li>iat : 発行時刻
   *   <li>exp : 有効期限
   * </ul>
   *
   * @param userId ユーザーID
   * @param displayName 表示名
   * @return 署名済み JWT
   */
  public String generateToken(Long userId, String displayName) {
    Date now = new Date();
    Date expiry = new Date(now.getTime() + jwtProperties.getExpiryMillis());

    return Jwts.builder()
        .setSubject(String.valueOf(userId))
        .claim("name", displayName)
        .setIssuedAt(now)
        .setExpiration(expiry)
        .signWith(getSigningKey(), SignatureAlgorithm.HS256)
        .compact();
  }

  public String generateRefreshToken(Long userId) {
    Date now = new Date();
    Date expiry = new Date(now.getTime() + jwtProperties.getRefreshExpiryMillis());

    return Jwts.builder()
        .setSubject(String.valueOf(userId))
        .claim("type", "refresh")
        .setIssuedAt(now)
        .setExpiration(expiry)
        .signWith(getSigningKey(), SignatureAlgorithm.HS256)
        .compact();
  }

  public boolean validateRefreshToken(String token) {
    try {
      Claims claims =
          Jwts.parserBuilder()
              .setSigningKey(getSigningKey())
              .build()
              .parseClaimsJws(token)
              .getBody();
      return "refresh".equals(claims.get("type", String.class));
    } catch (JwtException | IllegalArgumentException e) {
      return false;
    }
  }

  /**
   * JWT の正当性を検証する。
   *
   * <p>検証内容:
   *
   * <ul>
   *   <li>署名一致
   *   <li>トークン形式
   *   <li>有効期限内か
   * </ul>
   *
   * <p>失敗時は例外を握りつぶして false を返す。
   *
   * @param token JWT
   * @return 有効な場合 true
   */
  public boolean validateToken(String token) {
    try {
      Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);
      return true;
    } catch (JwtException | IllegalArgumentException e) {
      return false;
    }
  }

  /**
   * JWT の subject(sub)からユーザーIDを取得する。
   *
   * <p>sub には userId が格納されている前提。
   *
   * @param token JWT
   * @return userId
   */
  public Long getUserIdFromToken(String token) {
    return Long.parseLong(parseClaims(token).getSubject());
  }

  /**
   * JWT の発行時刻(iat)を取得する。
   *
   * <p>用途例:
   *
   * <ul>
   *   <li>passwordChangedAt との比較によるトークン失効判定
   * </ul>
   *
   * @param token JWT
   * @return 発行日時
   */
  public Date getIssuedAtFromToken(String token) {
    return parseClaims(token).getIssuedAt();
  }

  private Claims parseClaims(String token) {
    return Jwts.parserBuilder()
        .setSigningKey(getSigningKey())
        .build()
        .parseClaimsJws(token)
        .getBody();
  }
}