SpringSecurity微服务鉴权方式
前述
源代码已经上传到 Gitee,对应项目为
security-cloud2
本文在SpringSecurity在微服务中的应用 基础上进行修改
代码中使用的
redis
的有关配置请参考 SpringBoot整合Redis网关鉴权以及权限判断几个关键代码参考了RuoYi-Cloud
本实例旨在学习如何在网关(
Gateway
)中去实现系统鉴权等
微服务鉴权
微服务中鉴权大致分为两种
1、在各个服务中进行鉴权,网关层面只做路由转发,这个可以参考下SpringSecurity在微服务中的应用
2、在网关处就实现鉴权并进行路由转发。security-cloud2
就是在网关处进行鉴权
自定义鉴权注解
在 security-cloud2
中,鉴权注解有三种,分别是 RequiresLogin
、RequiresPermissions
RequiresRoles
。
1、RequiresLogin
/**
* 登录认证:只有登录之后才能进入该方法
*
* @author LiJunYi
* @date 2022/08/03
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresLogin{}
2、RequiresPermissions
/**
* @version 1.0.0
* @className: RequiresPermissions
* @description: 权限认证:必须具有指定权限才能进入该方法
* @author: LiJunYi
* @create: 2022/8/2 14:48
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresPermissions
{
/**
* 需要校验的权限码
*/
String[] value() default {};
/**
* 验证模式:AND | OR,默认AND
*/
Logical logical() default Logical.AND;
}
3、RequiresRoles
/**
* 角色认证:必须具有指定角色标识才能进入该方法
*
* @author LiJunYi
* @date 2022/08/03
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresRoles
{
/**
* 需要校验的角色标识
*/
String[] value() default {};
/**
* 验证逻辑:AND | OR,默认AND
*/
Logical logical() default Logical.AND;
}
4、基于AOP的注解鉴权实现
/**
* 基于 Spring Aop 的注解鉴权
*
* @author LiJunYi
* @date 2022/08/03
*/
@Aspect
@Component
public class PreAuthorizeAspect
{
/**
* 构建
*/
public PreAuthorizeAspect()
{
}
/**
* 定义AOP签名 (切入所有使用鉴权注解的方法)
*/
public static final String POINTCUT_SIGN = " @annotation(org.example.common.security.annotation.RequiresLogin) || "
+ "@annotation(org.example.common.security.annotation.RequiresPermissions) || "
+ "@annotation(org.example.common.security.annotation.RequiresRoles)";
/**
* 声明AOP签名
*/
@Pointcut(POINTCUT_SIGN)
public void pointcut()
{
}
/**
* 环绕切入
*
* @param joinPoint 切面对象
* @return 底层方法执行后的返回值
* @throws Throwable 底层方法抛出的异常
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable
{
// 注解鉴权
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
checkMethodAnnotation(signature.getMethod());
try
{
// 执行原有逻辑
return joinPoint.proceed();
}
catch (Throwable e)
{
throw e;
}
}
/**
* 对一个Method对象进行注解检查
*/
public void checkMethodAnnotation(Method method)
{
// 校验 @RequiresLogin 注解
RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
if (requiresLogin != null)
{
AuthUtil.checkLogin();
}
// 校验 @RequiresRoles 注解
RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
if (requiresRoles != null)
{
AuthUtil.checkRole(requiresRoles);
}
// 校验 @RequiresPermissions 注解
RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
if (requiresPermissions != null)
{
AuthUtil.checkPermission(requiresPermissions);
}
}
}
具体鉴权工具类
AuthUtil
/**
* @version 1.0.0
* @className: AuthUtil
* @description: Token 权限验证工具类
* @author: LiJunYi
* @create: 2022/8/2 14:43
*/
public class AuthUtil
{
/**
* 底层的 AuthLogic 对象
*/
public static AuthLogic authLogic = new AuthLogic();
/**
* 会话注销
*/
public static void logout()
{
authLogic.logout();
}
/**
* 会话注销,根据指定Token
*
* @param token 指定token
*/
public static void logoutByToken(String token)
{
authLogic.logoutByToken(token);
}
/**
* 检验当前会话是否已经登录,如未登录,则抛出异常
*/
public static void checkLogin()
{
authLogic.checkLogin();
}
/**
* 获取当前登录用户信息
*/
public static LoginUser getLoginUser(String token)
{
return authLogic.getLoginUser(token);
}
/**
* 验证当前用户有效期
*/
public static void verifyLoginUserExpire(LoginUser loginUser)
{
authLogic.verifyLoginUserExpire(loginUser);
}
/**
* 当前账号是否含有指定角色标识, 返回true或false
*
* @param role 角色标识
* @return 是否含有指定角色标识
*/
public static boolean hasRole(String role)
{
return authLogic.hasRole(role);
}
/**
* 当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
*
* @param role 角色标识
*/
public static void checkRole(String role)
{
authLogic.checkRole(role);
}
/**
* 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotRoleException
*
* @param requiresRoles 角色权限注解
*/
public static void checkRole(RequiresRoles requiresRoles)
{
authLogic.checkRole(requiresRoles);
}
/**
* 当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
*
* @param roles 角色标识数组
*/
public static void checkRoleAnd(String... roles)
{
authLogic.checkRoleAnd(roles);
}
/**
* 当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
*
* @param roles 角色标识数组
*/
public static void checkRoleOr(String... roles)
{
authLogic.checkRoleOr(roles);
}
/**
* 当前账号是否含有指定权限, 返回true或false
*
* @param permission 权限码
* @return 是否含有指定权限
*/
public static boolean hasPermission(String permission)
{
return authLogic.hasPermission(permission);
}
/**
* 当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
*
* @param permission 权限码
*/
public static void checkPermission(String permission)
{
authLogic.checkPermission(permission);
}
/**
* 根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotPermissionException
*
* @param requiresPermissions 权限注解
*/
public static void checkPermission(RequiresPermissions requiresPermissions)
{
authLogic.checkPermission(requiresPermissions);
}
/**
* 当前账号是否含有指定权限 [指定多个,必须全部验证通过]
*
* @param permissions 权限码数组
*/
public static void checkPermissionAnd(String... permissions)
{
authLogic.checkPermissionAnd(permissions);
}
/**
* 当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
*
* @param permissions 权限码数组
*/
public static void checkPermissionOr(String... permissions)
{
authLogic.checkPermissionOr(permissions);
}
}
AuthLogic-权限验证,逻辑实现类
/**
* @version 1.0.0
* @className: AuthLogic
* @description: Token 权限验证,逻辑实现类
* @author: LiJunYi
* @create: 2022/8/2 14:44
*/
public class AuthLogic
{
/** 所有权限标识 */
private static final String ALL_PERMISSION = "*:*:*";
/** 管理员角色权限标识 */
private static final String SUPER_ADMIN = "admin";
public TokenService tokenService = SpringUtil.getBean(TokenService.class);
/**
* 会话注销
*/
public void logout()
{
String token = SecurityUtils.getToken();
if (token == null)
{
return;
}
logoutByToken(token);
}
/**
* 会话注销,根据指定Token
*/
public void logoutByToken(String token)
{
tokenService.delLoginUser(token);
}
/**
* 检验用户是否已经登录,如未登录,则抛出异常
*/
public void checkLogin()
{
getLoginUser();
}
/**
* 获取当前用户缓存信息, 如果未登录,则抛出异常
*
* @return 用户缓存信息
*/
public LoginUser getLoginUser()
{
String token = SecurityUtils.getToken();
if (token == null)
{
throw new NotLoginException("未提供token");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null)
{
throw new NotLoginException("无效的token");
}
return loginUser;
}
/**
* 获取当前用户缓存信息, 如果未登录,则抛出异常
*
* @param token 前端传递的认证信息
* @return 用户缓存信息
*/
public LoginUser getLoginUser(String token)
{
return tokenService.getLoginUser(token);
}
/**
* 验证当前用户有效期, 如果相差不足120分钟,自动刷新缓存
*
* @param loginUser 当前用户信息
*/
public void verifyLoginUserExpire(LoginUser loginUser)
{
tokenService.verifyToken(loginUser);
}
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermission(String permission)
{
return hasPermission(getPermissionList(), permission);
}
/**
* 验证用户是否具备某权限, 如果验证未通过,则抛出异常: NotPermissionException
*
* @param permission 权限字符串
*/
public void checkPermission(String permission)
{
if (!hasPermission(getPermissionList(), permission))
{
throw new NotPermissionException(permission);
}
}
/**
* 根据注解(@RequiresPermissions)鉴权, 如果验证未通过,则抛出异常: NotPermissionException
*
* @param requiresPermissions 注解对象
*/
public void checkPermission(RequiresPermissions requiresPermissions)
{
if (requiresPermissions.logical() == Logical.AND)
{
checkPermissionAnd(requiresPermissions.value());
}
else
{
checkPermissionOr(requiresPermissions.value());
}
}
/**
* 验证用户是否含有指定权限,必须全部拥有
*
* @param permissions 权限列表
*/
public void checkPermissionAnd(String... permissions)
{
Set<String> permissionList = getPermissionList();
for (String permission : permissions)
{
if (!hasPermission(permissionList, permission))
{
throw new NotPermissionException(permission);
}
}
}
/**
* 验证用户是否含有指定权限,只需包含其中一个
*
* @param permissions 权限码数组
*/
public void checkPermissionOr(String... permissions)
{
Set<String> permissionList = getPermissionList();
for (String permission : permissions)
{
if (hasPermission(permissionList, permission))
{
return;
}
}
if (permissions.length > 0)
{
throw new NotPermissionException(permissions);
}
}
/**
* 判断用户是否拥有某个角色
*
* @param role 角色标识
* @return 用户是否具备某角色
*/
public boolean hasRole(String role)
{
return hasRole(getRoleList(), role);
}
/**
* 判断用户是否拥有某个角色, 如果验证未通过,则抛出异常: NotRoleException
*
* @param role 角色标识
*/
public void checkRole(String role)
{
if (!hasRole(role))
{
throw new NotRoleException(role);
}
}
/**
* 根据注解(@RequiresRoles)鉴权
*
* @param requiresRoles 注解对象
*/
public void checkRole(RequiresRoles requiresRoles)
{
if (requiresRoles.logical() == Logical.AND)
{
checkRoleAnd(requiresRoles.value());
}
else
{
checkRoleOr(requiresRoles.value());
}
}
/**
* 验证用户是否含有指定角色,必须全部拥有
*
* @param roles 角色标识数组
*/
public void checkRoleAnd(String... roles)
{
Set<String> roleList = getRoleList();
for (String role : roles)
{
if (!hasRole(roleList, role))
{
throw new NotRoleException(role);
}
}
}
/**
* 验证用户是否含有指定角色,只需包含其中一个
*
* @param roles 角色标识数组
*/
public void checkRoleOr(String... roles)
{
Set<String> roleList = getRoleList();
for (String role : roles)
{
if (hasRole(roleList, role))
{
return;
}
}
if (roles.length > 0)
{
throw new NotRoleException(roles);
}
}
/**
* 获取当前账号的角色列表
*
* @return 角色列表
*/
public Set<String> getRoleList()
{
try
{
LoginUser loginUser = getLoginUser();
return loginUser.getRoles();
}
catch (Exception e)
{
return new HashSet<>();
}
}
/**
* 获取当前账号的权限列表
*
* @return 权限列表
*/
public Set<String> getPermissionList()
{
try
{
LoginUser loginUser = getLoginUser();
return loginUser.getPermissions();
}
catch (Exception e)
{
return new HashSet<>();
}
}
/**
* 判断是否包含权限
*
* @param authorities 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermission(Collection<String> authorities, String permission)
{
return authorities.stream().filter(StringUtils::hasText)
.anyMatch(x -> ALL_PERMISSION.contains(x) || PatternMatchUtils.simpleMatch(x, permission));
}
/**
* 判断是否包含角色
*
* @param roles 角色列表
* @param role 角色
* @return 用户是否具备某角色权限
*/
public boolean hasRole(Collection<String> roles, String role)
{
return roles.stream().filter(StringUtils::hasText)
.anyMatch(x -> SUPER_ADMIN.contains(x) || PatternMatchUtils.simpleMatch(x, role));
}
}
鉴权思路
大体就是在用户登录时,将用户的权限与角色封装到登录类 LoginUser
中,然后在拦截器中,将用户有关信息存入线程中,后续请求操作里,如果有鉴权需要,在鉴权工具类中就可以从当前线程里获取到登录用户信息,包括权限、角色、token等。
这里的线程使用了阿里的
TransmittableThreadLocal
,官网地址, TTL的使用后面再详细说明下。TransmittableThreadLocal
(TTL
):在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal
值的传递功能,解决异步执行时上下文传递的问题。
TTL有关设定
SecurityContextHolder
/**
* @version 1.0.0
* @className: SecurityContextHolder
* @description: 获取当前线程变量中的用户id、用户名称、Token等信息
* @author: LiJunYi
* @create: 2022/8/2 13:05
*/
public class SecurityContextHolder
{
private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();
/**
* 设置值
*
* @param key key
* @param value 值
*/
public static void set(String key, Object value)
{
Map<String, Object> map = getLocalMap();
map.put(key, value == null ? StringUtils.EMPTY : value);
}
/**
* 获取
*
* @param key 关键
* @return {@link String}
*/
public static String get(String key)
{
Map<String, Object> map = getLocalMap();
return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY));
}
/**
* 获取
*
* @param key 关键
* @param clazz clazz
* @return {@link T}
*/
public static <T> T get(String key, Class<T> clazz)
{
Map<String, Object> map = getLocalMap();
return (T) map.getOrDefault(key, null);
}
/**
* 获取当地Map集合
*
* @return {@link Map}<{@link String}, {@link Object}>
*/
public static Map<String, Object> getLocalMap()
{
Map<String, Object> map = THREAD_LOCAL.get();
if (map == null)
{
map = new ConcurrentHashMap<String, Object>();
THREAD_LOCAL.set(map);
}
return map;
}
public static void setLocalMap(Map<String, Object> threadLocalMap)
{
THREAD_LOCAL.set(threadLocalMap);
}
public static Long getUserId()
{
return Convert.toLong(get(SecurityConstants.DETAILS_USER_ID), 0L);
}
public static void setUserId(String account)
{
set(SecurityConstants.DETAILS_USER_ID, account);
}
public static String getUserName()
{
return get(SecurityConstants.DETAILS_USERNAME);
}
public static void setUserName(String username)
{
set(SecurityConstants.DETAILS_USERNAME, username);
}
public static String getUserKey()
{
return get(SecurityConstants.USER_KEY);
}
public static void setUserKey(String userKey)
{
set(SecurityConstants.USER_KEY, userKey);
}
public static void remove()
{
THREAD_LOCAL.remove();
}
}
自定义请求头拦截器
/**
* @version 1.0.0
* @className: HeaderInterceptor
* @description: 自定义请求头拦截器,将Header数据封装到线程变量中方便获取
* @author: LiJunYi
* @create: 2022/8/2 14:40
*/
public class HeaderInterceptor implements AsyncHandlerInterceptor
{
/**
* 在执行controller方法之前将请求头中的token信息解析出来,放入SecurityContextHolder中(TransmittableThreadLocal)
* @param request 请求
* @param response 响应
* @param handler 处理程序
* @return boolean
* @throws Exception 异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (!(handler instanceof HandlerMethod))
{
return true;
}
SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));
SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));
SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));
String token = SecurityUtils.getToken();
if (StringUtils.isNotEmpty(token))
{
LoginUser loginUser = AuthUtil.getLoginUser(token);
if (ObjectUtil.isNotNull(loginUser))
{
AuthUtil.verifyLoginUserExpire(loginUser);
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
}
}
return true;
}
/**
* 在视图渲染之后执行,意味着一次请求结束,清除TTL中的身份信息
*
* @param request 请求
* @param response 响应
* @param handler 处理程序
* @param ex 异常
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
SecurityContextHolder.remove();
}
}
WebMvcConfig配置加入请求头
/**
* @version 1.0.0
* @className: WebMvcConfig
* @description: 拦截器设备
* @author: LiJunYi
* @create: 2022/8/2 14:36
*/
public class WebMvcConfig implements WebMvcConfigurer
{
/** 不需要拦截地址 */
public static final String[] excludeUrls = { "/login", "/logout", "/refresh" };
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(getHeaderInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(excludeUrls)
.order(-10);
}
/**
* 自定义请求头拦截器
*/
public HeaderInterceptor getHeaderInterceptor()
{
return new HeaderInterceptor();
}
}
openFeign的设置
因为需要用到openFeign来发送请求,所以需要配置下openFeign,防止OpenFeign 异步调用丢失上下文信息
自定义请求拦截器
/**
* feign 请求拦截器
*
* @author LiJunYi
* @date 2022/08/03
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor
{
@Override
public void apply(RequestTemplate requestTemplate)
{
HttpServletRequest httpServletRequest = ServletUtils.getRequest();
if (ObjectUtil.isNotNull(httpServletRequest))
{
Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
// 传递用户信息请求头,防止丢失
String userId = headers.get(SecurityConstants.DETAILS_USER_ID);
if (StrUtil.isNotEmpty(userId))
{
requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);
}
String userName = headers.get(SecurityConstants.DETAILS_USERNAME);
if (StrUtil.isNotEmpty(userName))
{
requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);
}
String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
if (StrUtil.isNotEmpty(authentication))
{
requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
}
// 配置客户端IP
requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr(ServletUtils.getRequest()));
}
}
}
加入配置
/**
* Feign 配置注册
*
* @author LiJunYi
* @date 2022/08/03
*/
@Configuration
public class FeignAutoConfiguration
{
@Bean
public RequestInterceptor requestInterceptor()
{
return new FeignRequestInterceptor();
}
}
自定义feign注解
/**
* 自定义feign注解
*
* @author LiJunYi
* @date 2022/08/03
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableFeignClients
public @interface EnableRyFeignClients
{
String[] value() default {};
String[] basePackages() default { "org.example" };
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
网关中AuthFilter鉴权过滤器
/**
* @version 1.0.0
* @className: AuthFilter
* @description: 网关鉴权
* @author: LiJunYi
* @create: 2022/8/2 15:13
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered
{
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Autowired
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
// 跳过不需要验证的路径
List<String> ignoreUrls = ignoreWhite.getWhites();
if (ignoreUrls.stream().anyMatch(f -> f.equalsIgnoreCase(url)))
{
return chain.filter(exchange);
}
String token = getToken(request);
if (StrUtil.isEmpty(token))
{
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims = JwtUtils.parseToken(token);
if (claims == null)
{
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin)
{
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
if (StrUtil.isEmpty(userid) || StrUtil.isEmpty(username))
{
return unauthorizedResponse(exchange, "令牌验证失败");
}
// 设置用户信息到请求
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value)
{
if (value == null)
{
return;
}
String valueStr = value.toString();
String valueEncode = ServletUtils.urlEncode(valueStr);
mutate.header(name, valueEncode);
}
private void removeHeader(ServerHttpRequest.Builder mutate, String name)
{
mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg)
{
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
}
/**
* 获取缓存key
*/
private String getTokenKey(String token)
{
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
/**
* 获取请求token
*/
private String getToken(ServerHttpRequest request)
{
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StrUtil.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
{
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
return token;
}
@Override
public int getOrder() {
return -200;
}
}
最终的效果
最后
目前自己感觉鉴权主要有关的方法就是上面几个了,虽然目前还不够深入,但也算给了自己一点思路,比如如何将用户信息传递给下游服务、openFeign异步调用下信息丢失怎么处理,知道了有TTL这种线程数据工具等。希望后续能够更加深入理解、熟悉微服务鉴权方案😁