统一认证服务,应用服务端token的查询与缓存策略

文章目录

    • 业务场景
    • 缓存策略
      • redis缓存
      • 内存Map缓存
    • 缓存策略介绍

业务场景

  • 我们使用Spring Cloud微服务架构,使用Spring Boot 构建项目
  • 现在需要将项目与另一个业务系统集成,使用同一个认证中心和用户系统
  • 平台服务接入认证中心时,约定在网关服务里进行用户认证,所有服务接口调用都经过网关服务
  • 网关服务接口调用时,判断是否已获取token,是否在有效期,如果没有则重新校验获取token
  • 统一认证服务,提供token申请接口、校验接口和刷新接口,网关服务可以调用这些接口进行token的获取与校验
  • 如果每个接口都调用统一认证服务的token校验接口,会对统一认证服务造成比较大的压力,需要做一定的缓存

缓存策略

redis缓存

  • 第一步是考虑使用redis缓存,统一认证服务将以通过校验的token缓存到redis,有效期到了后再从redis里删除
  • 当调用刷新token接口时,通过校验后,延续token的有效期,同时更新redis里的token的失效时间
  • 网关服务连接统一认证服务使用的这个redis服务,通过判断token是否在redis里,来判断token是否通过校验
  • 相关代码如下:
    /**
     * 检查token
     *
     * @param request
     * @return
     */
    public Boolean checkToken(ServerHttpRequest request) {
        boolean pass = false;
        String token = HttpClientUtil.getTokenFromRequest(request);
        String[] arr = token.split(" ");
        if (arr.length == 1 || "null".equals(arr[arr.length - 1]) || "undefined".equals(arr[arr.length - 1])) {
            return pass;
        }
        // 先从redis里校验,校验不通过再从接口校验
        if (Boolean.TRUE.equals(redisTemplate.hasKey(REDIS_TOKEN_KEY + arr[arr.length - 1]))) {
            pass = true;
        } else {
            logger.info("token从redis里未获取到,从api接口检查:{}", REDIS_TOKEN_KEY + arr[arr.length - 1]);
            pass = checkTokenFromApi(request);
        }
        // token校验通过则主动刷新token(前端也有主动刷新,此处自动刷新就降低频率)
        if (pass && RandomUtils.nextInt(1, 100) > 90) {
            refreshToken(request);
        }
        return pass;
    }
  • 统一认证接入后,进行一些功能测试和性能测试,最简单的测试方式是,大量持续发出接口请求到网关服务,网关服务再获取和校验token
  • 在频繁调用redis校验token时,发生了一个错误,redisTemplate.hasKey请求校验已有的token时偶发返回false(实际在redis里存在,在有效期内)
  • 从接口获取,和从redis获取,加了log日志打印,发现同时发起40个请求时,中间有2次redisTemplate.hasKey返回false
  • 排查了下代码,发现是代码了做了token刷新,调用统一认证中心接口refreshToken请求刷新token
  • 为了防止频繁调用刷新接口给统一认证服务造成压力,这里简单使用RandomUtils做了个随机刷新,在调用此接口刷新时,统一认证服务会重新设置token有效期,可能是此时redis里短暂没有token了(由于看不到统一认证服务的代码,不知道token刷新时redis的具体处理步骤,此处为猜测)

内存Map缓存

  • 为了减少进一步对统一认证服务的接口请求频率,并减少redis请求压力,决定在redis之上,再在内存里加一层缓存
  • 最简单的实现方式,就是使用Map缓存,代码如下:
    /**
     * 检查token
     *
     * @param request
     * @return
     */
    public Boolean checkToken(ServerHttpRequest request) {
        boolean pass = false;
        String token = HttpClientUtil.getTokenFromRequest(request);
        String[] arr = token.split(" ");
        if (arr.length == 1 || "null".equals(arr[arr.length - 1]) || "undefined".equals(arr[arr.length - 1])) {
            return pass;
        }
        String tokenKey = REDIS_TOKEN_KEY + arr[arr.length - 1];
        // 先从内存判断是否失效,如果失效再从redis判断,再失效从接口判断
        if (!TokenCache.isExpired(tokenKey)) {
            pass = true;
        } else if (Boolean.TRUE.equals(redisTemplate.hasKey(tokenKey))) { // 先从redis里校验,校验不通过再从接口校验
            pass = true;
            // redis判断通过,内存里缓存5s
            TokenCache.put(tokenKey, 5 * 1000L);
        } else {
            logger.info("token从api接口检查:{}", tokenKey);
            pass = checkTokenFromApi(request);
            if (pass) {
                // 接口判断通过,内存里缓存60s
                TokenCache.put(tokenKey, 60 * 1000L);
            }
        }
        // token校验通过则主动刷新token(前端也有主动刷新,此处自动刷新就降低频率)
        if (pass && RandomUtils.nextInt(1, 100) > 90) {
            refreshToken(request);
        }
        return pass;
    }
  • 使用内存缓存后,大大减少请求redis频率和请求接口频率,增加了系统的并发量

缓存策略介绍

  • 以要存储的tokenkey,以有效期时间戳为 value
  • 每次查询时,判断是否存在此key或者是否已过期(与当前时间戳对比)
  • 具体代码如下:
package com.newatc.com.authorization;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 将token缓存到内存里,减少redis压力
 *
 * @author yanyulin
 * @date 2024-1-10 17:19:39
 */
public class TokenCache {

    private static Map<String, Long> cacheExpireTimeMap = new ConcurrentHashMap<>();

    /**
     * 将数据存入内存
     *
     * @param key        数据
     * @param expireTime 有效期(毫秒数)
     */
    public static void put(String key, Long expireTime) {
        cacheExpireTimeMap.put(key, System.currentTimeMillis() + expireTime);
    }

    public static Object get(String key) {
        if (isExpired(key)) {
            remove(key);
            return null;
        }
        return cacheExpireTimeMap.get(key);
    }

    public static void remove(String key) {
        cacheExpireTimeMap.remove(key);
    }

    /**
     * 判断token是否过期
     *
     * @param key
     * @return true-过期,false-有效
     */
    public static boolean isExpired(String key) {
        // 没有默认是过期
        if (!cacheExpireTimeMap.containsKey(key)) {
            return true;
        }
        Long expireTime = cacheExpireTimeMap.get(key);
        if (expireTime == null || System.currentTimeMillis() > expireTime) {
            remove(key);
            return true;
        }
        return false;
    }
}
  • 这个只是使用Map做内存缓存的最简单的一种方式,如果同时要存一些数据,也可以使用2个Map去实现
// 存储数据
Map<String, Object> cacheValueMap = new ConcurrentHashMap<>();
// 存储失效时间
Map<String, Long> cacheExpireTimeMap = new ConcurrentHashMap<>();
  • 除了使用ConcurrentHashMap作为内存缓存,也可以使用其他技术框架,如Caffeine(Caffeine 是一个基于 Java 8 ConcurrentHashMap 和 Google Guava 的高性能内存缓存库。它提供了非阻塞的缓存数据结构,利用了 Java 8 的一些新特性,性能非常高)
  • ConcurrentHashMap实现方式,只能在单体应用里生效,如果需要分布式缓存,就需要其他技术方案了,如EhcacheRedissonHazelcast

你可能感兴趣的:(java,缓存,token,统一认证服务,网关服务,内存缓存)