实例环境
IDE | 框架/脚手架 |
---|---|
IDEA v2019.1.2 | Spring boot 2+ |
- | Vue cli 3+ |
概述
- 前端携带账户信息请求登录 -> 后端生成带有寿命的
token
-> 前端存储token
到cookie/localStorage
- 前端每次请求
api
都携带token
头字段 -> 后端检测token
并续期 - 前端退出 -> 后端清除
token
前端
前端除登录外每次请求api
都需要携带token
头字段,为了方便操作,可以给数据请求框架( Ajax、fetch、axios等)加一个全局过滤器(或拦截器)
例如axios
的语法为:
axios.interceptors.request.use(config => {
// 这里使用的是 vue 的状态管理,store.state.user 表示 user 状态
// user.token 在登录成功后进行赋值,同时存进cookie防止浏览器关闭造成的登录状态丢失
// 登录状态:进行过滤
// 未登录状态:不进行过滤
if (store.state.user.token) {
config.headers.Authorization = `${store.state.user.token}`;
}
return config;
},err => {
return Promise.reject(err);
});
添加了"Authorization"头字段后效果如下:
后端
后端生成的token
需带有生命期,防止前端过长时间不去退出而造成账户安全问题,针对生命期的管理,这里使用Redis
存储系统,实现token
的存储,删除,超时自动删除功能
Redis安装使用
// 启动,启动后后台运行
// 默认服务端口6379
redis-server.exe redis.windows.conf
// 操作,启动上面那个至后才能操作
redis-cli.exe -h 127.0.0.1 -p 6379
Token管理代码
要使用redisTemplate
就必须开启Redis
服务
生成token
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
/**
* 生成token
* @param userId 用户ID
* @return Token
* */
public String createToken(int userId){
UUID uuid = UUID.randomUUID(); // 根据机器和时间生成唯一字符
String token = userId + "_" + uuid.toString().replace("-",""); // token = userId_uuid(去-)
String key = userId + "_token";
redisTemplate.opsForValue().set(key, token,
12, TimeUnit.HOURS); // redis set操作:key,value,时间 12,单位小时
return token;
}
检测Token
/**
* 检查token
* @param token
* @return true更新token;false令牌不存在
* */
public Boolean checkToken(String token){
if(token == null || token.equals("")){ // 空token 返回false
return false;
}
String[] arr1 = token.split("_"); // 分解token
if(arr1.length != 2){ // 格式不对返回false
return false;
}
try {
String key = arr1[0] + "_token";
String r_token = redisTemplate.opsForValue().get(key).toString(); // 读取服务器token
if(r_token == null || ! r_token.equals(token)){ // 服务器token 过期 或 与用户token 不相等返回false
return false;
}
redisTemplate.opsForValue().set(key, token, // 更新token时长
12, TimeUnit.HOURS);
return true;
}catch (Exception e){
System.out.println(e);
}
return false;
}
移除Token
/**
* 注销token
* @param token
* @return true成功;false失败
* */
public Boolean clearToken(String token){
if(token == null || token.equals("")){ // 空token 返回false
return false;
}
String[] arr1 = token.split("_"); // 分解token
if(arr1.length != 2){ // 格式不对返回false
return false;
}
try {
String key = arr1[0] + "_token";
String r_token = redisTemplate.opsForValue().get(key).toString(); // 读取服务器token
if(r_token == null || ! r_token.equals(token)){ // 服务器token 过期 或 与用户token 不相等返回false
return false;
}
redisTemplate.delete(key);
return true;
}catch (Exception e){
System.out.println(e);
}
return false;
}
拦截器配置
前端发送的除登录以外的每一次请求,都应进行拦截,检测Token
注意:前端在进行复杂跨域请求前,会进行一次预请求(OPTIONS
),试探性的服务器响应是否正确,然后才会发送真正请求,所以拦截器也应放行预请求
@Configuration
public class TokenInterceptor extends WebMvcConfigurationSupport {
@Autowired
private TokenManager tokenManager;
@Override
public void addInterceptors(InterceptorRegistry registry) {
HandlerInterceptor handlerInterceptor=new HandlerInterceptor() {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object hander) throws Exception {
// 放行OPTIONS请求,防止因跨域导致的请求失败
if (request.getMethod().toUpperCase().equals("OPTIONS")) {
return true;
}
// 非OPTIONS请求TOKEN验证
String token = request.getHeader("authorization");
if (token != null) {
boolean flag = tokenManager.checkToken(token);
if (flag) {
return true;
}
}
return false;
}
};
// 拦截路径配置,不拦截 login(exclude表示排除)
registry.addInterceptor(handlerInterceptor).excludePathPatterns("/*/login");
}
}