美文网首页Java 核心技术编程语言爱好者
RedisTokenStore 源码解析 以及内存泄漏问题

RedisTokenStore 源码解析 以及内存泄漏问题

作者: 蓝梅 | 来源:发表于2021-11-02 00:11 被阅读0次

前言

前端时间,正好在做公司权限相关的架构问题,然后选择了Spring OAuth2来作为公司权限框架,先记录下目前遇到原生问题吧,后续有时间再来整理这个框架的整体脉络;

RedisTokenStore源码解析

RedisTokenStore 主要是来做token持久化到redis的工具类

缓存key

我们先来看下缓存到redis中有哪些key

  private static final String ACCESS = "access:";
  private static final String AUTH_TO_ACCESS = "auth_to_access:";
  private static final String AUTH = "auth:";
  private static final String REFRESH_AUTH = "refresh_auth:";
  private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
  private static final String REFRESH = "refresh:";
  private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
  private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
  private static final String UNAME_TO_ACCESS = "uname_to_access:";

ACCESS :用来存放 AccessToken 对象(登录的token值,还有登录过期时间,token刷新值)
AUTH_TO_ACCESS :缓存的也是AccessToken 对象,是可以根据用户名和client_id来查找当前用户的AccessToken
AUTH :用来存放用户信息(OAuth2Authentication),有权限信息,用户信息等
ACCESS_TO_REFRESH :可以根据该值,通过AccessToken找到refreshToken
REFRESH :用来存放refreshToken
REFRESH_TO_ACCESS :根据refreshToken来找到AccessToken
CLIENT_ID_TO_ACCESS :存放当前client_id有多少AccessToken
UNAME_TO_ACCESS :当没有做单点登录的话,可以使用该key,根据用户名查找当前用户有多少AccessToken可以使用

方法解析

  public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        String key = authenticationKeyGenerator.extractKey(authentication);
        byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key);
        byte[] bytes = null;
        RedisConnection conn = getConnection();
        try {
            bytes = conn.get(serializedKey);
        } finally {
            conn.close();
        }
        OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
        if (accessToken != null) {
            OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue());
            if ((storedAuthentication == null || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) {
                // Keep the stores consistent (maybe the same user is
                // represented by this authentication but the details have
                // changed)
                storeAccessToken(accessToken, authentication);
            }

        }
        return accessToken;
    }

根据client_id和用户名,来获取当前用户的accessToken,如果缓存中的OAuth2Authentication已经过期,或者雷勇有变化,则会重新更新缓存;

    public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
                //序列化 accessToken
        byte[] serializedAccessToken = serialize(token);
                //序列化OAuth2Authentication
        byte[] serializedAuth = serialize(authentication);
                //序列化 access key值(access: + accessToken value值)
        byte[] accessKey = serializeKey(ACCESS + token.getValue());
                //序列化 authentication key值(auth: + accessToken value值)
        byte[] authKey = serializeKey(AUTH + token.getValue());
                //下面都是序列化key值,可以自己看下序列化规则,我就不一一列举了
        byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
        //主要注意这个key UNAME_TO_ACCESS  和 CLIENT_ID_TO_ACCESS 
        //uname_to_access: + (client_id和username的序列化值)
        byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
        //client_id_to_access: + client_id
        byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
        //不同版本redis,缓存方法不一样
        RedisConnection conn = getConnection();
        try {
            //使用redis  pipeline 操作,可以提高并发性能,但是该操作不会保证原子性
            conn.openPipeline();
            if (springDataRedis_2_0) {
                try {
                    this.redisConnectionSet_2_0.invoke(conn, accessKey, serializedAccessToken);
                    this.redisConnectionSet_2_0.invoke(conn, authKey, serializedAuth);
                    this.redisConnectionSet_2_0.invoke(conn, authToAccessKey, serializedAccessToken);
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            } else {
                conn.set(accessKey, serializedAccessToken);
                conn.set(authKey, serializedAuth);
                conn.set(authToAccessKey, serializedAccessToken);
            }
            //当前这个key使用的是set集合数据类型
            if (!authentication.isClientOnly()) {
                conn.sAdd(approvalKey, serializedAccessToken);
            }
            //当前这个key使用的是set集合数据类型
            conn.sAdd(clientId, serializedAccessToken);
            //设置超时时间
            if (token.getExpiration() != null) {
                int seconds = token.getExpiresIn();
                conn.expire(accessKey, seconds);
                conn.expire(authKey, seconds);
                conn.expire(authToAccessKey, seconds);
                conn.expire(clientId, seconds);
                conn.expire(approvalKey, seconds);
            }
            OAuth2RefreshToken refreshToken = token.getRefreshToken();
           //如果有   refreshToken  则缓存
            if (refreshToken != null && refreshToken.getValue() != null) {
                byte[] refresh = serialize(token.getRefreshToken().getValue());
                byte[] auth = serialize(token.getValue());
                byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
                byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue());
                if (springDataRedis_2_0) {
                    try {
                        this.redisConnectionSet_2_0.invoke(conn, refreshToAccessKey, auth);
                        this.redisConnectionSet_2_0.invoke(conn, accessToRefreshKey, refresh);
                    } catch (Exception ex) {
                        throw new RuntimeException(ex);
                    }
                } else {
                    conn.set(refreshToAccessKey, auth);
                    conn.set(accessToRefreshKey, refresh);
                }
                if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                    ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
                    Date expiration = expiringRefreshToken.getExpiration();
                    if (expiration != null) {
                        int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
                                .intValue();
                        conn.expire(refreshToAccessKey, seconds);
                        conn.expire(accessToRefreshKey, seconds);
                    }
                }
            }
            conn.closePipeline();
        } finally {
            conn.close();
        }
    }

