背景
在涉及到统一网关入口的时候,难免会涉及到多系统的鉴权机制,如果在在集群方案中,会有一个问题,当用户第一次登录在tomcat1上了,接口回去请求其他的接口,由于负载均衡的原因,下一个请求可能会打在了tomcat2上,此时是校验不通过的,为了解决这一问题,于是分布式token来了。
实现原理
解释
第一步:用户登录,验证账号密码成功将token写入到cookie中,并同时将用户信息写入redis
第二步:用户请求其他的接口,携带着登录成功返回的cookie
第三步:服务器从request中获取到cookie,解析,查询token是否存在
第四步:存在,继续业务,不存在,返回错误
实现
为了方便,此文使用了注解+aop的方式来实现。
项目结构
pom配置
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
redisTemplate配置
@Configuration
public class RedisTemplateConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private Integer port;
@Value("${spring.redis.password}")
private String password;
@Bean
public JedisPoolConfig jedisPoolConfig(){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setTestOnCreate(true);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
poolConfig.setTestWhileIdle(true);
return poolConfig;
}
@Bean
public JedisConnectionFactory jedisConnectionFactory(){
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig());
jedisConnectionFactory.setHostName(host);
jedisConnectionFactory.setPort(port);
checkPasswordIfNull(jedisConnectionFactory);
return jedisConnectionFactory;
}
private void checkPasswordIfNull(JedisConnectionFactory jedisConnectionFactory){
if (!StringUtils.isEmpty(password)) {
jedisConnectionFactory.setPassword(password);
}
}
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setValueSerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
自定义注解
/**
* 接口是否需要登录的注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedLogin {
String value() default "";
}
切面验证逻辑
/**
* 接口是否需要登录切面验证
*/
@Component
@Aspect
@Slf4j
public class VerificationAspect {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Pointcut("@annotation(com.zcc.distributetoken.anootation.NeedLogin)")
public void verification() {}
@Around("verification()")
public Object verification(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0){
throw new NotLoginException("用户未登录");
}
for (Cookie cookie : cookies) {
if (Constant.TOKEN.equals(cookie.getName())){
if (null != redisTemplate.opsForValue().get(cookie.getValue())){
return proceedingJoinPoint.proceed();
}
}
}
throw new NotLoginException("用户未登录");
}
}
全局异常处理
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(value = NotLoginException.class)
@ResponseBody
public Result<?> notLoginExceptionHandler(HttpServletRequest req, NotLoginException e){
log.error("用户未登录");
return Result.error(CODE_SYS_ERROR, e.getMessage());
}
}
测试代码
@RestController
public class TestController {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@RequestMapping("login")
public Result<?> login(HttpServletRequest request, HttpServletResponse response){
String token = IdUtil.simpleUUID();
redisTemplate.opsForValue().set(token, "用户信息", 30 * 60, TimeUnit.SECONDS);
Cookie c = new Cookie(Constant.TOKEN, token);
response.addCookie(c);
return Result.success("OK");
}
/**
* 需要验证权限的接口 加上 NeedLogin 注解即可
* @return
*/
@RequestMapping("ppp")
@NeedLogin
public Result<?> ppp(){
redisTemplate.opsForValue().set("name", "dsdsdds");
return Result.success("OK");
}
}
优化
可以考虑将上述代码封装成starter,便于公司内其他业务线一起使用,降低接入成本,便于维护。
总结
本文主要介绍了分布式token的背景,为了解决什么问题,演化,以及怎么去实现一个插拔式的分布式token系统
代码地址
\