項目總結30:token的實現以及原理(附源碼)
什么是token
一句話概括:token是身份令牌,用於客戶端請求服務端時提供身份證明;
再具體點(以APP為例):
-
- 用戶通過賬號和密碼登陸APP后,服務端會返回一個參數給客戶端,假設服務端很粗暴的將userId(即數據庫中的用戶id)傳給用戶,用戶接下請求接口,每次只需要傳這個userId給服務端,就這一證明自己的身份,這樣是可以實現(身份證明)功能;
- 但是,直接將userId暴露給用戶值非常危險的,相當於每次別人問你誰,你就把身份證給他看一樣;
- 於是,一個更合理的方案出現了,用戶登陸APP時,服務端傳一個根據userId進行加密得到的字符串給用戶即可,這個字符串就是token,用戶每次請求接口只需要那這個加密過的字符串就可以了,既能證明身份,又安全保密;
token的實現原理
- 生成token:用戶請求登陸接口,從數據庫正確獲取userId后,將userId加密生成token,我們以token為key,userId為value,將數據保存在redis中(或者數據庫中),然后將token返給用戶;
- 校驗token:用戶請求需要身份校驗的接口時,直接將第1步返回的token,作為參數傳給服務端;服務端拿到token后,去redis(或者數據庫)中根據token找到對應的userId,即完成了身份校驗;
- 更新token:redis(或者數據庫)記錄token,是有時效性的(為了安全起見), 每次校驗token成功時,需要刷新一下這個時間;有點類似於屏保,2分鍾內不觸屏就自動上鎖,但是一旦2分鍾內觸屏了,就重新開始計時;
- 備注:實際在redis中保存token時,並不會直接以userId為value,而是將token和userId封裝成一個對象,作為value保存(參考AccessToken類);
源碼解析
AccessToken類:普通POJO
package com.hs.web.common.token; import java.io.Serializable; import com.hs.common.util.encrypt.EncryptUtil; /** * Token WMS管理實體 * * @comment * @update */ public class AccessToken implements Serializable { private static final long serialVersionUID = 4759692267927548118L; private String token;// AccessToken字符串 private String userId; public AccessToken(){ } public AccessToken(String userId){ this.userId = userId; this.token = EncryptUtil.encrypt(userId); } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } }
RedisKeySuffixEnum:枚舉類,如果一個項目中需要不同類型的token,可以在枚舉類中枚舉每一個token在保存時的key的前綴和value的有效時間
package com.hs.web.model; public enum RedisKeySuffixEnum { REGISTER_MOBILE_CODE("fmk_register_mobile_code_", 30 * 60), // register_mobile_code_{mobile}格式 CHANGE_PASSWORD_CODE("fmk_change_password_code_", 30 * 60), //change_password_code_{mobile}格式 USER_TOKEN("fmk_user_token_", 60 * 60 *24 * 7); // user_token_{mobile}格式 private String key; private long expireTime; private RedisKeySuffixEnum(String key, long expireTime){ this.key = key; this.expireTime = expireTime; } public String getKey(){ return key; } public long getExpireTime() { return expireTime; } }
AccessTokenManager:單例模式,用於管理token,包括創建、查找、更新token三個功能
package com.hs.web.common.token; import java.lang.reflect.Field; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.reflect.FieldUtils; import com.hs.common.util.redis.RedisUtil; import com.hs.web.model.RedisKeySuffixEnum; /** * 用戶Token管理工具 * * @comment * @update */ public class AccessTokenManager { private static AccessTokenManager instance = new AccessTokenManager(); private AccessTokenManager(){ } //單例模式 public static AccessTokenManager getInstance(){ return instance; } //獲取token:根據token獲取用戶id public AccessToken getToken(String token){ if(!StringUtils.isBlank(token) && RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){ //AccessToken accessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token); AccessToken accessToken = convertAccessToken(RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)); return accessToken; } return null; } //根據用戶id生成token,並保存在redis中 public String putToken(String userId){ AccessToken token = new AccessToken(userId); RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token.getToken(), token, RedisKeySuffixEnum.USER_TOKEN.getExpireTime()); return token.getToken(); } //每次用戶使用某一個token時,自動刷新該token的有效時間 public void updateToken(String token){ if(!StringUtils.isBlank(token) && RedisUtil.exists(RedisKeySuffixEnum.USER_TOKEN.getKey() + token)){ AccessToken assessToken = (AccessToken) RedisUtil.get(RedisKeySuffixEnum.USER_TOKEN.getKey() + token); if(assessToken == null){ return; } RedisUtil.set(RedisKeySuffixEnum.USER_TOKEN.getKey() + token, token, RedisKeySuffixEnum.USER_TOKEN.getExpireTime()); } } /** * 反射轉換:解決因類加載器不同導致的轉換異常 * com.hs.web.common.token.AccessToken cannot be cast to com.hs.web.common.token.AccessToken * */ private AccessToken convertAccessToken(Object redisObject){ AccessToken at = new AccessToken(); at.setToken(ReflectUtils.getFieldValue(redisObject,"token")+""); at.setUserId(ReflectUtils.getFieldValue(redisObject,"userId")+""); return at; } } //本類私用反射方法—————目的是為了解決因為redis和java虛擬機類加載機制不一樣,而引起的對同一類的引用卻無法轉換的異常; class ReflectUtils{ public static Object getFieldValue(Object obj, String fieldName){ if(obj == null){ return null ; } Field targetField = getTargetField(obj.getClass(), fieldName); try { return FieldUtils.readField(targetField, obj, true ) ; } catch (IllegalAccessException e) { e.printStackTrace(); } return null ; } public static Field getTargetField(Class<?> targetClass, String fieldName) { Field field = null; try { if (targetClass == null) { return field; } if (Object.class.equals(targetClass)) { return field; } field = FieldUtils.getDeclaredField(targetClass, fieldName, true); if (field == null) { field = getTargetField(targetClass.getSuperclass(), fieldName); } } catch (Exception e) { } return field; } }