JWT單點登錄
單點登錄
概念:登錄某集團的某一產品之后,訪問其他產品的網站時就會是登錄狀態,比如登錄QQ之后,進入QQ游戲的時候就是登錄過的狀態,具體實現方法有以下:
Redis+token實現單點登錄:
生成一個隨機字符串token,以token為key,用戶信息為value存儲在redis緩存中,用戶訪問時帶著這個token就可以實現單點登錄;
這里的token是無實際意義的,和用戶信息無關的,否則用戶每次登錄的時候token都是同一個,容易被黑;
JWT實現單點登錄:
這里jwt維護的也是一個token,但是這個token是有意義的,可以理解成用戶信息的加密數據。通過這串加密數據就可以反向解出當前登錄的是哪一個用戶。
Redis+Token登錄和校驗流程:
登錄->校驗用戶名密碼->生成隨機token->將token放入redis中并返回給前端->結束
校驗->后端攔截請求,從header中獲取token,如果沒有就返回錯誤->根據token在redis中獲取數據->如果有數據校驗成功登錄,否則登錄校驗失敗->結束
JWT單點登錄流程
登錄->校驗用戶名密碼->JWT工具包隨機生成token->將token放入redis(也可以不放),并返回給前端->結束
校驗->后端攔截請求,獲取header中的token,如果沒有就返回錯誤->使用工具包解密校驗token->如果校驗成功登錄,否則登錄校驗失敗->結束
JWT原理
結構
- Header 頭部信息,主要聲明了JWT的簽名算法等信息
- Payload 載荷信息,主要承載了各種聲明并傳遞明文數據
- Signature 簽名,擁有該部分的JWT被稱為JWS,也就是簽了名的JWS,用于校驗數據
整體結構是:
header.payload.signature
JWT模塊的核心主要是兩個類:
JWT類用于鏈式生成、解析或驗證JWT信息。JWTUtil類主要是JWT的一些工具封裝,提供更加簡潔的JWT生成、解析和驗證工作
JWT生成
-
HS265(HmacSHA256)算法
// 密鑰
點擊查看代碼
// 密鑰
byte[] key = "1234567890".getBytes();
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setKey(key)
.sign();
byte[] key = "1234567890".getBytes();
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setKey(key)
.sign();
生成的內容為:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40
- 其他算法
點擊查看代碼
// 密鑰
byte[] key = "1234567890".getBytes();
// SHA256withRSA
String id = "rs256";
JWTSigner signer = JWTSignerUtil.createSigner(id,
// 隨機生成密鑰對,此處用戶可自行讀取`KeyPair`、公鑰或私鑰生成`JWTSigner`
KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(signer)
.sign();
// 密鑰
byte[] key = "1234567890".getBytes();
// SHA256withRSA
String id = "rs256";
JWTSigner signer = JWTSignerUtil.createSigner(id,
// 隨機生成密鑰對,此處用戶可自行讀取`KeyPair`、公鑰或私鑰生成`JWTSigner`
KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)));
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(signer)
.sign();
- 不簽名JWT
點擊查看代碼
//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(JWTSignerUtil.none())
.sign()
//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.
String token = JWT.create()
.setPayload("sub", "1234567890")
.setPayload("name", "looly")
.setPayload("admin", true)
.setSigner(JWTSignerUtil.none())
.sign()
JWT解析
JWT驗證
- 驗證簽名
點擊查看代碼
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40";
// 密鑰
byte[] key = "1234567890".getBytes();
// 默認驗證HS265的算法
JWT.of(rightToken).setKey(key).verify()
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40";
// 密鑰
byte[] key = "1234567890".getBytes();
// 默認驗證HS265的算法
JWT.of(rightToken).setKey(key).verify()
- 詳細驗證
點擊查看代碼
String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9." +
"536690902d931d857d2f47d337ec81048ee09a8e71866bcc8404edbbcbf4cc40";
JWT jwt = JWT.of(rightToken);
// JWT
jwt.getHeader(JWTHeader.TYPE);
// HS256
jwt.getHeader(JWTHeader.ALGORITHM);
// 1234567890
jwt.getPayload("sub");
// looly
jwt.getPayload("name");
// true
jwt.getPayload("admin");
除了驗證簽名,Hutool提供了更加詳細的驗證:validate,主要包括:
- Token是否正確
- 生效時間不能晚于當前時間
- 失效時間不能早于當前時間
- 簽發時間不能晚于當前時間
使用方式如下:
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJNb0xpIiwiZXhwIjoxNjI0OTU4MDk0NTI4LCJpYXQiOjE2MjQ5NTgwMzQ1MjAsInVzZXIiOiJ1c2VyIn0.L0uB38p9sZrivbmP0VlDe--j_11YUXTu3TfHhfQhRKc";
byte[] key = "1234567890".getBytes();
boolean validate = JWT.of(token).setKey(key).validate(0);
其他自定義詳細驗證見JWT驗證-JWTValidator章節。
JWT存在的問題及解決方案講解
1、token被解密
加鹽值(密鑰),每個項目的鹽值不能一樣
2、token被拿到第三方使用(別人包裝你的頁面使多個用戶的操作走一個用戶)
沒啥好方法,使用限流
包裝成工具類
點擊查看代碼
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class);
/**
* 鹽值很重要,不能泄漏,且每個項目都應該不一樣,可以放到配置文件中
*/
private static final String key = "Yubaibai12306";
/**
* 生成token
* @param id
* @param mobile
* @return token
*/
public static String createToken(Long id, String mobile) {
DateTime now = DateTime.now();
DateTime expTime = now.offsetNew(DateField.HOUR, 24);
Map<String, Object> payload = new HashMap<>();
// 簽發時間
payload.put(JWTPayload.ISSUED_AT, now);
// 過期時間
payload.put(JWTPayload.EXPIRES_AT, expTime);
// 生效時間
payload.put(JWTPayload.NOT_BEFORE, now);
// 內容
payload.put("id", id);
payload.put("mobile", mobile);
String token = JWTUtil.createToken(payload, key.getBytes());
LOG.info("生成JWT token:{}", token);
return token;
}
/**
* 驗證token
* @param token
* @return 校驗結果
*/
public static boolean validate(String token) {
JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
// validate包含了verify
boolean validate = jwt.validate(0);
LOG.info("JWT token校驗結果:{}", validate);
return validate;
}
/**
* 解密token對應的內容
* @param token
* @return token對應內容對象
*/
public static JSONObject getJSONObject(String token) {
JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
JSONObject payloads = jwt.getPayloads();
payloads.remove(JWTPayload.ISSUED_AT);
payloads.remove(JWTPayload.EXPIRES_AT);
payloads.remove(JWTPayload.NOT_BEFORE);
LOG.info("根據token獲取原始內容:{}", payloads);
return payloads;
}
public static void main(String[] args) {
createToken(1L, "123");
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE3MjUyNjQ1MzMsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE3MjUzNTA5MzMsImlhdCI6MTcyNTI2NDUzM30.lSeDNb5QAp_CH1A-nF5Xw5Qk2zyvd4L3KrDmw97gQvM";
validate(token);
getJSONObject(token);
}
}
前端使用vuex保存登錄信息
vuex或稱store,用于存儲全局變量,可用于各頁面傳遞參數,或放置項目全局信息
state:定義一個全局變量
getters:獲取變量時,做些額外的轉換,如日期格式化
mutations:相當于java的setter,用于修改變量
actions:發起異步任務
modules:項目較大,變量較多時,可以模塊化
缺點:頁面刷新后,數據會丟失
vuex配合h5的session解決瀏覽器刷新問題
前端小技巧,使用:|| {} 為變量賦值,可防止空指針異常
演示gateway攔截器的使用
登錄校驗兩個步驟:
前端請求帶上token,放在header里
后端校驗token有效性,在gateway里統一校驗
gateway有多個攔截器時,使用order來確定攔截器的順序
點擊查看代碼
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class Test1Filter implements GlobalFilter, Ordered {
private static final Logger LOG = LoggerFactory.getLogger(Test1Filter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
LOG.info("Test1Filter");
return chain.filter(exchange);
// return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return 1;
}
}
點擊查看代碼
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class Test2Filter implements GlobalFilter, Ordered {
private static final Logger LOG = LoggerFactory.getLogger(Test2Filter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
LOG.info("Test2Filter");
return chain.filter(exchange);
// return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return 0;
}
}
測試結果:
結果
33:11.638 INFO c.g.t.g.config.Test2Filter :17 reactor-http-nio-2 Test2Filter
33:11.638 INFO c.g.t.g.config.Test1Filter :17 reactor-http-nio-2 Test1Filter
為gateway增加登錄校驗攔截器
登錄攔截器
import com.guaigen.train.gateway.util.JwtUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class LoginMemberFilter implements GlobalFilter, Ordered {
private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (path.contains("/admin")
|| path.contains("/hello")
|| path.contains("/member/member/login")
|| path.contains("/member/member/sendCode")) {
LOG.info("不需要登錄驗證:{}", path);
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("token");
LOG.info("會員登錄驗證開始, token:{}", token);
if (token == null || token.isEmpty()) {
LOG.info("token為空請求被攔截");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 驗證token是否有效
boolean validate = JwtUtil.validate(token);
if (validate) {
LOG.info("token有效,請求放行");
return chain.filter(exchange);
} else {
LOG.info("token無效,請求攔截");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
@Override
public int getOrder() {
return 0;
}
}

浙公網安備 33010602011771號