分布式session接入

一、背景

随着业务发展,需要跨应用调用其他组维护的服务。且其他组使用session存储用户登陆信息。我的应用调用时也需要甄别用户信息。

二、实施

  1. 分析其他组应用session的存储方式。
  2. 采用一种通用的方式统一去存储/读写session信息。

经过分析发现其他组的session已经使用spring-session存储到了redis里,那我只需要去读他们存在redis里的session信息就可。

注意点:

  1. 尽量不修改其他组应用存在session里的信息。
  2. 使用同一种序列化/反序列化方式去解析redis里的session信息。(极度不推荐使用jdk自带的序列化)
  3. 尽量保持同一个数据对象。
  4. 自己的应用配置对方spring-session redis时要考虑到自己项目里的redis配置不要收到影响。
  5. 确保对方的redis在自己应用的各个(测试、预发、生产)环境都能访问。

1. 配置redis

2. 配置bean

@Slf4j
@Configuration
public class BossSessionConfiguration {
    public BossSessionConfiguration() {
        log.info("Initializing  BossSessionConfiguration");
    }

    @Bean
    public SessionRepositoryCustomizer<RedisIndexedSessionRepository> sessionRepositoryCustomizer() {
        return sessionRepository -> {
            // 默认值为SaveMode.ON_SET_ATTRIBUTE,无法从session中获取attr
            sessionRepository.setSaveMode(SaveMode.ALWAYS);
            RedisOperations<Object, Object> redisOperations = sessionRepository.getSessionRedisOperations();
            RedisTemplate<Object, Object> redisTemplate = (RedisTemplate<Object, Object>)redisOperations;
            redisTemplate.setHashKeySerializer(new JdkSerializationRedisSerializer());
        };
    }

    @ConfigurationProperties(prefix = "spring.boss-session-redis")
    @Bean("sessionRedisProperties")
    public RedisProperties sessionRedisProperties() {
        return RedisAutoConfiguration.createRedisProperties();
    }

    @Bean("sessionRedisConnectionFactory")
    @SpringSessionRedisConnectionFactory
    public JedisConnectionFactory sessionConnectionFactory(
            @Qualifier("sessionRedisProperties") RedisProperties properties) {
        log.info("BossSessionConfiguration properties,{}", Jackson.build().writeValueAsString(properties));
        return RedisAutoConfiguration.createJedisConnectionFactory(properties);
    }

    @Bean("sessionRedisTemplate")
    public RedisTemplate<String, Object> redisTemplate(
            @Qualifier("sessionRedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
                Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(createObjectMapper());
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.setEnableDefaultSerializer(false);
        template.setDefaultSerializer(stringRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean("sessionStringRedisTemplate")
    public StringRedisTemplate stringRedisTemplate(
            @Qualifier("sessionRedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        //自定义redis的key和value序列化方式
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new StringRedisSerializer());
        return template;
    }

    private ObjectMapper createObjectMapper() {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return om;
    }

    @Bean
    public HttpSessionIdResolver httpSessionIdResolver() {
        return new SmartHttpSessionIdResolver();
    }

    static class SmartHttpSessionIdResolver implements HttpSessionIdResolver {
        private static final String HEADER_X_AUTH_TOKEN = "X-Auth-Token";
        private final HttpSessionIdResolver browserSessionStrategy = new CookieHttpSessionIdResolver();
        private final HttpSessionIdResolver restfulSessionStrategy = new HeaderHttpSessionIdResolver("X-Auth-Token");

        SmartHttpSessionIdResolver() {
        }

        public List<String> resolveSessionIds(HttpServletRequest request) {
            HttpSessionIdResolver idResolver = this.getHttpSessionIdResolver(request);
            if (idResolver instanceof CookieHttpSessionIdResolver) {
                return idResolver.resolveSessionIds(request);
            } else {
                String headerValue = request.getHeader("X-Auth-Token");
                if (headerValue == null) {
                    headerValue = request.getParameter("X-Auth-Token");
                }

                return headerValue != null ? Collections.singletonList(headerValue) : Collections.emptyList();
            }
        }

        public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
            this.getHttpSessionIdResolver(request).setSessionId(request, response, sessionId);
        }

        public void expireSession(HttpServletRequest request, HttpServletResponse response) {
            this.getHttpSessionIdResolver(request).expireSession(request, response);
        }

        private HttpSessionIdResolver getHttpSessionIdResolver(HttpServletRequest request) {
            String headerValue = request.getHeader("X-Auth-Token");
            if (headerValue == null) {
                headerValue = request.getParameter("X-Auth-Token");
            }

            return StringUtils.isNotBlank(headerValue) ? this.restfulSessionStrategy : this.browserSessionStrategy;
        }
    }
}
public class RedisAutoConfiguration{
	
	public static RedisProperties createRedisProperties() {
        return new RedisProperties();
    }

    public static JedisPoolConfig createJedisPoolConfig(Pool pool) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(pool.getMaxActive());
        config.setMaxIdle(pool.getMaxIdle());
        config.setMinIdle(pool.getMinIdle());
        config.setMaxWaitMillis(100000L);
        if (pool.getTimeBetweenEvictionRuns() != null) {
            config.setTimeBetweenEvictionRunsMillis(pool.getTimeBetweenEvictionRuns().toMillis());
        }
        if (pool.getMaxWait() != null) {
            config.setMaxWaitMillis(pool.getMaxWait().toMillis());
        }
        return config;
    }

    /**
     * @param redisProperties redis参数
     *
     * @return redis配置
     */
    public static RedisConfiguration getRedisConfiguration(RedisProperties redisProperties) {
        if (redisProperties.getCluster() != null) {
            return getClusterConfiguration(redisProperties);
        } else {
            return getStandaloneConfiguration(redisProperties);
        }
    }

    /**
     * 单机redis
     */
    private static RedisStandaloneConfiguration getStandaloneConfiguration(RedisProperties redisProperties) {

        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setDatabase(redisProperties.getDatabase());
        configuration.setHostName(redisProperties.getHost());
        configuration.setPort(redisProperties.getPort());
        if (redisProperties.getPassword() != null) {
            configuration.setPassword(redisProperties.getPassword());
        }
        return configuration;
    }

    /**
     * 集群redis
     */
    private static RedisClusterConfiguration getClusterConfiguration(RedisProperties redisProperties) {

        RedisProperties.Cluster clusterProperties = redisProperties.getCluster();
        RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes());
        if (clusterProperties.getMaxRedirects() != null) {
            config.setMaxRedirects(clusterProperties.getMaxRedirects());
        }
        if (redisProperties.getPassword() != null) {
            config.setPassword(RedisPassword.of(redisProperties.getPassword()));
        }
        return config;
    }

	
}

