简介
退出登录的一种比较简单的实现是直接在客户端删除token,但是这存在一个问题,就是被删除的 token 依然有效,按理说注销登录之后 token 应该也是失效的。这一篇文章就主要介绍怎么让 token 失效
网上方法有很多,可以参考这篇文章:JWT 身份认证优缺点分析以及常见问题解决方案
这里我们使用 Redis 黑名单的方式
安装Redis
1、安装 docker (这里使用阿里云,系统 CentOS 7)
# 安装需要的工具包
sudo yum install -y yum-utils
# 设置镜像仓库 (这里使用阿里云的)
sudo yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 安装docker
sudo yum install docker-ce docker-ce-cli containerd.io
# 启动 docker
sudo systemctl start docker
说明:
- 如果下载镜像太慢的话可以使用阿里云的镜像加速服务
2、安装 Portainer 可视化面板(这一步根据个人习惯,个人喜欢界面操作)
# 创建容器数据卷
docker volume create portainer_data
# 安装 Portainer
docker run -d -p 9000:9000 -p 8000:8000 --name portainer --restart always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer
3、安装完成之后,需要在阿里云开启安全组才能在本地访问,这里为了省事,笔者直接将8000~9000的全添加了进去
4、这个时候就可以在本机访问服务器的 9000 端口,来管理 docker 了
5、新建 stack
这里启动容器是基于docker compose ,注意版本为 2
docker-compose.yml 文件内容如下
version: '2'
services:
redis:
image: redis
container_name: redis
hostname: redis
restart: always
ports:
- 8379:6379
volumes:
- ./conf/redis.conf:/etc/redis/redis.conf:rw
- ./data:/data/redis_data:rw
command:
redis-server /etc/redis/redis.conf --appendonly yes
然后点击 “Deployment” 进行发布,redis就安装好了
6、安装好 redis 之后我们需要一个链接工具,这种类型的工具很多,这里使用的是一个叫 redis-commander 的工具,是使用node写的
github: https://github.com/joeferner/redis-commander
安装并启动:
# 全局安装
npm install -g redis-commander
# 指定服务器地址,端口
redis-commander --redis-host 47.94.128.215 --redis-port 8379
然后在浏览器访问http://localhost:8081
即可
SpringBoot 集成 Redis
1、引入依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、新建 Redis 配置类
package pers.qianyucc.qblog.config;
import org.springframework.boot.autoconfigure.*;
import org.springframework.boot.autoconfigure.data.redis.*;
import org.springframework.context.annotation.*;
import org.springframework.data.redis.connection.lettuce.*;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.*;
import java.io.*;
import java.nio.charset.*;
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
// key的序列化器设置成StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
// 解决中文乱码问题
template.setValueSerializer(new GenericToStringSerializer<>(String.class, StandardCharsets.UTF_8));
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
3、编写 logout 的api接口,因为 Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1),所以使用 set 存储 已经过期的 JWT
UserController
@PostMapping("/auth/logout")
@ApiOperation("用户注销登录")
public Results<Object> logout(@RequestAttribute("token") String token) {
redisTemplate.opsForSet().add(JwtConstants.REDIS_KEY, token);
return Results.ok("退出登录成功", null);
}
4、修改拦截器,增加对 Redis 里面是否存在token的判断
package pers.qianyucc.qblog.interceptor;
//......
@Slf4j
@Component
public class JwtTokenInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
String token = req.getHeader(JwtConstants.TOKEN_HEADER);
// 判断 token 是否非空,判断 token 的前缀
if (Objects.isNull(token) || !token.startsWith(JwtConstants.TOKEN_PREFIX)) {
throw new BlogException(ErrorInfoEnum.NOT_LOGIN);
}
// 判断token是否过期
token = token.replace(JwtConstants.TOKEN_PREFIX, StrUtil.EMPTY);
if (JwtUtils.isTokenExpired(token)) {
throw new BlogException(ErrorInfoEnum.TOKEN_EXPIRED);
}
// 判断 token 是否失效
Boolean isMember = redisTemplate.opsForSet().isMember(JwtConstants.REDIS_KEY, token);
if (isMember) {
throw new BlogException(ErrorInfoEnum.TOKEN_INVALID);
}
Claims claims = JwtUtils.getTokenBody(token);
String[] roles = Optional.ofNullable(claims.get(JwtConstants.ROLE_CLAIMS))
.map(r -> r.toString().split(","))
.orElse(new String[0]);
// 判断角色是否正确
if (!ArrayUtil.contains(roles, UserRoleEnum.ADMIN.getValue())) {
throw new BlogException(ErrorInfoEnum.NO_AUTHORITY);
}
req.setAttribute("token", token);
return true;
}
}
前端设计
同样,vue-template-admin 已经帮我们完成了大部分内容,我们只需要在他的基础上进行修改就行了
1、修改@/api/user.js
的logout方法
export function logout() {
return request({
url: '/auth/logout',
method: 'post'
})
}
2、下面分析一下退出登录的执行流程
在@/layout/components/NavBar.vue
中的logout函数,实际上执行了 vuex 中的logout方法
async logout() {
await this.$store.dispatch("user/logout");
this.$router.push(`/login?redirect=${this.$route.fullPath}`);
},
在@/store/modules/user.js
的logout方法中可以看到,调用logout api 成功之后,主要执行了三个方法
- removeToken:使用js-cookie插件,删除cookie中的token
- resetRouter:重置路由
- commit(‘RESET_STATE’):将 vuex 中的值恢复到默认值
logout({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
removeToken() // must remove token first
resetRouter()
commit('RESET_STATE')
resolve()
}).catch(error => {
reject(error)
})
})
},
@/utils/auth.js
export function removeToken() {
return Cookies.remove(TokenKey)
}
@/router/index.js
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
@/store/modules/user.js
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: ''
}
}
测试效果
1、然后我们登录之后再退出,可以在 Redis 中看到已经存入了一个过期的token
2、然后我们再使用这个 token 去进行操作,发现已经提示过期
参考代码:https://gitee.com/qianyucc/QBlog2/tree/v-10.0