排查
-
起因
服务上线生产环境后使用飞书登录有些时候会登录失败,查看日志出现以上错误Illegal state [FEISHU],但是测试环境没有出现这个情况 -
排查
经过排查发现是JustAuth 报的错
- 分析出现原因
在JustAuth找到出现原因和解决方案
原文地址:异常相关问题 | JustAuth
异常原因
- 单机的情况
- state 默认有效期为3分钟(#AuthCacheConfig.java(opens new window)),第三方回调会开发者服务器后,因为异常原因在3分钟内没有处理回调请求,此时会抛出该异常。
- 集群的情况
- 默认 state 是缓存到 map 中,当开发者的服务为集群时,不同集群间的内存缓存无法共享,抛出该异常。具体异常流程为:有 AB 两台服务器, A 服务器生成授权链接(同时生成 state 并存到 A 服务器的本机内存中),第三方登录完回调到了 B 服务器,此时 B 服务器内存中没有 state 缓存,并且无法访问到 A 服务器的内存。 系统抛出异常。
- 内网穿透的情况
- 服务部署到云端,通过内网穿透到本机。在云端生成授权链接(同时生成 state 并存到云端服务器的内存中),回调请求穿透到本地,异常流程参考上面 “集群情况”。
- 前后端分离的情况
- 项目实现前后端分离,并且生成第三方授权链接时为前端项目自行拼接,未使用 JA 提供的 authorize 方法。JA 在处理 code 换 token 时,默认会校验 state 是否存在,前端自己拼接的链接授权后,即使第三方返回了 state,但因为没有使用 JA 提供的 authorize 方法,所以缓存中并不存在 state,因此会导致验证失败。
解决方案
- 针对第一种情况,先排查是否存在异常情况,然后具体分析为什么会延迟3分钟(是否本地在进行 DEBUG?是否第三方授权时长时间未点击确认授权)。最后可以适当将AuthCacheConfig.java#timeout(opens new window)参数值调高。
- 针对第二种情况,建议使用自定义state缓存
- 针对第三种情况,可临时将 AuthConfig.java#ignoreCheckState(opens new window)参数设为 true,待测试完成后,再将配置修改回来(建议仅本地测试使用,如需上线,不建议将ignoreCheckState设为 true)
- 针对第四种情况,可将 AuthConfig.java#ignoreCheckState(opens new window)参数设为 true(仅此情况下,才建议将ignoreCheckState设为 true)
- 如果不属于以上情况,另外还可以使用临时解决方案:将 AuthConfig.java#ignoreCheckState(opens new window)参数设为 true。至于为何将该条标注为【不建议】,请参考下图:
- 结合异常原因分析本次生产出现这个异常的原因
- 我们的测试环境是单台机器所以不会出现这个问题
- 我们这个服务在生产部署了2个
- 所以异常原因是集群导致的state异常
- 解决方案
- 快速临时解决方案: 停掉一个服务(快速解决问题,让服务可用)
- 集群解决方案:自定义state缓存
JustAuth 自定义state缓存(redis缓存)
- 添加redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 添加redis配置
# 集成redis实现自定义的state缓存
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=xxxx
- 实现state缓存接口
package me.zhyd.justauth;
import me.zhyd.oauth.cache.AuthCacheConfig;
import me.zhyd.oauth.cache.AuthStateCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
/**
* 扩展Redis版的state缓存
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0
* @date 2019/10/24 13:38
* @since 1.8
*/
@Component
public class AuthStateRedisCache implements AuthStateCache {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private ValueOperations<String, String> valueOperations;
@PostConstruct
public void init() {
valueOperations = redisTemplate.opsForValue();
}
/**
* 存入缓存,默认3分钟
*
* @param key 缓存key
* @param value 缓存内容
*/
@Override
public void cache(String key, String value) {
valueOperations.set(key, value, AuthCacheConfig.timeout, TimeUnit.MILLISECONDS);
}
/**
* 存入缓存
*
* @param key 缓存key
* @param value 缓存内容
* @param timeout 指定缓存过期时间(毫秒)
*/
@Override
public void cache(String key, String value, long timeout) {
valueOperations.set(key, value, timeout, TimeUnit.MILLISECONDS);
}
/**
* 获取缓存内容
*
* @param key 缓存key
* @return 缓存内容
*/
@Override
public String get(String key) {
return valueOperations.get(key);
}
/**
* 是否存在key,如果对应key的value值已过期,也返回false
*
* @param key 缓存key
* @return true:存在key,并且value没过期;false:key不存在或者已过期
*/
@Override
public boolean containsKey(String key) {
return redisTemplate.hasKey(key);
}
}
- 获取Request(以飞书为例)
// 1. 注入新添加的cache
@Autowired
private AuthStateRedisCache stateRedisCache;
// 2. 创建request时传入stateRedisCache
AuthRequest authRequest = new AuthFeishuRequest(AuthConfig.builder()
.clientId("clientId")
.clientSecret("clientSecret")
.redirectUri("redirectUri")
.build(), stateRedisCache);// 此处传入自定义实现的类