• SpringBoot Security 单点登出清除所有业务系统的 token


    需求

    1. A、B、C 系统通过 sso 服务实现登录
    2. A、B、C 系统分别获取 Atoken、Btoken、Ctoken 三个 token
    3. 其中某一个系统主动登出后,其他两个系统也登出
    4. 至此全部 Atoken、Btoken、Ctoken 失效
       

    记录token

     
    pom 文件引入依赖

    • Redis数据库依赖
    • hutool:用于解析token
    <dependency>
       <groupId>org.springframework.bootgroupId>
       <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    <dependency>
       <groupId>cn.hutoolgroupId>
       <artifactId>hutool-allartifactId>
       <version>5.7.13version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

     
    token 存储类 实现 AuthJdbcTokenStore

    • TokenStore 继承 JdbcTokenStore
    • 使用登录用户的用户名 username 做 Redis 的 key
    • 因为用户登录的系统会有多个,所以 value 使用 Redis 的列表类型来存储 token
    • 设置有效时间,保证不少于 list 里 token 的最大有效时间
    @Component
    public class AuthJdbcTokenStore extends JdbcTokenStore {
        public static final String USER_HAVE_TOKEN = "user-tokens:";
    
        @Resource
        RedisTemplate redisTemplate;
    
        public AuthJdbcTokenStore(DataSource connectionFactory) {
            super(connectionFactory);
        }
    
        @Override
        public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
            super.storeAccessToken(token, authentication);
    
            if (Optional.ofNullable(authentication.getUserAuthentication()).isPresent()) {
                User user = (User) authentication.getUserAuthentication().getPrincipal();
                String userTokensKey = USER_HAVE_TOKEN + user.getUsername();
                String tokenValue = token.getValue();
    
                redisTemplate.opsForList().leftPush(userTokensKey, tokenValue);
    
                Long seconds = redisTemplate.opsForValue().getOperations().getExpire(userTokensKey);
                Long tokenExpTime = getExpTime(tokenValue);
                Long expTime = seconds < tokenExpTime ? tokenExpTime : seconds;
                redisTemplate.expire(userTokensKey, expTime, TimeUnit.SECONDS);
            }
        }
    
        private long getExpTime(String accessToken) {
            JWT jwt = JWTUtil.parseToken(accessToken);
            cn.hutool.json.JSONObject jsonObject = jwt.getPayload().getClaimsJson();
            long nowTime = Instant.now().getEpochSecond();
            long expEndTime = jsonObject.getLong("exp");
            long expTime = (expEndTime - nowTime);
            return expTime;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

     
    oauth_access_token 使用 JdbcTokenStore 存储 token 需要新增表

    CREATE TABLE `oauth_access_token` (
      `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
      `token_id` varchar(255) DEFAULT NULL,
      `token` blob,
      `authentication_id` varchar(255) DEFAULT NULL,
      `user_name` varchar(255) DEFAULT NULL,
      `client_id` varchar(255) DEFAULT NULL,
      `authentication` blob,
      `refresh_token` varchar(255) DEFAULT NULL,
      UNIQUE KEY `authentication_id` (`authentication_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

     
    AuthorizationServerConfigurerAdapter 使用 AuthJdbcTokenStore 做 token 存储

    • 引入 DataSource,因为 JdbcTokenStore 的构造方法必须传入 DataSource
    • 创建按 TokenStore,用 AuthJdbcTokenStore 实现
    • tokenServices 添加 TokenStore
    • endpoints 添加 tokenServices
    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private DataSource dataSource;
        
    	...
    	
        @Bean
        public TokenStore tokenStore() {
            JdbcTokenStore tokenStore = new AuthJdbcTokenStore(dataSource);
            return tokenStore;
        }
        
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
            DefaultTokenServices tokenServices = new DefaultTokenServices();
            tokenServices.setTokenStore(tokenStore());
    
            endpoints
                    .authenticationManager(authenticationManager)
                    .tokenServices(tokenServices)
                    .accessTokenConverter(converter)
            ;
        }
        
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

     

    清除 token

    • 继承 SimpleUrlLogoutSuccessHandler
    • 获取用户名 userName
    • 获取登录时存储在 Redis 的 token 列表
    • token 字符串转换成 OAuth2AccessToken
    • 使用 tokenStore 删除 token
    @Component
    public class AuthLogoutSuccessHandler1 extends SimpleUrlLogoutSuccessHandler {
    
        String USER_HAVE_TOKEN = AuthJdbcTokenStore.USER_HAVE_TOKEN;
    
        @Resource
        RedisTemplate redisTemplate;
        @Resource
        TokenStore tokenStore;
    
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            if (!Objects.isNull(authentication)) {
    
                String userName = authentication.getName();
                String userTokensKey = USER_HAVE_TOKEN + userName;
                
                Long size = redisTemplate.opsForList().size(userTokensKey);
                List<String> list = redisTemplate.opsForList().range(userTokensKey, 0, size);
    
                for (String tokenValue : list) {
                    OAuth2AccessToken token = tokenStore.readAccessToken(tokenValue);
    
                    if (Objects.nonNull(token)) {
                        tokenStore.removeAccessToken(token);
                    }
                }
                redisTemplate.delete(userTokensKey);
    
                super.handle(request, response, authentication);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

     

    解决登出时长过长

    场景:项目运行一段时间后,发现登出时间越来越慢

    问题:通过 debug 发现耗时主要在删除 token 那一段

    tokenStore.removeAccessToken(token);
    
    • 1

    原因:随着时间推移,token 越来越多,token 存储表 oauth_access_token 变得异常的大,所以删除效率非常差

    解决办法:使用其他 TokenStore,或者清除 oauth_access_token 的表数据

  • 相关阅读:
    【LeetCode】53. 最大子数组和
    mac安装应用提示已损坏的解决方法
    谷歌怒怼 iMessage 锁定 iPhone 用户,安卓用户受到同侪欺凌
    spring framework notes
    『无为则无心』Python基础 — 63、Python中的生成器
    C语言自定义类型一网打尽(结构体、位段/位域、枚举、联合体)
    【C++】从零开始认识泛型编程 — 模版
    online java
    BeanShell 如何加密加签?
    Redis配置、持久化以及相命令
  • 原文地址:https://blog.csdn.net/weixin_42555971/article/details/127782839