JSON Web Token (JWT),是一種根據將JSON 格式資料進行編碼的的開放標準(RFC 7519),用於雙方之間安全將訊息作為 JSON 物件進行傳輸,訊息是經過數位簽章 (Digital Signature) 加密,因此可以被驗證及信任。
目前也成為各大網站之間進行資源獲取時所採用證明身份的訊息格式。
可以透過這個網站來測試產生的 jwt 解開後的內容
標頭來顯示使用演算法及 token 類型
{
"alg": "HS256",
"typ": "JWT"
}
放入要攜帶的資訊或聲明 (claims) 的內容。
通常有sub, iat, exp 這些資訊。這邊補充後面導入 JWT 套件可以設定的資訊大寫名稱,等號後面也有指出對應的 jwt 內代表名稱
{
"sub": "sean",
"iat": 1725950094,
"exp": 1725950274
}
簽章會是根據 Header 和 Payload 分別都用 base64UrlEncode ,再加上密鑰 (secret) 進行雜湊演算,產生一組不可反解的亂數,當成簽章,用來驗證 JWT 是否經過篡改。
特別注意 Header, Payload 會經過 Base64 編碼的處理。是可以被還原請不要放敏感資訊。
pom 導入目前 jjwt 新板 0.12.6
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
導入相關參數至 application.properties
// 隨意設置
jwt.secret-key= {yourKey}
// 有效秒數
jwt.valid-seconds=60
我們將實際要產生 token 和驗證的方法放入 JwtService。
這邊提供兩種產生鑰匙的方式,可以透過 application.properties 環境參數帶入,但我測試時使用幾次如果自己隨便輸入可能會因為格式不符就會被認定不安全,所以另外提供 KeyGenerator 來協助產生的方法,寫在 generateSecretKey(),再由 getKey() 將密鑰類型從 String 轉成 Key ,這樣每次程式重新啟動都會產生一組密鑰,這邊也會印在 console 可以拿來回頭到前面提供的網站確認是否可以成功驗證。
@Service
public class JwtService {
@Value("${jwt.secret-key}")
private String JWT_SECRET_KEY_STR;
@Value("${jwt.valid-seconds}")
private int EXPIRATION_TIME;
private String secretKey;
public JwtService() {
secretKey = generateSecretKey();
}
public String generateSecretKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("HmacSHA256");
SecretKey secretKey = keyGen.generateKey();
System.out.println("Secret Key : " + secretKey.toString());
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Error generating secret key", e);
}
}
public Key getKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
使用 jjwt 套件提供的產生 token 方法來進行設置,把使用名稱 setSubject 設到 sub、有效時間 setExpiration 設到 exp,還有帶入上面取得的 key
public String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setHeaderParam("typ", "JWT")
.setSubject(authentication.getName())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * EXPIRATION_TIME * 3))
.signWith(getKey(), SignatureAlgorithm.HS256).compact();
}
這邊有多加入一個 Security 內提供的驗證,透過 AuthenticationManager 來產生 Authentication 物件協助驗證。
先回到 SecurityConfig 註冊 AuthenticationManager 的 Bean
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
// 略
}
引入 Controller 使用,只要我們傳遞 UsernamePasswordAuthenticationToken 這個物件,參數把 username, password 給他就可以回傳給我們 Authentication,裡面會包含是否通過帳號密碼驗證還有使用者相關的資訊 (UserDetails, Authorities…),我們可透過取得裡面的 authenticated 判斷是否有通過,這物件也提供 isAuthenticated() 方法來判斷。
@RestController
public class UserController {
@Autowired
private UserService userService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtService jwtService;
@Autowired
private MyUserDetailsService userDetailsService;
@PostMapping("/login")
public String login(@RequestBody UserLoginRequest userRequest) throws Exception {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(userRequest.getUsername(), userRequest.getPassword()));
if (authentication.isAuthenticated()) {
String jwtToken = jwtService.generateToken(authentication);
return jwtToken;
} else {
return "Login failed";
}
}
}
成功就可以看到回傳的 JWT
Ref:
相關文章也會同步更新我的部落格,有興趣也可以在裡面找其他的技術分享跟資訊。