项目总结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; } }