一、背景
随着业务发展,需要跨应用调用其他组维护的服务。且其他组使用session存储用户登陆信息。我的应用调用时也需要甄别用户信息。
二、实施
- 分析其他组应用session的存储方式。
- 采用一种通用的方式统一去存储/读写session信息。
经过分析发现其他组的session已经使用spring-session存储到了redis里,那我只需要去读他们存在redis里的session信息就可。
注意点:
- 尽量不修改其他组应用存在session里的信息。
- 使用同一种序列化/反序列化方式去解析redis里的session信息。(极度不推荐使用jdk自带的序列化)
- 尽量保持同一个数据对象。
- 自己的应用配置对方spring-session redis时要考虑到自己项目里的redis配置不要收到影响。
- 确保对方的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();
以上。