JwtService.java
Springboot/bookstore-api/src/main/java/com/example/bookstore/security/JwtService.java
package com.example.bookstore.security;
import com.example.bookstore.entity.Role;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* JWT 服務:負責「產生」與「驗證」JSON Web Token。
*
* 【JWT 的結構】三段 Base64Url 字串,用「.」串接:
*
* Header.Payload.Signature
* eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.dBjftJeZ4CVP...
*
* 1. Header : 宣告演算法,例如 {"alg":"HS256"}
* 2. Payload : 聲明(Claims),例如使用者帳號、角色、過期時間
* 3. Signature : 用「伺服器私密金鑰」對前兩段計算出的簽章
*
* 【重要觀念】
* - Payload 只是 Base64 編碼,「任何人都能解讀」→ 千萬別放密碼等敏感資料!
* - 簽章能保證 token「沒有被竄改」:只要內容被改一個字元,
* 簽章驗證就會失敗。而沒有金鑰的人無法偽造出合法簽章。
*/
@Service
public class JwtService {
/**
* 簽章用的密鑰字串,從 application.yml 的 app.jwt.secret 讀入。
* HS256 演算法要求至少 256 bits(32 bytes),太短會直接拋出例外。
*/
@Value("${app.jwt.secret}")
private String secret;
/** token 有效毫秒數,從 application.yml 的 app.jwt.expiration-ms 讀入 */
@Value("${app.jwt.expiration-ms}")
private long expirationMs;
/** 把密鑰字串轉成 HMAC-SHA 演算法使用的 SecretKey 物件 */
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
/**
* 產生 JWT(登入成功後呼叫)。
*
* @param username 使用者帳號 → 放入標準聲明 sub(Subject,主體)
* @param role 使用者角色 → 放入自訂聲明 "role"
*/
public String generateToken(String username, Role role) {
Date now = new Date();
Date expiry = new Date(now.getTime() + expirationMs);
return Jwts.builder()
.subject(username) // 標準聲明 sub:這個 token 屬於誰
.claim("role", role.name()) // 自訂聲明:使用者角色
.issuedAt(now) // 標準聲明 iat:簽發時間
.expiration(expiry) // 標準聲明 exp:過期時間
.signWith(getSigningKey()) // 用密鑰簽章(自動採用 HS256)
.compact(); // 組合成最終的字串
}
/**
* 從 token 中解析出使用者帳號(sub 聲明)。
* 若 token 無效(簽章錯誤、已過期、格式錯誤),parseClaims 會拋出例外。
*/
public String extractUsername(String token) {
return parseClaims(token).getSubject();
}
/**
* 驗證 token 是否有效:
* 1. 簽章正確(沒被竄改、確實是本伺服器簽發)
* 2. 尚未過期
* 3. token 中的帳號與資料庫載入的使用者一致
*/
public boolean isTokenValid(String token, UserDetails userDetails) {
try {
Claims claims = parseClaims(token);
boolean notExpired = claims.getExpiration().after(new Date());
boolean sameUser = claims.getSubject().equals(userDetails.getUsername());
return notExpired && sameUser;
} catch (JwtException | IllegalArgumentException e) {
// 簽章不符、過期、格式錯誤……一律視為無效
return false;
}
}
/**
* 解析並驗證 token,回傳 Payload(Claims)。
* verifyWith(key):解析時同步驗證簽章,驗證失敗會拋出 JwtException。
*/
private Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey()) // 設定驗章金鑰
.build()
.parseSignedClaims(token) // 解析 + 驗章 + 檢查過期
.getPayload();
}
/** 提供 token 有效毫秒數(登入回應中告知客戶端) */
public long getExpirationMs() {
return expirationMs;
}
}
関連記事
BookstoreApiApplication.java
BookstoreApiApplication.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/BookstoreApiApplication.java).
記事を読む →DataInitializer.java
DataInitializer.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/DataInitializer.java).
記事を読む →SecurityConfig.java
SecurityConfig.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/config/SecurityConfig.java).
記事を読む →AuthController.java
AuthController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/AuthController.java).
記事を読む →BookController.java
BookController.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/controller/BookController.java).
記事を読む →ApiErrorResponse.java
ApiErrorResponse.java — java source code from the Springboot learning materials (Springboot/bookstore-api/src/main/java/com/example/bookstore/dto/ApiErrorResponse.java).
記事を読む →