项目中使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 = ; offset < b.length; offset++) {
i = b[offset];
if (i < ) {
i += ;
}
if (i < ) {
buf.append("");
}
buf.append(Integer.toHexString(i));
}
// 32位
// return buf.toString();
// 16位
// return buf.toString().substring(0, 16); return buf.toString().substring(, length);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
} } public static int getRandomCode(){
int max=;
int min=;
Random random = new Random();
return random.nextInt(max)%(max-min+) + min;
}
public static void main(String[] args) {
System.out.println(MD5.getMd5("helloadsfdsffsf",));
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(),)+"-");
token.append(user.getId()+"-");
token.append(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())+"-");
token.append(MD5.getMd5(userAgent,)); 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=**;
/**
* 置换保护时间
*/
public final static int REPLACETOKEN_PROTECTION_TIMEOUT=*;
/**
* 旧的token延迟时间
*/
public final static int REPLACE=*;
//用户登录
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();
//用户是否存在
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 * , 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[]);
String format=simpleDateFormat.format(new Date());
long logtime=simpleDateFormat.parse(format).getTime()-startDate.getTime();
if (logtime<REPLACETOKEN_PROTECTION_TIMEOUT*){
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 * , 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(),)+"-");
token.append(user.getId()+"-");
token.append(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())+"-");
token.append(MD5.getMd5(userAgent,)); 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

项目中使token的更多相关文章

  1. flask项目中使用富文本编辑器

    flask是一个用python编写的轻量级web框架,基于Werkzeug WSGI(WSGI: python的服务器网关接口)工具箱和Jinja2模板,因为它使用简单的核心,用extension增加 ...

  2. MUI在项目中使用时遇到的问题的个人分享

    picker 添加年插件问题总结 1. 取消按钮点击事件无法获取 可以获取到取消按钮标签和确定按钮标签 但是只能获取去顶按钮事件,取消点击事件无法获取 通过判断picker的display状态也不能确 ...

  3. My97DatePicker时间控件在项目中的应用

    一.下载My97DatePicker的压缩包My97DatePicker.rar,解压. 注:My97DatePicker最新版本有开发包,项目中使用时删掉,以便节省空间,提高程序的运行效率. 二.在 ...

  4. 在Android项目中启用Java 8的部分特性--Lambda & Method References

    Android N发布时同时发布了一个新的编译工具Jack(AS2.1+支持),基于Jack我们可以使用Java 8 的部分特性,在低版本机器上能使用的更少,同时Jack也有诸多不完善,工具链的改变难 ...

  5. 分享我在 vue 项目中关于 api 请求的一些实现及项目框架

    本文主要简单分享以下四点 如何使用 axios 如何隔离配置 如何模拟数据 分享自己的项目框架 本文主要目的为以下三点 希望能够帮到一些人 希望能够得到一些建议 奉上一个使用Vue的模板框架 我只是把 ...

  6. DDD实战11 在项目中使用JWT的token 进行授权验证

    步骤: 1.首先要在webapi的管道中 使用认证(Authentication) 2.要在webapi的服务中注册验证条件 代码如下: namespace Dealer.WebApi { publi ...

  7. ARC 与非ARC 之间的转换,以及如何使一个项目中,ARC与非ARC共存

    1,非ARC 转 ARC的操作 XCode 的 Edit -- Refactor -- Convert to Object-C ARC (注意,一般在一个大项目中,很少直接使用此方法,其正确率有待考虑 ...

  8. vue项目中设置全局引入scss,使每个组件都可以使用变量

    在Vue项目中使用scss,如果写了一套完整的有变量的scss文件.那么就需要全局引入,这样在每个组件中使用. 可以在mian.js全局引入,下面是使用方法. 1: 安装node-sass.sass- ...

  9. 在基于MVC的Web项目中使用Web API和直接连接两种方式混合式接入

    在我之前介绍的混合式开发框架中,其界面是基于Winform的实现方式,后台使用Web API.WCF服务以及直接连接数据库的几种方式混合式接入,在Web项目中我们也可以采用这种方式实现混合式的接入方式 ...

随机推荐

  1. 向MySQL数据库插入数据出现乱码的情况分析

    (1)第一种情况在新建数据库时 (2)第二种情况就是,IDE环境里面配置编码设置为UTF-8 (3)第三种情况就是连接数据库时,没有设置编码.这个是最常规的.这个看起来很容易解决,但是需要注意MySQ ...

  2. Java多线程1:使用多线程的几种方式以及对比

    前言 Java多线程的使用有三种方法:继承Thread类.实现Runnable接口和使用Callable和Future创建线程,本文将对这三种方法一一进行介绍. 1.继承Thread类 实现方式很简单 ...

  3. 从hive中读取数据推送到kafka

    由python2.7语言实现的,包也比较旧了. # -*- coding: utf-8 -*- # Version: 1.0.0 # Description: py_Hive2Kafka2kafka ...

  4. Python实现PDF文件截取

    python3截取PDF文件中的一部分. from PyPDF2 import PdfFileWriter, PdfFileReader # 开始页 start_page = 0 # 截止页 end_ ...

  5. chromedriver.exe,自动化web, 安装谷歌驱动,以及可能遇到的坑

    1.下载谷歌驱动:chromedriver.exe http://chromedriver.storage.googleapis.com/index.html (下载驱动的链接) 2.把下载的chro ...

  6. layui 批量上传

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="piclist.aspx.c ...

  7. [Linux系统] (4)脚本编程

    一.bash shell 可以理解为一种解释器和启动器,解释命令文本,并执行命令. 命令来源: 用户交互输入 文本文件输入 1.示例,写一个最简单的文本 vi test.txt 写入以下内容: ech ...

  8. postfix -- 发件调试

    按照教程(https://www.cnblogs.com/huandada/p/10554603.html)搭建好postfix之后,由于收件的邮件运营商的限制,部分邮件不能正常发送,需要更多其他配置 ...

  9. DVWA--File Inclusion

    0x01了解什么叫file inclusion File Inclusion,意思是文件包含(漏洞),是指当服务器开启allow_url_include选项时,就可以通过php的某些特性函数(incl ...

  10. 分布式-信息方式-JMS大纲

     一.简介 JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息, ...