3. 设置用户信息

1. 添加与其他应用一样的用户对象

@Data
@NoArgsConstructor
public class BotBossUser {

    private Long userId;
    private String nickName;
    private String fullName;
    private String realName;
    private String mobile;
    private String email;
    private String pin;
    private Byte pinType;
    private String city;
    private String iconUrl;
    private Long deptId;
    private Byte type;
    private Byte sex;


    public BotBossUser(Long userId, String fullName, String mobile) {
        this.userId = userId;
        this.fullName = fullName;
        this.nickName = fullName;
        this.mobile = mobile;
    }

    public BotBossUser(Long userId) {
        this.userId = userId;
    }
}

2. 用户信息持有变量ThreadLocal

public class BotBossUserInfoHolder {

    private static final ThreadLocal<BotBossUser> userThreadLocal = ThreadLocal.withInitial(BotBossUser::new);

    public static BotBossUser getUser() {
        return userThreadLocal.get();
    }

    public static void setUser(BotBossUser user) {
        userThreadLocal.set(user);
    }

    public static void clear() {
        userThreadLocal.remove();
    }

    public static Long getUserId() {
        return getUser().getUserId();
    }

}

3. 添加拦截器,在接口是添加/获取用户信息

@Component
public class BossUserInfoInterceptor extends HandlerInterceptorAdapter {

    @Value("${loginUrl}")
    private String forbiddenUrl;


    @Override
    public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (SessionUtil.isLogin()) {
            // 从session获取用户信息
            BotSessionUser botUser = SessionUtil.getBotSessionUser();
            // 检测用户权限
            checkUserRight(handler, botUser);
            setUserInfo(botUser);
        }
        return true;
    }

    /**
     * 如果需要登录,且用户找不到,则认为无权限
     *
     * @param handler 调用接口方法
     * @param user session用户信息
     */
    private void checkUserRight(Object handler, BotSessionUser user) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod)handler;
            NoLoginRequired noLoginRequired = method.getMethodAnnotation(NoLoginRequired.class);
            if (noLoginRequired == null && BotBossUserInfoHolder.getUser() == null) {
                throw new BossUserNotFoundException(forbiddenUrl);
            }
        }
        // 校验用户信息是否完整
        if (StringUtils.isBlank(user.getFullName()) || StringUtils.isBlank(user.getMobile())) {
            throw new BossException(100402, "当前用户信息不完整,请补全用户名和手机号");
        }
        if (!user.isBotBossUser()) {
            // 当前用户无权访问运营平台
            throw new BossUserNotFoundException(forbiddenUrl);
        }
    }

    /**
     * 设置用户信息到当前ThreadLocal
     *
     */
    private void setUserInfo(BotSessionUser userInfo) {
        if (NullUtil.isNotNull(userInfo)) {
            BotBossUser user = new BotBossUser(userInfo.getUserId(), userInfo.getFullName(), userInfo.getMobile());
            BotBossUserInfoHolder.setUser(user);
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        BotBossUserInfoHolder.clear();
        super.afterCompletion(request, response, handler, ex);
    }

}

4. 注册拦截器

public void addInterceptors(InterceptorRegistry registry) {
     registry.addInterceptor(BossUserInfoInterceptor).addPathPatterns("/boss/**");
    } 

5. 代码里使用用户信息

BotBossUser user = BotBossUserInfoHolder.getUser();

以上。