@TOC
1、基本概念
为什么要使用token
优点
- 后台不用保存token,只需要验证是否是自己签发的token
- 支持多种前段,如移动端和浏览器
缺点和解决方法
每次都要去数据库查询权限信息验证token
解决:
将查询到的权限数据保存到session中,之后可以直接从session中获取
也可以使用redisJ解决
2、代码实操
tokenVo
过期时间:方便前端人员判断是否置换,在快过期但用户又有新操作时需要置换
package com.bean.vo;
import java.io.Serializable;
/**
* 返回前端-Token相关VO
*/
public class TokenVO implements Serializable {
/** 用户认证凭据 */
private String token;
/** 过期时间 */
//通常是总毫秒数: token生成时间+有效时长
private long expTime;
/** 生成时间 */
//通常是总毫秒数
private long 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;
}
public TokenVO() {
super();
}
public TokenVO(String token, long expTime, long genTime) {
super();
this.token = token;
this.expTime = expTime;
this.genTime = genTime;
}
}
2.1、前端
localStorage(可以将token保存到这里)
res.data是dto的data属性
<script type="text/javascript">
$(".submit").bind("click", function () {
$.post(
"/auth/login",
$("#actionForm").serialize(),
function (res) {
//判断
if (res.success == "true"){ //登录成功
//将token保存到localStorage
localStorage.token = res.data.token;
//保存token过期时间
localStorage.tokenExpire = res.data.expTime;
//跳转页面
location = "/page/main.html";
}else {
//显示错误提示
$(".info").html(res.msg);
}
}
);
})
</script>
package com.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ViewController {
@RequestMapping("/")
public String base(){
System.out.println(">>> base");
return "/page/login.html";
}
}
TokenUtil.java
session时自动刷新,而token要前端来请求,所以token的置换剩余时间少于1个小时
UserAgentInfo类来自于辅助工具依赖 的插件
package com.util;
import com.alibaba.fastjson.JSON;
import cz.mallat.uasparser.UserAgentInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
@Component
public class TokenUtil {
@Autowired
private RedisUtil redisUtil;
private int session_timeout = 60 * 60 * 2; //token有效时长
private int replacement_protection_timeout = 60 * 60; //token置换保护时长
private int replacement_delay= 60 * 2; //旧token延迟过期时间
private String token_prefix = "token:";//token前缀
/** 获取token默认有效时长 */
public int getTimeout(){
return session_timeout;
}
/** 获取登录时间的总毫秒数 */
public long getLogin(String token) throws ParseException {
//拆分token
String[] arr = token.split("[-]");
return new SimpleDateFormat("yyyyMMddHHmmss").parse(arr[3]).getTime();
}
/** token是否存在 */
public boolean exists(String token){
return redisUtil.exists(token);
}
/**
* 保存token
* @param token
* @param user
*/
public void save(String token, Object user) {
//判断, token是否以"token:PC-"开头
if (token.startsWith(token_prefix + "PC-")){
//PC端登录有效期2小时
redisUtil.setString(token, JSON.toJSONString(user), session_timeout);
}else {
//移动端登录永久有效
redisUtil.setString(token, JSON.toJSONString(user));
}
}
/**
* 读取token中的对象
* @param token
* @param clazz
* @return
*/
public Object load(String token, Class clazz){
return JSON.parseObject(redisUtil.getString(token), clazz);
}
/**
* 删除token
* @param token
*/
public void delete(String token) throws Exception {
//判断, token在redis中是否存在
if (!redisUtil.exists(token))
throw new Exception();
//删除token
redisUtil.delete(token);
}
/**
* 生成token
* @param name 用户账号
* @param id 用户id
* @param agent 设备信息
* @return token<br>
* 格式:<br>
* PC端: 前缀PC-32位的加密name-id-yyyyMMddHHmmss-6位加密的agent<br>
* 移动端: 前缀MOBLIE-32位的加密name-id-yyyyMMddHHmmss-6位加密的agent
*/
public String generateToken(String name, Object id, String agent){
StringBuilder sb = new StringBuilder();
//添加token前缀
sb.append(token_prefix);
//添加设备类型
try {
//获取终端类型
String deviceType = getDeviceType(agent);
//添加到token
sb.append(deviceType + "-");
} catch (IOException e) {
e.printStackTrace();
return null;
}
//添加账号(32位加密)
sb.append(MD5.getMd5(name, 32) + "-");
//添加id
sb.append(id + "-");
//添加登录时间, 格式yyyyMMddHHmmss
sb.append(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "-");
//添加设备信息(6加密)
sb.append(MD5.getMd5(agent, 6));
return sb.toString();
}
/**
* 验证token
* @param token
* @param agent 设备信息
* @param type 验证类型<br>
* 1: 基本验证. 包含结果1,11,12,13,20
* 2: 置换验证. 包含结果1,11,12,14
* @return 结果数值<br>
* 11: token不存在<br>
* 12: token格式有误<br>
* 13: token已超时<br>
* 14: token仍在保护期<br>
* 20: 其它token异常<br>
* 1: token正常
*/
public int validate(String token, String agent, String type){
// token不存在
if (!exists(token)) return 11;
//拆分token字符串
String[] tokenDetails = token.split("-");
//获取token生成时间的总毫秒数
long genTime = 0;
try {
genTime = getLogin(token);
} catch (ParseException e) {
e.printStackTrace();
return 12;
}
//计算登录时长
long passed = Calendar.getInstance().getTimeInMillis() - genTime;
System.out.println(passed);
System.out.println(passed / 1000 / 60);
//判断, 验证类型
if (type.equals("1")){
//判断, 是否已超时
if (passed > session_timeout * 1000) return 13;
//获取登录设备加密信息
String agentMD5 = tokenDetails[4];
//判断, 当前设备是否与登录设备一致
if(MD5.getMd5(agent, 6).equals(agentMD5)) return 1;
}else {
//判断, 是否处于置换保护期内
if (passed < replacement_protection_timeout * 1000) return 14;
return 1;
}
return 20;
}
/**
* 置换token
* @param token
* @param agent 设备信息
* @param clazz 置换对象的类型
* @param nameFieldName 账号属性名
* @param idFiledName id属性名
* @return
*/
public String replaceToken(String token, String agent, Class clazz, String nameFieldName, String idFiledName) {
//获取旧token中存储的用户数据
Object user = load(token, clazz);
System.out.println(user);
//获取旧token有效期(剩余秒数 )
long ttl = redisUtil.getExpire(token);
// System.out.println("ttl:" + ttl);
//判断token是否仍在有效期内容
// if (ttl > 0 || ttl == -1) {
Object name = ""; //用户账号
Object id = ""; //用户id
try {
//获取用户账号
Method getName = clazz.getDeclaredMethod(nameFieldName);
name = getName.invoke(user);
//获取用户id
Method getId = clazz.getDeclaredMethod(idFiledName);
id = getId.invoke(user);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//生成新token
String newToken = this.generateToken(name.toString(), id.toString(), agent);
//保存新token
this.save(newToken, user);
//设置2分钟后旧token过期,注意手机端由永久有效变为2分钟后失效
redisUtil.setString(token, JSON.toJSONString(user), replacement_delay);
return newToken;
// }
// return null;
}
/**
* 获取设备类型
* @param agent
* @return
*/
public String getDeviceType(String agent) throws IOException {
//用户设备信息对象
UserAgentInfo userAgentInfo = UserAgentUtil.getUasParser().parse(agent);
//判断, 设备类型是否未知
if (userAgentInfo.getDeviceType().equals(UserAgentInfo.UNKNOWN)) {
//判断, 已知设备类型是否为移动端
if (UserAgentUtil.CheckAgent(agent)) return "MOBILE";
//已知设备不是移动端, 则为PC端
else return "PC";
}
//判断, 设备类型是否为PC端
else if (userAgentInfo.getDeviceType().equals("Personal computer")) return "PC";
//其他类型均识别为移动端
else return "MOBILE";
}
}
控制类
登录方法
@PostMapping("/login")
@ResponseBody
public Dto login(User user, HttpServletRequest request){
System.out.println(">>> 用户登录");
System.out.println(user);
/* 数据验证(略) */
/* 登录查询 */
List<User> userList = userService.find(user);
/* 处理查询结果 */
//判断, 集合大小
if (userList.size() != 1)
return new Dto("用户名或密码错误!", "100001");
//获取登录用户对象
user = userList.get(0);
//判断, 账号状态
if (user.getStatus() == 0)
return new Dto("账号未激活, 请先激活账号!", "100002");
//登录成功,就要生成token
String token = tokenUtil.generateToken(user.getUserName(), user.getId(),
request.getHeader("user-agent"));
//保存token
tokenUtil.save(token, user);
try {
//获取tokenVo
TokenVO tokenVO = new TokenVO(token,
tokenUtil.getLogin(token) + tokenUtil.getTimeout()*1000,
tokenUtil.getLogin(token));
return new Dto(tokenVO);
} catch (ParseException e) {
e.printStackTrace();
return new Dto("token格式有误!", "100003");
}
}
@PostMapping("/registe/mail")
@ResponseBody
public Dto registeByEmail(User user){
System.out.println(">>> 用户注册--邮箱");
System.out.println(user);
//添加用户
userService.addUserByMail(user);
return new Dto("注册成功, 请尽快激活账号!");
}
启动启动类
启动nginx和redis
就可以访问了
获取header中的token
有时候我们登录之后要去token中拿数据
@GetMapping("/login/info")
@ResponseBody
public Dto getLoginInfo(HttpServletRequest request){
System.out.println(">>> 获取登录用户信息");
//获取header中的token
String token = request.getHeader("token");
System.out.println(token);
//验证token是否正常
int result = tokenUtil.validate(token, request.getHeader("user-agent"), "1");
//根据验证结果的所有异常
switch (result){
case 11:
return new Dto("token不存在", "200011");
case 12:
return new Dto("token格式有误", "200012");
case 13:
return new Dto("token已超时", "200013");
case 20:
return new Dto("token异常", "200020");
}
//获取token中的对象
User user = (User) tokenUtil.load(token, User.class);
//返回
return new Dto(user);
}
前端
每次需要登录向后台请求都要带上 headers
/* 加载登录用户信息 */
$.ajax({
type: "get",
url: "/auth/login/info",
success: function (res) {
if (res.success == "true") {
$("#loginUserName").html(res.data.realName);
}else {
$("#loginUserName").html("游客(未登录)");
}
},
headers: {
token: localStorage.token
}
});
置换token,退出
置换
前端需要判断token是否过期,如果过期就需要置换(如果需要登录才能访问的情况下)
前端
function parseToken(token){
//拆分token
var arr = token.split("-");
//获取年
var year = arr[3].substring(0, 4);
//获取月
var month = arr[3].substring(4, 6);
//获取日
var date = arr[3].substring(6, 8);
//获取时
var hours = arr[3].substring(8, 10);
//获取分
var min = arr[3].substring(10, 12);
//获取秒
var sec = arr[3].substring(12);
return new Date(year, month-1, date, hours, min, sec);
// alert(d);
}
replaceToken();
//置换token
function replaceToken() {
//获取token
var token = localStorage.token;
//获取过期时间
var expTime = localStorage.tokenExpire;
// alert(expTime)
// var date = parseToken(token);
//获取剩余登录时间的毫秒数
var loginTimeLeft = expTime - new Date().getTime();
// alert(loginTimeLeft < 1000 * 60 * 60);
//判断, 是否需要置换token
if (loginTimeLeft < 1000 * 60 * 60){
$.ajax({
headers: {
token: localStorage.token
},
type: "get",
url: "/auth/token/replace",
success: function (res) {
if (res.success == "true"){
localStorage.token = res.data.token;
localStorage.tokenExpire = res.data.expTime;
}
// else{
// alert(res.msg);
// }
}
});
}
}
控制类
@GetMapping("/token/replace")
@ResponseBody
public Dto replaceToken(HttpServletRequest request){
System.out.println(">>> 置换token");
//获取header中的token
String token = request.getHeader("token");
System.out.println(token);
//验证token是否正常
int result = tokenUtil.validate(token, null,"2");
try {
// System.out.println(tokenUtil.getLogin(token));
System.out.println(new Date(tokenUtil.getLogin(token)));
System.out.println(tokenUtil.getTimeout());
} catch (ParseException e) {
e.printStackTrace();
}
//根据验证结果的所有异常
switch (result){
case 11:
return new Dto("token不存在", "200011");
case 12:
return new Dto("token格式有误", "200012");
case 14:
return new Dto("token已超时", "200013");
}
//置换token, 获取新token
token = tokenUtil.replaceToken(token, request.getHeader("user-agent"),
User.class, "userName", "id");
//判断, 新token
if (token == null)
return new Dto("token置换失败!", "200030");
try {
TokenVO tokenVO = new TokenVO(token,
tokenUtil.getLogin(token) + tokenUtil.getTimeout()*1000,
tokenUtil.getLogin(token));
return new Dto(tokenVO);
} catch (ParseException e) {
e.printStackTrace();
return new Dto("token格式有误!", "100003");
}
}
@GetMapping("/login/info")
@ResponseBody
public Dto getLoginInfo(HttpServletRequest request){
System.out.println(">>> 获取登录用户信息");
//获取header中的token
String token = request.getHeader("token");
System.out.println(token);
//验证token是否正常
int result = tokenUtil.validate(token, request.getHeader("user-agent"), "1");
//根据验证结果的所有异常
switch (result){
case 11:
return new Dto("token不存在", "200011");
case 12:
return new Dto("token格式有误", "200012");
case 13:
return new Dto("token已超时", "200013");
case 20:
return new Dto("token异常", "200020");
}
//获取token中的对象
User user = (User) tokenUtil.load(token, User.class);
//返回
return new Dto(user);
}
退出
验证,是否是我记录的token,以及是否是同个设备
前端
<a href="javascript:void(0)" id="logoff">退出</a>
function replaceToken() {
$("#logoff").bind("click", function () {
if (!confirm("确定退出系统吗?")) return;
$.ajax({
headers: {
token: localStorage.token
},
url: "/auth/logoff",
type: "get",
success: function (res) {
if (res.success == "true") {
#清除
localStorage.token = null;
localStorage.tokenExpire = null;
location = "/page/login.html";
}
}
});
})
后台
@GetMapping("/logoff")
@ResponseBody
public Dto logoff(HttpServletRequest request){
System.out.println(">>> 退出登录");
//获取header中的token
String token = request.getHeader("token");
System.out.println(token);
//删除token
try {
tokenUtil.delete(token);
} catch (Exception e) {
e.printStackTrace();
return new Dto("token不存在!", "100011");
}
return new Dto("退出成功!");
}