1.Token是什么
Token是一个字符串,是一段根据特殊规则生成的唯一的、保存在缓存服务器(如Redis)中的key,这个key对应的value是用户账户数据。每个用户登录后,服务器生成一个类似Token并缓存,之后用户每次请求中都要带上这个Token,以便实现类似于HTTP Session的会话跟踪。
2.为什么要用Token
随着近几年上网设备的多样化,软件项目的UI层不仅仅再是窗体、html、还有iOS、安卓app,因此基于Servlet的HTTP Session也就不再适用,因为可能没有浏览器和html页面,也没有cookie了。这种情况下、就需要Token技术来完成用户的跟踪,使得一个账户可以从html登录,也可以从手机app登其它客户端登录。
3.Token如何生成
每个HTTP数据包括两大主要部分,头部字段和消息体,Header是对一个HTTP数据包的描述信息,一般有多个字段和值。为了对不同客户端的Token区别对待,生成Token时需要判断客户端类型,拼接不同前缀。为了保证Token的唯一性和保密性,生成Token时还要拼接用户名+ID+日期时间+6位密码(盐,salt根据客户端信息生成);代码如下
/**
* 为了保证Token的唯一性和保密性,生成Token时还要拼接用户名(加密)+ID+日期时间+6位密码(盐,salt根据客户端信息生成);
* @param agent
* @param user
* @return null
*/
@Override
public String generateToKen(String agent, ItripUser user) {
try {//UserAgentUtil可检测客户端类型,因为要根据客户端的类型生成不同的token
UserAgentInfo userAgentInfo = UserAgentUtil.getUasParser().parse(agent);
StringBuilder sb = new StringBuilder();
sb.append(tokenPrefix);
if(userAgentInfo.getDeviceType().equals(UserAgentInfo.UNKNOWN)){//未知客户端类型
if(UserAgentUtil.CheckAgent(agent)){
sb.append("MOBILE-");//如果包含移动设备的关键字,拼接移动设备的Token前缀
}else {
sb.append("PC-");//拼接PC的Token前缀
}
}else if(userAgentInfo.getDeviceType().equals("Personal computer")){
sb.append("PC-");//如果很明显是PC类型,拼接PC的TOken的前缀
}else{
sb.append("MOBILE-");//拼接移动设备的Token前缀
}
sb.append(MD5.getMd5(user.getUserCode(),32) + "-");//拼接加密用户名称
sb.append(user.getId() + "-");//拼接user id及日期
sb.append(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "-");
sb.append(MD5.getMd5(agent,6));//在拼接6个字符的密码
return sb.toString();
}catch (IOException e){
e.printStackTrace();
}
return null;
}
4.Token登录与退出
登录业务方法
/**
* 登录
* @return 登录成功的user对象,失败返回null
* @throws Exception
*/
@Override
public ItripUser login(String name, String password) throws Exception {
ItripUser user = this.findByUsername(name);
if(null != user&&user.getUserPassword().equals(password)){
if(user.getActivated() != 1){
throw new UserLoginFailedException("用户未激活");
}
return user;
}
return null;
}
保存Token到Redis缓存
/**
* TokenServiceImpl中,保存Token到Redis缓存服务器
* @param token
* @param user
*/
@Override
public void save(String token, ItripUser user) {
if(token.startsWith(tokenPrefix+"PC-")){
//如果客户端是PC,Token有过期时间
redisAPI.set(token,expire, JSON.toJSONString(user));
}else {
//客户端是手机,Token永不过期
redisAPI.set(token,JSON.toJSONString(user));
}
}
登录方法接收前端请求
/**
* 登陆方法接收前端请求
* @param name
* @param password
* @param request
* @return
*/
@RequestMapping(value = "/dologin",method = RequestMethod.POST,produces = "application/json")
@ResponseBody
public Dto dologin(@RequestParam String name, @RequestParam String password,
HttpServletRequest request){
if(!EmptyUtils.isEmpty(name) && !EmptyUtils.isEmpty(password)){
ItripUser user = null;
try { //调用service进行登录
user = userService.login(name.trim(),MD5.getMd5(password.trim(),32));
}catch (UserLoginFailedException e){//返回登录失败错误
return DtoUtil.returnFail(e.getMessage(),ErrorCode.AUTH_AUTHENTICATION_FAILED);
}catch (Exception e){
e.printStackTrace();
return DtoUtil.returnFail(e.getMessage(),ErrorCode.AUTH_UNKNOWN);//返回未知错误
}
if(EmptyUtils.isNotEmpty(user)){//如果用户不为空,表示登录成功
String token = tokenService.generateToKen(
request.getHeader("user-agent"),user);//调用service生成Token
tokenService.save(token,user);//保存token到redis缓存
//计算token过期时间毫秒数,为系统当前事件毫秒数+7200s*1000毫秒/s,即2小时
long timeout = Calendar.getInstance().getTimeInMillis()+ToKenService.SESSION_TIMEOUT*1000;
//创建要返回的ItripTokenVO
ItripTokenVO tokenVO = new ItripTokenVO(token,timeout,Calendar.getInstance().getTimeInMillis());
//tokenVo转为JSON并响应给客户端
return DtoUtil.returnDataSuccess(tokenVO);
}else{
return DtoUtil.returnFail("用户名密码错误",ErrorCode.AUTH_AUTHENTICATION_FAILED);
}
}else{
return DtoUtil.returnFail("参数错误!检查提交的参数名是否正确。",ErrorCode.AUTH_PARAMETER_ERROR);
}
}
验证token
/**
* 验证token是否有效:1.token在redis中是否存在;2.token是否过期
*/
@Override
public boolean validate(String agent, String token) {
boolean tokenValid = false; //该变量表示token是否有效
if(!redisAPI.exist(token)){ //如果token在redis不存在
return tokenValid; //返回false
}
try {
String [] tokenDatails = token.split("-");//按-分割token字符串
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss");
Date TokenGenTime = formatter.parse(tokenDatails[3]);//还原token生成时间
long passed = Calendar.getInstance().getTimeInMillis() - TokenGenTime.getTime();//计算token已生成多久时间(毫秒数)
if(passed > this.SESSION_TIMEOUT * 1000){//如果token已经过期
return tokenValid; //返回false
}
String agentMD5 = tokenDatails[4];
if(MD5.getMd5(agent,6).equals(agentMD5)){ //验证token的6位密码
tokenValid = true;//如果token中的密码也一致,token有效
}
}catch (ParseException e){
e.printStackTrace();
}
return tokenValid;
}
删除Token
/**
* 从redis删除token(key)和用户信息(value)
* @param token
*/
@Override
public void delete(String token) {
if(redisAPI.exist(token)){
redisAPI.delete(token);
}
}
退出登录方法
@RequestMapping(value = "/logout",method = RequestMethod.GET,produces = "application/json",headers = "token")
@ResponseBody
public Dto logout(HttpServletRequest request){
//从请求的Header中获取token
String token = request.getHeader("token");
//删除token和信息
try {
//验证Redis中是否有该token(且没有过期)
if(!tokenService.validate(request.getHeader("user-agent"),token)) {
return DtoUtil.returnFail("token无效", ErrorCode.AUTH_TOKEN_INVALID);
}else{
tokenService.delete(token);//从Redis删除
return DtoUtil.returnSuccess("注销成功!");
}
} catch (Exception e) {
e.printStackTrace();
return DtoUtil.returnFail("注销失败!", ErrorCode.AUTH_UNKNOWN);
}
}