项目中使token
如果项目架构采用前后端分离,并采用分布式架构,通过定义接口API,与前端进行数据交互,前端通过html前行实现。若加入移动端(Andriod,ios)实现,可直接使用API接口实现即可。由于该项目进行前后端分离,session就没有意义了。并且移动端也是无法使用session的。那么需要使用token进行session管理,通过搭建一个认证系统负责用户身份验证,并进行这个系统token的维护和管理。
1.1 用户表的设计
认证系统除了用户的自动注册意外,还有可能是第三方登陆(微信,qq,微博等)。如果用户使用微信登陆成功后,这需要进行账号合并,进行数据同步。具体的业务流程如下:对第一次使用第三方账号登陆系统的 用户(不注册,直接hi用微信登陆),那么系统会给他生成一个临时账号(userCode)和一个临时密码(userPassword),并且用户表需要记录微信的ID(微信接口返回),以便于微信用户下次登陆系统时继续使用微信登陆而不绑定注册的手机号或者邮箱。并且登陆成功后,需要把用户信息放到token里进行统一的管理。
那么用户表必须包含如下的字段:
1. id:主键ID
2. userType:用户类型(如果时第三方登陆的话,系统为自动生成唯一的账号密码;自动注册用户这位邮箱、手机号)
3. userPassword 用户密码
4. flatId (自动注册:用户的主键id
第三方登陆(qqid,微信id,微博id):该字段表示第三方登陆账号的唯一标识,token使用)
那么第二次第三方登陆(没有进行账号绑定),此时校验用户身份,需要使用第三方账号和flatId联合校验(为了避免不同平台返回的平台ID(flatId)出现一致的情况),所以不管是第三方登陆,还是自动注册登陆,token里面放的结构数据内容需要一致,并且在认证系统中需要实现自由平台的token维护和第三方账号登陆的维护。
1.2 token的数据结构及内容
token的数据结构为key-value,具体内容如下:
1. key:token,其设计原则:必须保证整个系统中唯一存在,根据不同的客户端(PC、移动端),为了便于同意管理和维护,token的设计生成算法如下:
token:PC/mobile-userCode(加密)-id-date-6位随机字符串
代码:
/** *实体类 */ public class User { private Integer id;//主键id private String userCode;//若是第三方登录,系统将自动生成唯一账号;自注册用户则为邮箱或者手机号 private String userPassword;//若是第三方登录,系统将自动生成唯一密码;自注册用户则为自定义密码 private String userType;//用户类型(标识:0 自注册用户 1 微信登录 2 QQ登录 3 微博登录) private String flatId;//平台ID(根据不同登录用户,进行相应存入:自注册用户主键ID、微信ID、QQID、微博ID) private Integer activated;//是否激活(0:否 1:是) public Integer getId() { return id; } public Integer getActivated() { return activated; } public void setActivated(Integer activated) { this.activated = activated; } public void setId(Integer id) { this.id = id; } public String getUserCode() { return userCode; } public void setUserCode(String userCode) { this.userCode = userCode; } public String getUserPassword() { return userPassword; } public void setUserPassword(String userPassword) { this.userPassword = userPassword; } public String getUserType() { return userType; } public void setUserType(String userType) { this.userType = userType; } public String getFlatId() { return flatId; } public void setFlatId(String flatId) { this.flatId = flatId; }
MD5加密:
package com.kgc.utils.common; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Random; public class MD5 { public static String getMd5(String plainText,int length) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(plainText.getBytes()); byte b[] = md.digest(); int i; StringBuffer buf = new StringBuffer(""); for (int offset = 0; offset < b.length; offset++) { i = b[offset]; if (i < 0) { i += 256; } if (i < 16) { buf.append("0"); } buf.append(Integer.toHexString(i)); } // 32位 // return buf.toString(); // 16位 // return buf.toString().substring(0, 16); return buf.toString().substring(0, length); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } } public static int getRandomCode(){ int max=9999; int min=1111; Random random = new Random(); return random.nextInt(max)%(max-min+1) + min; } public static void main(String[] args) { System.out.println(MD5.getMd5("helloadsfdsffsf",6)); System.out.println(getRandomCode()); } }
生成token的代码:
/** * 生成token * * @param User * @param userAgent 判断是移动端还是PC端 需要controller传入 HttpServletRequest request String userAgent = request.getHeader("user-agent"); * @return */ public String createToken(User ser, String userAgent) throws IOException { StringBuffer token=new StringBuffer(); token.append("token:"); UserAgentInfo userAgentInfo = UserAgentUtil.getUasParser().parse(userAgent); //获取访问设备并拼接 if(userAgentInfo.getDeviceType().equals(UserAgentInfo.UNKNOWN)){ if(UserAgentUtil.CheckAgent(userAgent)){ token.append("MOBILE-"); }else { token.append("PC-"); } }else if(userAgentInfo.getDeviceType().equals("Personal computer")){ token.append("PC-"); }else { token.append("MOBILE-"); } token.append(MD5.getMd5(ser.getUserCode(),32)+"-"); token.append(user.getId()+"-"); token.append(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())+"-"); token.append(MD5.getMd5(userAgent,6)); return token.toString(); }
2.value:存储用户登陆的信息(数据内容是json格式)
id userCode userPassword userType flatId activated
1.3 token的有效期维护
基于系统的安全性考虑,需要设置token的有效期,为了维护token的有效期,需要把token放到redis进行维护管理。对于不同的客户端(PC端,移动端)token的有效期设置有所不同。
1.3.1 PC端
token的有效期为两个小时,如果两个小时内的token没有进行置换的话,就会自动在该redis里清除token了,那么当用户再次放送请求时,则会提示token失效,情重新登陆。此处应注意:前端需要自动挂你token的生命周期,token存在cookie,web的安全性比较差。
java代码:
controller
@RequestMapping(value = "/api") @RestController public class LoginController { @Resource private TokenService tokenService; /** * 用户登录 * * @param name * @param password * @return */ @RequestMapping(value = "/dologin", method = RequestMethod.POST, produces = "application/json") public Dto dologin(@RequestParam(value = "name") String name, @RequestParam(value = "password") String password, HttpServletRequest request) { try { String userAgent = request.getHeader("user-agent"); return tokenService.dologin(name, password,userAgent); } catch (Exception e) { e.printStackTrace(); return DtoUtil.returnFail("系统异常", ErrorCode.AUTH_UNKNOWN); } }
/**
* 用户注销
* @param
* @return
*/
@RequestMapping(value ="/logout",method = RequestMethod.GET,produces = "application/json")
public Dto logout(HttpServletRequest request){
try {
return tokenService.logout(request.getHeader("token"));
}catch (Exception e){
e.printStackTrace();
return DtoUtil.returnFail("系统异常", ErrorCode.AUTH_UNKNOWN);
}
}
/**
* 客户端置换token
* @param request
* @return
*/
@RequestMapping(value ="/retoken",method = RequestMethod.POST,produces = "application/json")
public Dto retoken(HttpServletRequest request){
try {
return tokenService.replacetoken(request.getHeader("token"),request.getHeader("user-agent"));
} catch (Exception e) {
e.printStackTrace();
return DtoUtil.returnFail("系统异常", ErrorCode.AUTH_UNKNOWN);
}
}
}
service代码:
public interface TokenService { /** * 会话时间 */ public final static int SESSION_TIMEOUT=60*2*60; /** * 置换保护时间 */ public final static int REPLACETOKEN_PROTECTION_TIMEOUT=60*60; /** * 旧的token延迟时间 */ public final static int REPLACE=60*2; //用户登录 public Dto dologin(String userCode, String userPassword,String userAgent) throws Exception; //用户注销 public Dto logout(String token) throws Exception; //客户端置换token public Dto replacetoken(String token,String userAgent) throws Exception; }
impl
@Service("LoginService") public class TokenServerImpl implements TokenService { @Resource private UserMapper UserMapper; @Resource private RedisAPI redisAPI; /** * 登录业务 * * @param userCode * @param userPassword * @return * @throws Exception */ @Override public Dto dologin(String userCode, String userPassword, String userAgent) throws Exception { Map<String, Object> userMap = new HashMap<>(); userMap.put("userCode", userCode); user user = userMapper.getListByMap(userMap).get(0); //用户是否存在 if (EmptyUtils.isNotEmpty(user)) { //判断用户密码是否正确 if (DigestUtil.hmacSign(userPassword, "kgc").equals(user.getUserPassword())) { String tokenString = createToken(user, userAgent); //存到缓存服务器中 redisAPI.set(tokenString, JSONObject.toJSONString(user)); System.out.println("tokenString=="+tokenString); //返回给前端 TokenVO tokenVO = new TokenVO(tokenString, Calendar.getInstance().getTimeInMillis() + SESSION_TIMEOUT * 1000, Calendar.getInstance().getTimeInMillis()); return DtoUtil.returnDataSuccess(tokenVO); } else { return DtoUtil.returnFail("用户密码错误", ErrorCode.AUTH_PARAMETER_ERROR); } } else { return DtoUtil.returnFail("用户不存在", ErrorCode.AUTH_USER_ALREADY_NOTEXISTS); } } @Override public Dto logout(String token) throws Exception { //删除服务端 redisAPI.del(token); return DtoUtil.returnSuccess(); } /** * 客户端置换token * @param token * @return * @throws Exception */ @Override public Dto replacetoken(String token,String userAgent) throws Exception { //判断token是否存在 if (!redisAPI.exists(token)){ return DtoUtil.returnFail("token不存在",ErrorCode.AUTH_TOKEN_INVALID); } String [] tokens=token.split("-"); SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyyMMssHHmmss"); Date startDate=simpleDateFormat.parse(tokens[3]); String format=simpleDateFormat.format(new Date()); long logtime=simpleDateFormat.parse(format).getTime()-startDate.getTime(); if (logtime<REPLACETOKEN_PROTECTION_TIMEOUT*1000){ return DtoUtil.returnFail("token处于保护时间,禁止替换",ErrorCode.AUTH_REPLACEMENT_FAILED); } //以上情况都符合 User user=JSON.parseObject(redisAPI.get(token),User.class); //生成新的token String newtoken=createToken(user,userAgent); //覆盖新的请求,减少过期时间 redisAPI.set(token,JSONObject.toJSONString(user),REPLACE); redisAPI.set(newtoken,JSONObject.toJSONString(user),SESSION_TIMEOUT); //返回给前端 TokenVO tokenVO = new TokenVO(newtoken, Calendar.getInstance().getTimeInMillis() + SESSION_TIMEOUT * 1000, Calendar.getInstance().getTimeInMillis()); return DtoUtil.returnDataSuccess(tokenVO); } /** * 生成token * * @param User * @param userAgent 判断是移动端还是PC端 * @return */ public String createToken(User user, String userAgent) throws IOException { StringBuffer token=new StringBuffer(); token.append("token:"); UserAgentInfo userAgentInfo = UserAgentUtil.getUasParser().parse(userAgent); //获取访问设备并拼接 if(userAgentInfo.getDeviceType().equals(UserAgentInfo.UNKNOWN)){ if(UserAgentUtil.CheckAgent(userAgent)){ token.append("MOBILE-"); }else { token.append("PC-"); } }else if(userAgentInfo.getDeviceType().equals("Personal computer")){ token.append("PC-"); }else { token.append("MOBILE-"); } token.append(MD5.getMd5(user.getUserCode(),32)+"-"); token.append(user.getId()+"-"); token.append(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())+"-"); token.append(MD5.getMd5(userAgent,6)); return token.toString(); } }
redis:
import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import javax.annotation.Resource; @Component public class RedisAPI { @Resource private JedisPool jedisPool; /** * 以键值对的方式保存数据到redis * * @param key * @param value */ public void set(String key, String value) { //获取连接 Jedis jedis = jedisPool.getResource(); try { String result = jedis.set(key, value); // 资源还回到连接池当中 //返还到连接池 jedisPool.returnResource(jedis); } catch (Exception e) { e.printStackTrace(); //销毁资源 jedisPool.returnBrokenResource(jedis); } } /** * 以键值对的方式保存数据到redis * * @param key * @param value * @param expire 时间 单位[秒] */ public void set(String key, String value, int expire) { //获取连接 Jedis jedis = jedisPool.getResource(); try { String result = jedis.setex(key, expire, value); // 资源还回到连接池当中 jedisPool.returnResource(jedis); } catch (Exception e) { e.printStackTrace(); //销毁资源 jedisPool.returnBrokenResource(jedis); } } /** * 取值 * * @param key */ public String get(String key) { //获取连接 Jedis jedis = jedisPool.getResource(); try { String result = jedis.get(key); // 资源还回到连接池当中 jedisPool.returnResource(jedis); return result; } catch (Exception e) { e.printStackTrace(); //销毁资源 jedisPool.returnBrokenResource(jedis); return null; } } /** * 获取剩余秒数 * * @param key */ public Long ttl(String key) { //获取连接 Jedis jedis = jedisPool.getResource(); try { Long result = jedis.ttl(key); // 资源还回到连接池当中 jedisPool.returnResource(jedis); return result; } catch (Exception e) { e.printStackTrace(); //销毁资源 jedisPool.returnBrokenResource(jedis); return null; } } /** * 判断key是否存在 * * @param key */ public Boolean exists(String key) { //获取连接 Jedis jedis = jedisPool.getResource(); try { System.out.println("key=========="+key); Boolean result = jedis.exists(key); // 资源还回到连接池当中 jedisPool.returnResource(jedis); return result; } catch (Exception e) { e.printStackTrace(); //销毁资源 jedisPool.returnBrokenResource(jedis); return false; } } /** * 删除 * * @param key */ public Long del(String key) { //获取连接 Jedis jedis = jedisPool.getResource(); try { Long result = jedis.del(key); // 资源还回到连接池当中 jedisPool.returnResource(jedis); return result; } catch (Exception e) { e.printStackTrace(); //销毁资源 jedisPool.returnBrokenResource(jedis); return null; } } }
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @ApiModel(value ="TokenVO",description = "用户认证凭证信息") public class TokenVO { @ApiModelProperty("用户认证凭据") private String token; @ApiModelProperty("过期时间,单位:毫秒") private long expTime; @ApiModelProperty("生成时间,单位:毫秒") private long genTime; public TokenVO() { } public TokenVO(String token, long expTime, long genTime) { this.token = token; this.expTime = expTime; this.genTime = genTime; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public long getExpTime() { return expTime; } public void setExpTime(long expTime) { this.expTime = expTime; } public long getGenTime() { return genTime; } public void setGenTime(long genTime) { this.genTime = genTime; } }
1.3.2 移动端
token永不失效,修改密码后需要置换token。由于移动端的token不需要过期,只有但PC页面进行密码修改后,移动端才会推出重新登录,或者当移动端进行密码修改后,用户也不需要进行推出登录,知道在redis中更新该token中的密码即可。
1.4.2 Token 置换
Token 置换规则定义:前端获取 Token 的 1.5 时后可进行 Token 置换,若在最后的半个小时内,客户端发出请求,则会进行 Token 置换,拿到重新生成的 Token(包括:token(key)、
生成时间、失效时间),若客户端在最后的半个小时内没有发送任何请求,那么两个小时后自动过期,即:该 Token 自动从 Redis 里清除,用户须重新登录。 需要注意事项:
1> 不论是最后半个小时的置换时间还是 Token 的 2 个小时有效期,都是根据系统的业务需求所设计的策略方案。
2> 为了防止客户端恶意的进行 Token 置换,需要保证生成 Token 后的 1 个小时内不允许置换。
3> 需要保证客户端传递有效的 Token 进行置换。
4> 为了解决页面的并发问题,在进行置换 Token 时,生成新 Token,但是旧 Token 不能立即失效,应设置为置换后的时间延长 2 分钟。
token的使用
在 Controller 的处理方法中通过 request.getHeader("token")来获取 token 字符串,为了方便进行 Token 的验证,提供统一的 ValidationToken.java,该工具类主要负责通过传入的 token(key) 去 Redis 里 进 行 value 的 查 找 (User currentUser =validationToken.getCurrentUser(token);),若找到相应的 value,则返回 currentUser(当前用户),若无,则返回 null。
/** * Token验证 * */ @Component public class ValidationToken { private Logger logger = Logger.getLogger(ValidationToken.class); private @Resource RedisAPI redisAPI; public RedisAPI getRedisAPI() { return redisAPI; } public void setRedisAPI(RedisAPI redisAPI) { this.redisAPI = redisAPI; } public ser getCurrentUser(String tokenString){ //根据token从redis中获取用户信息 /* test token: key : token:1qaz2wsx value : {"id":"100078","userCode":"myusercode","userPassword":"78ujsdlkfjoiiewe98r3ejrf","userType":"1","flatID":"10008989"} */ User ser = null; if(null == tokenString || "".equals(tokenString)){ return null; } try{ String userInfoJson = redisAPI.get(tokenString); ser = JSONObject.parseObject(userInfoJson,User.class); }catch(Exception e){ ser = null; logger.error("get userinfo from redis but is error : " + e.getMessage()); } return ser; } }
后端
Auth 系统需要提供 API 如下:
1> 生成 Token
该接口返回的数据内容包括:Token 的 key(注:需要对敏感信息进行加密处理)、 Token 的生成时间、Token 的失效时间(注:过期时间减去生成时间一定是两个小时)
2> Token 置换
该接口返回新 Token。实现过程中需要注意如下几点:
a) 生成 Token 后的 1 个小时内不允许置换(注:主要是为了防止客户端恶意的进行 Token 置换)
b) 由于需要保证客户端传递的置换 Token 为真实存在并有效的,故需要在该
API 方法内首先判断 Token 是否有效。
c) 在进行置换 Token,生成新 Token,旧 Token 不能立即失效,应设置为置换后的时间延长 2 分钟。
前端
1> 登录成功后,接收 Token 放入 cookie 中,请求的时候从 cookie 中取出放入到 header 里,如下:
$.ajax({ headers:{
Accept:"application/json;charset=utf-8",
Content-Type:"application/json;charset=utf-8",
//从 cookie 中获取
token:"token:PC-3066014fa0b10792e4a762-23-20170531133947-4f6496"
},
type:"post",
.....
})
2> 负责服务器时间同步(根据 API 返回的 Token 生成时间、失效时间进行同步)
3> 置换 Token 需要同步处理,即:保证只有一个请求置换 Token