文章目錄
使用JWT登陸驗證方案
- session登陸驗證:
1> 登陸時攜帶用戶名和密碼,向服務器發送post請求。
2> 服務器收到請求後查詢數據庫的用戶名和密碼是否匹配。
3> 不匹配,登錄失敗,返回:用戶名或密碼錯誤
3> 匹配:將用戶信息儲存到session中,並返回cookie。
4> 用戶下次請求時攜帶cookie信息查詢session中是否有用戶信息。
5> 有,通過驗證,執行下一步操作。沒有,驗證失敗,重新登錄。
一、JWT簡介
JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。
- JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。
1、頭部(Header)
頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。使用一個JSON對象來表示,並對其進行base64加密形成的字符串就是header。
{
"type":"JWT" // 類型
,"alg":"HS256" // 簽名所用加密算法
}
base64加密後:
eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ==
2、載荷(playload)
載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分,也稱聲明。也是使用json對象表示,對其進行base64加密。
- 1> 標準中註冊的聲明(建議但不強制使用)
iss: jwt簽發者 sub: jwt所面向的用戶 aud: 接收jwt的一方 exp: jwt的過期時間,這個過期時間必須要大於簽發時間 nbf: 定義在什麼時間之前,該jwt都是不可用的. iat: jwt的簽發時間 jti: jwt的唯一身份標識,主要用來作爲一次性token。
- 2> 公共的聲明
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因爲該部分在客戶端可解密。 - 3> 私有的聲明:自定義聲明
私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因爲base64是對稱解密的,意味着該部分信息可以歸類爲明文信息。
標準聲明和自定義聲明區別:
JWT規定的claim(聲明),JWT的接收方在拿到JWT之後,都知道怎麼對這些標準的claim進行驗證(還不知道是否能夠驗證成功);而私有claims不會驗證,除非明確告訴接收方要對這些claim進行驗證以及規則才行。
- 定義一個載荷:
{
"sub":"1234567890" // 設置標準聲明
,"usrId":"125" // 自定義聲明,用戶id
,"name":"John Doe" // 自定義聲明,用戶name
,"admin":true} // 自定義聲明,用戶權限
base64加密後:
eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNySWQiOiIxMjUiLCJuYW1lIjoiSm9ob
iBEb2UiLCJhZG1pbiI6dHJ1ZX0=
3、簽證(signature)
簽證由三部分組成:
header (base64後的)
payload (base64後的)
secret // 服務器端保存的私匙
這個部分由header(base64加密後的)和payload(base64加密後的)使用.(點),連接組成的字符串,然後再通過header中聲明的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意: secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
二、使用JJWT簽發與驗證token
JJWT是一個提供端到端的JWT創建和驗證的Java庫。永遠免費和開源(Apache License,版本2.0),JJWT很容易使用和理解。它被設計成一個以建築爲中心的流暢界面,隱藏了它的大部分複雜性。
1、引入maven依賴:
- jjwt依賴:
<!--jwt依賴--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
- json依賴:用來將java對象轉化成json對象
<!--json依賴--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.41</version> </dependency>
2、創建JwtHelper工具類:
/**
* @Author: smile
* @Date: 2020/2/4
*/
public class JwtHelper {
/**
* 設置過期時間爲30天(自定義)
*/
private static final long TOKEN_EXPIRED_TIME = 30*24*60*60*1000L;
/**
* 設置生成祕鑰的字符串(自定義)
*/
private static final String JWT_SECRET = "132456";
/**
* 設置jwt的id(自定義)
*/
private static final String jwtId = "tokenId";
/**
* 創建JWT的工具
* @param claim //私有聲明(自定義聲明)
* @param time // 過期時間
* @return // jwt token
*/
public static String createJWT(Map<String,Object> claim, long time) {
//指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的時間
long nowMillis = System.currentTimeMillis();
// 指定簽發時間
Date iat = new Date(nowMillis);
// 生成密匙,方法是下面自定義的
SecretKey secretKey = generalKey();
//爲payload添加各種標準聲明和私有聲明瞭
JwtBuilder jwtBuilder = Jwts.builder()
// 如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之後,就是覆蓋了那些標準的聲明的
.setClaims(claim)
//設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置爲一個不重複的值,主要用來作爲一次性token,從而回避重放攻擊。
.setId(jwtId)
//iat: jwt的簽發時間
.setIssuedAt(iat)
//設置簽名使用的簽名算法和簽名使用的祕鑰
.signWith(signatureAlgorithm, secretKey);
//設置過期時間
if (time >= 0) {
long expMillis = nowMillis + time;
Date exp = new Date(expMillis);
jwtBuilder.setExpiration(exp);
}
return jwtBuilder.compact();
}
/**
* 驗證jwt
*/
public static Claims verifyJwt(String token) {
//簽名祕鑰,和生成的簽名的祕鑰一模一樣
SecretKey key = generalKey();
Claims claims;
try {
//得到DefaultJwtParser
claims = Jwts.parser()
//設置簽名的祕鑰
.setSigningKey(key)
//設置需要解析的jwt
.parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 從token中單獨解析出簽名
* @param token
* @return
*/
public static String resolveSignture(String token) {
//簽名祕鑰,和生成的簽名的祕鑰一模一樣
SecretKey key = generalKey();
String signature = Jwts.parser()
//設置簽名的祕鑰
.setSigningKey(key)
//設置需要解析的jwt
.parsePlaintextJws(token).getSignature();
return signature;
}
/**
* 由字符串JWT_SECRET生成加密key
* @return
*/
private static SecretKey generalKey() {
String stringKey = JWT_SECRET;
byte[] encodedKey = Base64.encodeBase64(stringKey.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 根據對象生成token的方法
*/
public static<T> String generateToken(String key, T object) {
Map<String, Object> map = new HashMap<>();
map.put(key, object);
return createJWT(map, TOKEN_EXPIRED_TIME);
}
}
3、創建認證中心(controller層):
/**
* 認證中心
* @Author: smile
* @Date: 2020/2/3
*/
@RestController
public class AuthenticationCenterController {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 登錄接口,生成token
* @param user 接收用戶信息
* @param request
* @return
*/
@PostMapping("login")
public ResponseEntity loginToken(User user, HttpServletRequest request) {
// 校驗用戶名或密碼
if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return new ResponseEntity(new RestfulBody<>(401,"請輸入正確的用戶名或密碼"), HttpStatus.BAD_REQUEST);
}
// 查詢數據庫驗證用戶名或密碼
if (false) {
return new ResponseEntity(new RestfulBody<>(401,"用戶名或密碼錯誤"), HttpStatus.BAD_REQUEST);
}
// 獲取瀏覽器UA,用於下次判斷是否同一用戶
String userAgent = request.getHeader("user-agent");
user.setId(120L);
user.setUserAgent(userAgent);
// 使用user信息生成token
String token = JwtHelper.generateToken("user", user);
// 將簽名保存到redis中,並設置過期時間
redisTemplate.opsForValue().set("token:"+user.getId(), token, 60*60, TimeUnit.SECONDS);
return new ResponseEntity(new RestfulBody<User>(200,token,"成功"), HttpStatus.CREATED);
}
/**
* 登出接口
* @param request
* @return
*/
@GetMapping("logout")
public ResponseEntity logout(HttpServletRequest request) {
// 接收客戶端的token
String token = request.getHeader("token");
// 解析客戶端token
Claims claims = JwtHelper.verifyJwt(token);
// 獲取token中的用戶信息
if (claims != null) {
// 從解析出的客戶端token獲取用戶
Object object = claims.get("user");
User user = JSON.parseObject(JSON.toJSONString(object), User.class);
redisTemplate.delete("token:" + user.getId());
}
return new ResponseEntity(new RestfulBody<User>(200,"退出成功"), HttpStatus.valueOf(200));
}
4、創建攔截器:
創建攔截器,攔截所有需要驗證的接口請求。
-
認證攔截器:設置攔截規則
/** * 認證攔截 * @Author: smile * @Date: 2020/2/4 */ @Component public class AuthenticationInteceptor implements HandlerInterceptor{ @Autowired private StringRedisTemplate redisTemplate; /** * 攔截所有需要認證的請求,驗證token * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 允許跨域 response.setHeader("Access-Control-Allow-Origin", "*"); // 接收客戶端的token String token = request.getHeader("token"); // 解析客戶端token Claims claims = JwtHelper.verifyJwt(token); // token驗證1-判斷token是否能被解析,解析失敗,返回失敗信息 if (claims == null) { Object json = JSON.toJSON(new RestfulBody<>(404, "token校驗失敗,請重新登錄")); this.responseWriter(json, response); return false; } // 從解析出的客戶端token獲取用戶 Object object = claims.get("user"); User user = JSON.parseObject(JSON.toJSONString(object), User.class); System.out.println(user.getUserAgent()); // 根據客戶端token中的信息從redis中查詢token String redistoken = redisTemplate.opsForValue().get("token:" + user.getId()); // token驗證2-判斷token是否存在,不存在返回失敗信息 if (StringUtils.isBlank(redistoken)) { Object json = JSON.toJSON(new RestfulBody<>(404, "token超時,請重新登錄")); this.responseWriter(json, response); return false; } // 解析redis中的token,獲取user信息 Claims redisClaims = JwtHelper.verifyJwt(redistoken); Object redisObject = redisClaims.get("user"); User redisUser = JSON.parseObject(JSON.toJSONString(redisObject), User.class); // token驗證3-判斷客戶端token中的UA和redis中的token的UA是否一致,不一致則返回錯誤信息 if (!redisUser.getUserAgent().equals(user.getUserAgent())) { Object json = JSON.toJSON(new RestfulBody<>(404, "token無效,請重新登錄")); this.responseWriter(json, response); return false; } return true; } /** * 給客戶端響應json對象封裝的方法 */ public void responseWriter(Object json, HttpServletResponse response) { response.setCharacterEncoding("utf-8"); PrintWriter writer = null; try { writer = response.getWriter(); writer.print(json); } catch (Exception e) { e.printStackTrace(); }finally { writer.close(); } } }
-
攔截器配置類:設置攔截路徑和排除路徑
/** * 添加攔攔截器的截路徑和排除攔截路徑 * @Author: smile * @Date: 2020/2/4 */ @Configuration public class AuthenticationWebConfig implements WebMvcConfigurer { /** * 注入攔截器 */ @Autowired private AuthenticationInteceptor authenticationInteceptor; /** * 添加攔截規則 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInteceptor) .addPathPatterns("/**") .excludePathPatterns("/login") .excludePathPatterns("/logout"); } }
三、JWT適用場景
- 授權:這是最常見的使用場景,解決單點登錄問題。因爲JWT使用起來輕便,開銷小,服務端不用記錄用戶狀態信息(無狀態),所以使用比較廣泛。
- 信息交換:JWT是在各個服務之間安全傳輸信息的好方法。因爲JWT可以簽名,例如,使用公鑰/私鑰對兒 - 可以確定請求方是合法的。此外,由於使用標頭和有效負載計算簽名,還可以驗證內容是否未被篡改。