缓存OAuth2AccessToken 和 OAuth2Authentication 对象,该方法主要在登录时调用,会把上面说的所有key值都缓存起来;

再看下,移除accessToken时

  public void removeAccessToken(String tokenValue) {
        //序列化key值
        byte[] accessKey = serializeKey(ACCESS + tokenValue);
        byte[] authKey = serializeKey(AUTH + tokenValue);
        byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
        RedisConnection conn = getConnection();
        //操作移除redis缓存值
        try {
            conn.openPipeline();
            conn.get(accessKey);
            conn.get(authKey);
            conn.del(accessKey);
            conn.del(accessToRefreshKey);
            // Don't remove the refresh token - it's up to the caller to do that
            conn.del(authKey);
            List<Object> results = conn.closePipeline();
            byte[] access = (byte[]) results.get(0);
            byte[] auth = (byte[]) results.get(1);

            OAuth2Authentication authentication = deserializeAuthentication(auth);
            if (authentication != null) {
                String key = authenticationKeyGenerator.extractKey(authentication);
                byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
                byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
                byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
                conn.openPipeline();
                conn.del(authToAccessKey);
                conn.sRem(unameKey, access);
                conn.sRem(clientId, access);
                conn.del(serialize(ACCESS + key));
                conn.closePipeline();
            }
        } finally {
            conn.close();
        }
    }

目前主要是看这几个方法,其他方法也挺简单的,主要是一些redis缓存操作的;

内存泄漏问题

client_id_to_access 和 uname_to_access 使用的是set集合,众所周知redis的set集合的过期时间是按照整个key来设置的;
每次登陆时,会先根据client_id和用户名去缓存中查找是否有可使用的AccessToken,如果有则返回缓存中的值,没有则生成新的;
所以每次登陆都会往这两个集合中放入新的accessToken,如果当某个用户在AccessToken有效期内没有操作,则当前用户的登陆信息会被动下线,access 和 auth 中缓存的值都会过期,再次登陆时就查找不到了;
但是如果当前平台用户量不小,那么一直都会有人操作,client_id_to_access 这个集合就会一直续期,那么过期了的accessToken就会一直存在该集合中,且不会减少,造成内存泄漏;

解决方案

1.自己实现TokenStore,修改client_id_to_access 数据结构
2.写个定时任务,定期扫描client_id_to_access ,清除掉已经过期的token
3.如果不需要知道当前有哪些用户登录,或者该功能已经用了其他方式实现的,可以直接去掉这两个redis key

相关文章

  • RedisTokenStore 源码解析 以及内存泄漏问题

    前言 前端时间,正好在做公司权限相关的架构问题,然后选择了Spring OAuth2来作为公司权限框架,先记录下目...

  • Handler

    目录 什么是Handler 使用方式 源码解析 内存泄漏问题 1. 什么是Handler 安卓线程间的一种通信机制...

  • LeakCanary源码解析

    LeakCanary源码解析 内存泄露 今天来讲解一下老生常谈的问题了,内存泄露以及讲解LeakCanary是如果...

  • 常见内存泄漏以及个人解决方法

    内存泄漏问题检测使用 LeakCanary 多线程操作造成的内存泄漏以及崩溃问题(RxJava2为例) 首先配置B...

  • LeakCanary源码解析

    LeakCanary源码解析 前言 对于内存泄漏的检测,基于MAT起点较高,所以一般我们都使用LeakCanary...

  • dubbo内存泄漏源码解析

    第一步:privoder进程停止 privoder进程停止,zk上节点被删除,consumer收到EMPTY开头的...

  • 2020-12-09

    LeakCanary工作原理和源码解析 内存泄漏一直以来就是Android APP需要着重解决的点,而LeakCa...

  • Android 内存泄漏

    【Android 内存泄漏】 引用: ★★★ 【知识必备】内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟...

  • ThreadLocal的源码解析以及内存泄漏的原理分析

    介绍了Java中的ThreadLocal的作用、原理、源码以及应用,并且介绍了ThreadLocal的内存泄漏的原...

  • ThreadLocal源码解析

    ThreadLoal源码解析 关于弱引用和内存泄漏、hash冲突的解决内容,来源于:【黑马程序员Java基础教程由...

网友评论

    本文标题:RedisTokenStore 源码解析 以及内存泄漏问题

    本文链接:https://www.haomeiwen.com/subject/tpntzltx.html