JSON Web Token(JWT)是一個非常輕巧的規范。這個規范允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。
一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。
未加密前的jwt就是一個json
頭部:頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也可以被表示成一個JSON對象
{"typ":"JWT","alg":"HS256"}
然后用base64進行編碼
載荷:載荷就是存放有效信息的地方。
{"sub":"1234567890","name":"John Doe","admin":true}
然后用base64進行編碼
簽證:jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:
1 是頭部經base64編碼后的字符
2 是載荷經base64編碼后的字符
3 是鹽(密鑰),通常存於服務器
將1和2用.連接,通過頭部中聲明的加密算法進行加鹽(3)計算,得到第三部分
將頭的base64和載荷的base64和簽證三部分用.分割,得到的最終字符串就是jwt
java中使用jwt
引入坐標
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
編寫測試類
public class JwtTest {
public static void main(String[] args) {
//獲取系統的當前時間,以便設置過期時間
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis);
//生成jwt令牌
JwtBuilder jwtBuilder = Jwts.builder()
.setId("66")//設置jwt編碼 這是載荷
.setSubject("黑馬程序員")//設置jwt主題 這是載荷
.setIssuedAt(new Date())//設置jwt簽發日期 這是載荷
.setExpiration(date)//設置jwt的過期時間 目前是創建就會過期
.claim("roles","admin") //自定義的信息 這是載荷 多條自定義信息就多寫幾個claim
.claim("company","itheima")//自定義的信息 這是載荷
.signWith(SignatureAlgorithm.HS256, "itheima");
//生成jwt令牌
String jwtToken = jwtBuilder.compact();
System.out.println(jwtToken);
//解析jwt令牌
Claims claims = Jwts.parser().setSigningKey("itheima")//簽名必須和生成時的一樣 簽名是signwith方法參數
.parseClaimsJws(jwtToken).getBody();
System.out.println(claims);
}
}
在項目中的jwt
工具類
package com.changgou.system.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; /** * JWT工具類 */ public class JwtUtil { //有效期為 public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一個小時 //設置秘鑰明文 public static final String JWT_KEY = "itcast"; /** * 創建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); SecretKey secretKey = generalKey(); JwtBuilder builder = Jwts.builder() .setId(id) //唯一的ID .setSubject(subject) // 主題 可以是JSON數據 .setIssuer("admin") // 簽發者 .setIssuedAt(now) // 簽發時間 .signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個參數為秘鑰 .setExpiration(expDate);// 設置過期時間 return builder.compact(); } /** * 生成加密后的秘鑰 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
用戶提交時由服務器發放令牌,隨后會再次提交用來驗證
//用戶登錄 @PostMapping("/login") public Result login(@RequestBody Admin admin){ boolean result = adminService.login(admin); if(result){ //密碼是正確的 //生成jwt令牌,返回到客戶端 Map<String,String> info = new HashMap<>(); info.put("username",admin.getLoginName()); //基於工具類生成jwt令牌 String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), admin.getLoginName(), null); info.put("token",jwt); return new Result(true,StatusCode.OK,"登錄成功",info); } else { return new Result(false,StatusCode.ERROR,"登錄失敗",result); } }
服務器驗證用戶提交的令牌,使用攔截器,要實現兩個接口
@Component public class AuthorizeFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //獲取請求對象 ServerHttpRequest request = exchange.getRequest(); //獲取響應對象 ServerHttpResponse response = exchange.getResponse(); //判斷當前的請求是否為登錄請求,如果是登錄請求,直接放行 if(request.getURI().getPath().contains("/admin/login")){ //放行 return chain.filter(exchange); } //獲取所有的請求頭信息 HttpHeaders headers = request.getHeaders(); //獲取jwt令牌信息 String jwtToken = headers.getFirst("token"); //判斷當前令牌是否存在 if(StringUtils.isEmpty(jwtToken)){ //令牌為空或不存在,返回錯誤信息 response.setStatusCode(HttpStatus.UNAUTHORIZED);//返回一個狀態,狀態為未認證 return response.setComplete();//設置完成,類似與break } //如果當前令牌存在,解析令牌,判斷是否合法,如果不合法,則向客戶端返回錯誤提示 try { //解析令牌 通過密鑰解析令牌,如果能正常解析,說明令牌是正確的 JwtUtil.parseJWT(jwtToken); }catch (Exception e){ e.printStackTrace(); //令牌解析失敗 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //如果當前令牌合法,則放行 return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
生成私鑰和公鑰
RAS算法生成私鑰和公鑰,桌面新建一個jwt文件夾,在文件夾里打開cmd,執行
生成私鑰
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
keytool是java提供的證書管理工具
-genkeypair 要生成密鑰 -alias 證書的別名
-keyalg RSA 指定加密算法 -keypass密鑰的訪問密碼
-keystore 生成的文件的名字 -storepass 密鑰庫的訪問密碼
生成過程中會詢問一些信息,最后確定時輸入 y
查看密鑰,在生成的密鑰文件所在的文件里,cmd
keytool -list -keystore changgou.jks
導出公鑰
使用openssl導出公鑰,安裝好后要配置一下環境變量
然后在生成的密鑰文件所在的文件里,重啟cmd
keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey
即可看到公鑰
復制,復制時要帶着-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----,在私鑰處新建一個public.key,用np++打開,粘貼,注意所有文字都要在同一行
借助私鑰和公鑰,使用代碼完成jwt驗證,非項目,非網絡請求,僅為演示,如有疑問,查看暢購項目的changgou-user-oauth的test
私鑰和公鑰都在當前模塊的resources下
引入坐標
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-data</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency>
編寫測試類創建jwt,CreateJwtTest.class
public class CreateJwtTest { @Test public void createJWT(){ //基於私鑰生成JWT //創建一個密鑰工廠 //私鑰的位置 本模塊中resources中的changgou.jks就是私鑰,public.key就是公鑰 ClassPathResource classPathResource = new ClassPathResource("changgou.jks"); //密鑰庫的密碼 String keyPass = "changgou"; /** * 參數1 私鑰的位置 * 參數2 密鑰庫的密碼 */ KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,keyPass.toCharArray()); //基於工廠獲取私鑰 //密鑰的別名 String alias = "changgou"; //密鑰的密碼 String password ="changgou"; /** * 參數1 密鑰的別名 * 參數2 密鑰的密碼 */ KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray()); //將當前的私鑰轉為RSA的私鑰 RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); //生成jwt Map<String,String > map = new HashMap<>(); map.put("company","heima"); map.put("address","beijing"); /** * 參數1 當前的令牌的內容 * 參數2 簽名(用RSA的私鑰來做簽名) */ Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey)); String jwtEncoded = jwt.getEncoded(); System.out.println(jwtEncoded); } }
編寫測試類解析jwt,ParseJwtTest.class
public class ParseJwtTest { @Test public void parseJwt(){ //基於公鑰解析jwt 這是直接復制的CreateJwtTest.class的打印結果 String jwt ="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiYmVpamluZyIsImNvbXBhbnkiOiJoZWltYSJ9.cjZNz8G0m4noNYN2VM1SH3ujAtbHElW5Vtbadb0NDI0cjM1DaAXzMA53Qbj4pmVQPl_IfSKqUEXbLxowdRa5NHR43laFsR0kzGbJiTINfSVSroSslYpDdEVwCeAF_a7I-R819YTj4p6sjuYKXbzXpeZQErczFbWWWGR2_U44xH6u1ejRNv8PikFiuzNw-muL7zUJkvqeSJzbEMnQdZMbfvZp4LtSI6B4G_PqpdNXkv19-juxAh99VgJInH_ItF0y5IBOxofA7gRebCZmU8L57gO9ohf2L00D95kis_Ji8lmA1ptLIfXqO_qLVvLBUNH-VtgjGAF0-0pyB-5jlbHP7w"; //公鑰,直接復制的Public.key里的公鑰 String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----"; //解析和驗簽jwt,獲取令牌 Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey)); //解析令牌,獲取令牌中的載荷 String claims = token.getClaims(); System.out.println(claims);//打印結果為{"address":"beijing","company":"heima"} } }
1