在前幾篇文章中,我們已經理解了 JWT 的結構(Header、Payload、Signature),以及它如何被應用在 Web 的身份驗證機制上。
今天我們將進一步練習,嘗試完全不用任何簡單的框架,直接用Java 原生工具(Base64 + HMAC-SHA256)來手動實作一個最小可行的 JWT。
這樣做的目的,是讓讀者徹底理解:
簽名(Signature)不是黑魔法,而是透過「演算法」與「金鑰」計算出來的字串。
如果要看到詳細的程式碼,可以使用這個Branch:
https://github.com/AnsathSean/spring-security-30days/tree/day10-ManualJWT
那我們就繼續開始吧。
首先,我們要來回顧一下JWT,JWT 的格式固定為:
Header.Payload.Signature
HS256
)與類型(JWT
)例如一個簡單的 Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcyNTE3NTI4OX0
.0rjP7kpC6JgGkIhM1iZHZwqHg-5T5wLvgdT73lBlGxk
這次我們會分成三個部分:
JwtUtil
→ 生成與驗證 TokenJwtFilter
→ 過濾請求,檢查是否攜帶正確的 TokenAuthController
→ 提供登入與測試 APIpackage com.ansathsean.security;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
public class JwtUtil {
private static final String SECRET = "mySecretKey";
private static final ObjectMapper mapper = new ObjectMapper();
// 生成 JWT
public static String generateToken(Map<String, Object> payload) throws Exception {
// Header
String headerJson = mapper.writeValueAsString(Map.of("alg", "HS256", "typ", "JWT"));
String headerEncoded = base64UrlEncode(headerJson.getBytes(StandardCharsets.UTF_8));
// Payload
String payloadJson = mapper.writeValueAsString(payload);
String payloadEncoded = base64UrlEncode(payloadJson.getBytes(StandardCharsets.UTF_8));
// 簽名
String data = headerEncoded + "." + payloadEncoded;
String signature = hmacSha256(data, SECRET);
return data + "." + signature;
}
// 驗證 JWT
public static boolean validateToken(String token) throws Exception {
String[] parts = token.split("\\.");
if (parts.length != 3) return false;
String data = parts[0] + "." + parts[1];
String signature = parts[2];
String expectedSignature = hmacSha256(data, SECRET);
return expectedSignature.equals(signature);
}
// 解析 Payload
public static Map<String, Object> getPayload(String token) throws Exception {
String[] parts = token.split("\\.");
if (parts.length != 3) throw new IllegalArgumentException("Invalid JWT format");
String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
return mapper.readValue(payloadJson, new TypeReference<Map<String, Object>>() {});
}
// 取出 sub
public static String getSubject(String token) throws Exception {
return (String) getPayload(token).get("sub");
}
// 取出 role
public static String getRole(String token) throws Exception {
return (String) getPayload(token).get("role");
}
private static String base64UrlEncode(byte[] bytes) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
private static String hmacSha256(String data, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
return base64UrlEncode(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
}
👉 在這裡,簽名的生成過程就是:
Signature = HMACSHA256( base64UrlEncode(Header) + "." + base64UrlEncode(Payload), SECRET )
package com.ansathsean.security;
import java.io.IOException;
import java.util.List;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String path = request.getRequestURI();
if ("/login".equals(path)) {
filterChain.doFilter(request, response);
return;
}
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
if (JwtUtil.validateToken(token)) {
// 從 token 解析出使用者資訊
String username = JwtUtil.getSubject(token);
String role = JwtUtil.getRole(token); // 假設你有把角色存進去
// 建立 Authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username,
null,
List.of(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
);
// 放進 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid or missing JWT");
}
}
package com.ansathsean.security;
import java.util.Map;
import org.springframework.web.bind.annotation.*;
@RestController
public class AuthController {
@PostMapping("/login")
public String login(@RequestBody Map<String, String> request) throws Exception {
String username = request.get("username");
String password = request.get("password");
// 模擬帳密驗證
if ("admin".equals(username) && "password".equals(password)) {
return JwtUtil.generateToken(Map.of(
"sub", username,
"role", "admin",
"iat", System.currentTimeMillis() / 1000
));
}
return "帳號或密碼錯誤";
}
@GetMapping("/hello")
public String hello() {
return "Hello, JWT!";
}
}
/login
admin / password
→ 會回傳一個 JWT。GET /hello
在 Header 加上:
Authorization: Bearer <你的JWT>
如果驗證成功,會回傳:
Hello, JWT!
今天我們完成了 「手工打造 JWT」 的流程,我們可以從中學到以下幾點:
SECRET
,就能檢查 Token 是否可信。這樣的基礎實作,幫助我們真正理解 JWT 的內部機制。這個JWT機制非常的複雜,有手工刻的部分,其實也使用到了Spring boot Security,建議大家先啟動,測試通過之後,再來思考一下這個JWT的設計邏輯。
結束了今天的教學後,接下來我們會進一步引入 Spring Security 與 JWT 整合,讓程式碼更乾淨、更安全,也符合業界的最佳實踐。
打完收工,我們明天見!