今回はめちゃシンプルに「ユーザー名とパスワードでログイン → JWT発行 → 以降のAPIアクセスはトークン認証」って流れにする。
ざっくり構成
Spring Boot
├── SecurityConfig(セキュリティ設定)
├── JwtUtil(トークン発行・検証)
├── JwtAuthenticationFilter(リクエストのJWTチェック)
├── UserDetailsService(ユーザー情報取得)
├── AuthController(ログイン用API)
└── SampleController(認証が必要なAPI)
実装する流れ(ざっくり)
- 依存ライブラリ追加
- JwtUtil作成
- 認証フィルター作成
- SecurityConfig作成
- UserDetailsService実装
- ログインAPI作成
- フロント側でAPIにトークンの付与と保持
- 動作確認
サンプルコード
① 依存ライブラリ(build.gradle)
これをpom.xmlに追加。今回は上の方で実装しました。
jjwt-api → API の定義
jjwt-impl → 実装部分 (runtime スコープ)
jjwt-jackson → Jackson を使った JSON パース (runtime スコープ)
特徴
シンプルな API で JWT の生成・検証が可能
JWS (JSON Web Signature) や JWE (JSON Web Encryption) に対応
HMAC, RSA, ECDSA などの署名アルゴリズム をサポート
Jackson を使った JSON パース (jjwt-jackson が必要)
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
またはこっち!
特徴
Auth0 によって開発・メンテナンス
JWS (署名付き JWT) のみ対応 → JWE (暗号化) は非対応
よりシンプルな API
依存関係が少なく、軽量
まあ、簡単に説明するとJWE (暗号化) ができるかどうか位に認識しか今はしてないです...
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
もしspring sequrityが入ってない場合は下記も追加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
JwtUtil(トークンの発行と検証)
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
@Component
public class JwtUtil {
private final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 1時間の有効期限
.signWith(SECRET_KEY)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
JWT (JSON Web Token) を扱うためのユーティリティクラス JwtUtil です。 主に トークンの生成・検証・ユーザー名の抽出 を行います。
このコードの動作
generateToken(username) → username を含む JWT を生成
extractUsername(token) → トークンから username を抽出
validateToken(token) → トークンが 改ざんされていないか検証
コードの解説
- クラス定義と SECRET_KEY の作成
@Component
public class JwtUtil {
private final Key SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
@Component → Spring によって Beanとして管理 される
SECRET_KEY → HMAC-SHA256 の 秘密鍵を生成
Keys.secretKeyFor(SignatureAlgorithm.HS256) は、適切な長さの秘密鍵を自動生成する
- generateToken(String username): JWT の生成
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 1時間の有効期限
.signWith(SECRET_KEY)
.compact();
}
setSubject(username) → トークンの対象(subject)として username をセット setIssuedAt(new Date()) → トークンの発行日時を現在時刻に設定 setExpiration(...) → 1時間後にトークンが無効化 される設定 signWith(SECRET_KEY) → 生成した秘密鍵 (SECRET_KEY) で署名
compact() → トークンを 最終的な文字列形式 に変換
- extractUsername(String token): トークンからユーザー名を取得
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
parseClaimsJws(token) → トークンを解析
getBody().getSubject() → トークンの中から username を取得
validateToken(String token): トークンの検証
まとめ
JWT を生成 (generateToken)
JWT から username を取得 (extractUsername)
JWT の正当性を検証 (validateToken)
Spring の @Component で Bean 管理
HMAC-SHA256 で署名を行い安全性を確保
このコードで 認証・セッション管理 ができるので、Spring Security と組み合わせて使うとより強力になります
JwtAuthenticationFilter(リクエスト毎にトークンチェック)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final MyUserDetailsService myUserDetailsService;
@Autowired
public JwtAuthenticationFilter(JwtUtil jwtUtil, MyUserDetailsService myUserDetailsService) {
this.jwtUtil = jwtUtil;
this.myUserDetailsService = myUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = request.getHeader("Authorization");//JWT(トークン)を取得.HTTPリクエストの Authorization ヘッダーからJWTを取得。通常、JWTは "Bearer <TOKEN>" という形式で送られてくる。
if (token != null && jwtUtil.validateToken(token.replace("Bearer ", ""))) {//トークンが存在するかチェック & 検証。token が null ではなく、かつ jwtUtil.validateToken() で正しいトークンかどうか検証。.replace("Bearer ", "") で "Bearer " を削除し、純粋なJWTの文字列を取得。
String username = jwtUtil.extractUsername(token.replace("Bearer ", ""));//ユーザー名を抽出
UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);//ユーザー情報の取得。UserDetailsService を使って、username に該当する ユーザー情報(権限など) を取得。
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(//認証オブジェクトを作成
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
filterChain.doFilter(request, response);//フィルタの継続
}
}
この JwtAuthenticationFilter クラスは、Spring Security を使って JWT を検証し、認証情報をセットするフィルター です。 認証済みのリクエストのみを処理するための重要な要素!
コードの解説
- クラス定義とフィールド
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final MyUserDetailsService myUserDetailsService;
@Component → Spring によって Bean管理 される
JwtUtil → JWT の生成・検証を担当するクラス
MyUserDetailsService → ユーザー情報を取得する UserDetailsService
- コンストラクタ (@Autowired)
@Autowired
public JwtAuthenticationFilter(JwtUtil jwtUtil, MyUserDetailsService myUserDetailsService) {
this.jwtUtil = jwtUtil;
this.myUserDetailsService = myUserDetailsService;
}
依存関係の注入 (JwtUtil と MyUserDetailsService) を Spring に任せることで、適切に管理できるようになってる。
- JWT 認証のフィルタ処理 (doFilterInternal)
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {3.
このメソッドは、すべてのリクエストに適用されるフィルター処理 を実装する部分です。
- Authorization ヘッダーからトークンを取得
String token = request.getHeader("Authorization");
リクエストの Authorization ヘッダーから JWT を取得します。 通常、ヘッダーの値は "Bearer " の形式になっているので、次の処理で "Bearer " を取り除きます。
if (token != null && jwtUtil.validateToken(token.replace("Bearer ", ""))) {
・token != null → トークンが 存在するか チェック
・jwtUtil.validateToken() → JWT の検証(有効なトークンかどうか確認)
・.replace("Bearer ", "") → Bearer の接頭辞を削除
- ユーザー名をトークンから取得
String username = jwtUtil.extractUsername(token.replace("Bearer ", ""));
jwtUtil.extractUsername() を使い、トークンからユーザー名を取得。
- ユーザー名をトークンから取得
jwtUtil.extractUsername() を使い、トークンからユーザー名を取得。
UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);
- UsernamePasswordAuthenticationToken を作成
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
userDetails → ユーザー情報
null → 認証時にパスワードを不要にするため
userDetails.getAuthorities() → ユーザーの権限情報を付与
- Spring Security に認証情報をセット
SecurityContextHolder.getContext().setAuthentication(authToken);
この処理によって、Spring Security はユーザーが 認証済み であることを認識します。
- フィルターの継続
filterChain.doFilter(request, response);
ここで、次のフィルターに処理を 渡します。 他の Spring Security のフィルターと連携して動作するため、ここで止めずに渡す必要があります。
このコードの動作
・リクエストの Authorization ヘッダーを取得
・JWT の正当性を検証 (validateToken())
・トークンから username を取得 (extractUsername())
・ユーザー情報を MyUserDetailsService から取得
・Spring Security に認証情報をセット (SecurityContextHolder)
・次のフィルターに処理を渡す (filterChain.doFilter())
SecurityConfig(セキュリティ設定)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public SecurityConfig(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
UserDetailsService実装(今回は仮ユーザー)
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
// 仮ユーザー名とパスワード
return User.withUsername("testuser")
.password("{noop}password") // {noop}はパスワードエンコーダー無効化
.authorities("USER")
.build();
}
}
AuthController(ログインAPI)
@RestController
public class AuthController {
private final JwtUtil jwtUtil;
public AuthController(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> user) {
if (user.get("username").equals("testuser") && user.get("password").equals("password")) {
String token = jwtUtil.generateToken(user.get("username"));
return ResponseEntity.ok(Collections.singletonMap("token", token));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
SampleController(認証付きAPI)
@RestController
public class SampleController {
@GetMapping("/hello")
public String hello() {
return "Hello, Authenticated User!";
}
}
実行の流れ
1. POST /login
{"username": "testuser", "password": "password"}
→ 成功すると token が返ってくる
2. GET /hello
Authorization: Bearer ヘッダー付けてアクセス
→ “Hello, Authenticated User!” が返る
⸻