Java設(shè)置token有效期的5個應(yīng)用場景(雙token實(shí)現(xiàn))
token的簡介和生成校驗(yàn)已經(jīng)在前面分享過,有需要的小伙伴可以先進(jìn)行回顧。
傳送門:token介紹,以及如何生成以及校驗(yàn)token
前言:
Token最常見的應(yīng)用場景之一就是身份驗(yàn)證。在傳統(tǒng)的身份驗(yàn)證方式中,用戶需要輸入用戶名和密碼才能登錄系統(tǒng),這種方式容易被破解和盜用。而使用token方式進(jìn)行身份驗(yàn)證,可以有效防止用戶身份信息被盜用。
當(dāng)用戶進(jìn)行身份驗(yàn)證后,服務(wù)器會生成一個token并將其返回給客戶端,客戶端可以使用token來訪問受保護(hù)的資源,而不需要重新輸入用戶名和密碼。這種方式不僅可以提高安全性,還可以提高用戶體驗(yàn)。

場景一:網(wǎng)吧計(jì)時(shí)
場景分析:
嚴(yán)格規(guī)定登陸時(shí)長,超時(shí)則跳轉(zhuǎn)登陸頁面,必須重新輸入密碼才能繼續(xù)使用
對token的要求:
登陸成功后,服務(wù)器生成的token需要攜帶時(shí)間戳(token時(shí)間戳=當(dāng)前時(shí)間+有效時(shí)長),后臺定制一個有效期時(shí)長,在網(wǎng)吧計(jì)時(shí)收費(fèi)場景中有效范圍就是可以上機(jī)的時(shí)長。
每次請求都要校驗(yàn)token是否過期( 時(shí)間戳是否小于現(xiàn)在時(shí)間)
token也只用存在瀏覽器緩存即可,減少服務(wù)器端的存儲壓力。
實(shí)操:
javaWebToken為例:
<!-- 引入jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.8.0</version>
</dependency>
/**
* @description: 生成token
* @param: userInfo 用戶手機(jī)號和用戶Id
* @return: java.lang.String 返回token
**/
public static String getToken(String userPhone) {
try{
//從當(dāng)前時(shí)間算起,再加上有效時(shí)長30分鐘
Date date = new Date(System.currentTimeMillis() + 30*60*1000);
//用秘鑰生成簽名
Algorithm algorithm = Algorithm.HMAC256('P1ooisyGFJhgzrctMOofvaHLuiNFOmktedw');
//默認(rèn)頭部+載荷(手機(jī)號/id)+過期日期+簽名=jwt
String jwtToken= JWT.create()
.withClaim("userPhone", userPhone)
.withClaim("userId", "xxxxxxx")
.withExpiresAt(date)
.sign(algorithm);
return jwtToken;
}catch (Exception e){
log.error("用戶{}的token生成異常:{}",userPhone,e);
return null;
}
}
驗(yàn)證token有效期:
// 判斷 token 是否過期
public static String isExpire(String token) {
DecodedJWT jwt = JWT.decode(token);//解碼token
// 如果token的有效期小于當(dāng)前時(shí)間,則表示已過期,為true
boolean isExpire = jwt.getExpiresAt().getTime() < System.currentTimeMillis();
if(isExpire){
return jwt.getClaim("userPhone").asString();//獲取token攜帶的數(shù)據(jù)
}else{
return null;
}
}
攔截請求,開始驗(yàn)證token
public class JwtToken implements AuthenticationToken {
/**
* JWT的字符token
*/
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
@Component
public class ShiroRealm extends AuthorizingRealm {
/**
* @Title: doGetAuthenticationInfo
* @description: 校驗(yàn)token是否正確
* @param: auth
* @return: org.apache.shiro.authc.AuthenticationInfo
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
try{
String token = (String) auth.getCredentials();
String userPhone=JwtUtil.isExpire(token);
return new SimpleAuthenticationInfo(userPhone, token, getName());
}catch(Exception e){
throw new AuthenticationException("驗(yàn)證token失敗");
}
}
}
總結(jié):
優(yōu)點(diǎn):
- 嚴(yán)格規(guī)定登陸時(shí)長,簡單來說就是安全性高。
- 代碼邏輯簡單,token過期了就重定向到登陸頁面,不需要做延時(shí)等處理。
缺點(diǎn):
- 時(shí)間一到就跳轉(zhuǎn)到登陸頁面,對用戶來說非常突然,某種程度上說用戶體驗(yàn)非常不好。
- 用戶有可能停留在頁面不做任何操作,因此客戶端必須定期主動的給服務(wù)器發(fā)請求(或使用消息隊(duì)列),以便及時(shí)發(fā)現(xiàn)校驗(yàn)token過期。
場景二: Esxi系統(tǒng)頁面、jumpServer
場景分析:
Esxi系統(tǒng)頁面、jumpServer的web終端頁面等,超過一段時(shí)間內(nèi)不操作(例如0.5小時(shí))自動退出登錄,再想繼續(xù)操作需要重新登錄。
對token的要求:
和場景一類似,區(qū)別是增加了一個判斷:每次請求都會判斷token是否快要過期(例如設(shè)置token還有10分鐘過期)。
如果將要過期,則重置token有效期(服務(wù)器發(fā)個新token給客戶端);如果已經(jīng)過期,需要重新登錄。
實(shí)操:
重新生成一個新的token,前端收到新的token后把舊token丟棄(前端代碼略)
總結(jié):
優(yōu)點(diǎn):
- 用戶一直在使用頁面,token就會被一直重置,對活躍用戶友好。
- token有效期短,人離開一段時(shí)間就無法繼續(xù)使用軟件,這個設(shè)計(jì)安全性較高。
- 用戶在token過期后再次操作才會要求再次登陸,也就是說,不需要客戶端時(shí)時(shí)給服務(wù)器發(fā)消息驗(yàn)證token是否有效,減少網(wǎng)絡(luò)開銷。
缺點(diǎn):
- 如果有效期設(shè)計(jì)的短,用戶操作也不頻繁的情況下,會導(dǎo)致用戶頻繁登陸,體驗(yàn)較差。合理設(shè)置有效期非常重要。
場景三:微信、支付寶等app
場景分析:
微信、支付寶等手機(jī)app,我們一旦安裝并登陸以后,除了涉及資金或信息安全的場景需要輸入一些密碼,基本上我們打開app就能用,不會讓我們重復(fù)登陸。
對token的要求:
token有效期設(shè)置的很長(3個月、6個月、一年等)
每次請求還是會驗(yàn)證token有效期,但如果token過期,則發(fā)消息給客戶端。
客戶端收到消息以后,給服務(wù)器發(fā)送重置token的請求。
總結(jié):
適用于安全性有保證的場景(例如手機(jī)App:手機(jī)有鎖屏碼等安全機(jī)制,涉及到金錢等重要信息還有其他驗(yàn)證方式)
優(yōu)點(diǎn):
- 用戶只需要登陸一次,用戶體驗(yàn)很好
缺點(diǎn):
- 客戶端可以發(fā)送重置token的請求,故token一直有效,手機(jī)鎖屏被破解,任何人都能使用,也是個安全隱患。
- 只用登陸一次,用戶很有可能忘記密碼,想要避免用戶體驗(yàn)差,必須綁定手機(jī)號,支持驗(yàn)證碼登陸。
場景四:語雀等pc應(yīng)用程序(雙token)
場景分析:
場景四和場景二類似,區(qū)別是用戶長時(shí)間不使用的情況下才會被強(qiáng)制用戶登錄。
例如“語雀”等應(yīng)用程序,長時(shí)間不使用是會被要求重新登錄的。
我們每個人都安裝過一些使用頻率不高的軟件,這些軟件在產(chǎn)品設(shè)計(jì)時(shí)就決定了用戶的使用頻率和周期,那他們是如何界定“一直在使用的活躍用戶”和“長時(shí)間不使用的非活躍用戶”呢?
還是靠設(shè)置token有效期,有效期設(shè)置的長一些,例如3個月或6個月不使用才算非活躍用戶。
但是如何沿用場景二的token要求,設(shè)置有效期長的token,會留下很大的安全隱患:token一旦被黑客截獲后長時(shí)間可以被使用,還不會被服務(wù)器察覺。
解決方案:雙token
什么是雙token?以現(xiàn)有的短token的基礎(chǔ)上再增加一個長token,形成兩個token校驗(yàn)有效期的模式。
短token可以防止被截獲后無休止使用,所以還要使用有效期短的token用來驗(yàn)證有效期。
而新增加的長token,它的有效期用來區(qū)分“活躍用戶”和“非活躍用戶”,用來實(shí)現(xiàn)短token過期后,活躍用戶系統(tǒng)自動給重置token,非活躍用戶需要重新登錄。
對token的要求:
用戶登錄成功后,服務(wù)器生成一短一長兩個token返回給客戶端,客戶端每次請求服務(wù)器攜帶的是短token。
如果服務(wù)器發(fā)現(xiàn)短token過期,則通知給客戶端,此時(shí)客戶端攜帶長token給服務(wù)器校驗(yàn)。
如果長token未過期,表示用戶為“活躍用戶”,服務(wù)器重置短token和長token發(fā)給客戶端。
如果長token過期,表示用戶為“非活躍用戶”,用戶需要重新登錄。

總結(jié):
優(yōu)點(diǎn):
- 幫助使用頻率不高的軟件區(qū)別“活躍用戶”和“非活躍用戶”,提升“活躍用戶”的體驗(yàn),保證“非活躍用戶”的信息安全
缺點(diǎn):
- 刷新token期間,原有的token不能用,在并發(fā)情況下會導(dǎo)致其他問題。
場景五:提升響應(yīng)速度(redis)
場景分析:
場景一到四的共同點(diǎn):①token內(nèi)置了時(shí)間戳,②服務(wù)器端不存儲token
場景五是對以上以上四個場景在驗(yàn)證速度上的優(yōu)化,親測使用redis可以提高2-4倍的驗(yàn)證速度。
對token的要求:
token內(nèi)不設(shè)置時(shí)間戳,而是將token存在redis中(例如:鍵為token,值為用戶信息),并設(shè)置token存在redis中的保存時(shí)間。
客戶端仍然保存著token,每次請求都攜帶token。服務(wù)器接到請求,把token當(dāng)做鍵去redis中拿數(shù)據(jù):
如果能拿出數(shù)據(jù),則說名token沒過期,如果拿不到,則說明token過期了。
想要重置token有效期,直接根據(jù)鍵重置數(shù)據(jù)在redis中的有效期。

實(shí)操:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.0.0</version>
</dependency>
redis工具類
/**
* Redis工具類
*/
@Component
public final class RedisUtil<V> {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 普通緩存獲取
* @param key 鍵
* @return 值
*/
public String get(String key) {
return key == null ? null : (String) redisTemplate.opsForValue().get(key);
}
/**
* 根據(jù)key 獲取過期時(shí)間
* @param key 鍵 不能為null
* @return 時(shí)間(秒) 返回0代表為永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* HashSet 并設(shè)置時(shí)間
* @param key 鍵
* @param map 對應(yīng)多個鍵值
* @param time 時(shí)間(秒)
* @return true成功 false失敗
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
攔截請求,開始驗(yàn)證token
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private RedisUtil redisUtil;
/**
* @Title: doGetAuthenticationInfo
* @description: 校驗(yàn)token是否正確
* @param: auth
* @return: org.apache.shiro.authc.AuthenticationInfo
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//不使用token驗(yàn)證來校驗(yàn)token是否可用,改成把token存redis中,在redis中設(shè)置數(shù)據(jù)有效期
String token = (String) auth.getCredentials();
if (StringUtils.isEmpty(token)) {
throw new AuthenticationException(Constant.TOKEN_EXPIRED);
}
//判斷是否能從redis中用token拿到過期時(shí)間
try{
Long times=redisUtil.getExpire(token);
//如果還有0.5小時(shí)過期,就刷新token在redis中的有效時(shí)間(有效期設(shè)置為2小時(shí))
if(1800>times){
redisUtil.expire(token,7200);
}
String userId =redisUtil.get(token); //獲取key中的用戶id
return new SimpleAuthenticationInfo(userId, token, getName());
}catch(Exception e){
throw new AuthenticationException("驗(yàn)證token失敗");
}
}
}
驗(yàn)證redis校驗(yàn)速度
//token時(shí)間戳校驗(yàn)
long startTime=System.currentTimeMillis(); //獲取開始時(shí)間
for(int i=0;i<99;i++){
String userPhone = JwtUtil.isExpire(token);
}
long endTime=System.currentTimeMillis(); //獲取結(jié)束時(shí)間
System.out.println("用時(shí)間戳方式校驗(yàn)100次token是否有效: "+(endTime-startTime)+"毫秒");
//redis校驗(yàn)
long startTime=System.currentTimeMillis(); //獲取開始時(shí)間
for(int i=0;i<99;i++){
if(StringUtils.isEmpty(redisUtil.get(token))){
return null;
}
}
long endTime=System.currentTimeMillis(); //獲取結(jié)束時(shí)間
System.out.println("用redis設(shè)置過期時(shí)間方式校驗(yàn)100次token是否有效: "+(endTime-startTime)+"毫秒");

總結(jié):
優(yōu)點(diǎn):
- 服務(wù)器生成了token就直接存到redis中,token是不會被攔截篡改的,因此默認(rèn)token是正確的,也就減少了驗(yàn)證token是否正確這一步驟
- 重置了token,token本身也不會變,減少客戶端處理廢棄token、再存儲新token的邏輯
- redis的數(shù)據(jù)存在內(nèi)存中,IO速度快
缺點(diǎn):
- 依賴第三方軟件,需要搭建redis服務(wù)器,考慮到redis掛了軟件也不能使用,還要搭建redis集群
思想升華:
每種設(shè)置token有效期的方案都有對應(yīng)的場景,拋開場景談方案是狹隘的。再學(xué)習(xí)計(jì)算機(jī)的過程中,我發(fā)現(xiàn)無論是磁盤調(diào)度方式、內(nèi)存存儲方式、raid0-6,所有方式方法的誕生都和當(dāng)時(shí)的場景相關(guān),并且往往在時(shí)間和空間上面進(jìn)行取舍。所以沒有最優(yōu)的方案,只有當(dāng)下相對合適的方案。
到此這篇關(guān)于Java設(shè)置token有效期的5個應(yīng)用場景(雙token實(shí)現(xiàn))的文章就介紹到這了,更多相關(guān)Java設(shè)置token有效期內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Security Oauth2.0 實(shí)現(xiàn)短信驗(yàn)證碼登錄示例
本篇文章主要介紹了Spring Security Oauth2.0 實(shí)現(xiàn)短信驗(yàn)證碼登錄示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
springboot項(xiàng)目中后端接收前端傳參的方法示例詳解
這篇文章主要介紹了springboot項(xiàng)目中一些后端接收前端傳參的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06
SpringBoot配置GlobalExceptionHandler全局異常處理器案例
這篇文章主要介紹了SpringBoot配置GlobalExceptionHandler全局異常處理器案例,通過簡要的文章說明如何去進(jìn)行配置以及使用,需要的朋友可以參考下2021-06-06
java生成csv文件亂碼的解決方法示例 java導(dǎo)出csv亂碼
這篇文章主要介紹了java生成csv文件亂碼的解決方法,大家可以直接看下面的示例2014-01-01
mybatis 忽略實(shí)體對象的某個屬性(2種方式)
這篇文章主要介紹了mybatis 忽略實(shí)體對象的某個屬性方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06

