业务场景:
原生是一个考试系统,在后续需求中,需要添加一个中介用户进行登录,进行中介的对应的业务操作,但是原来版本写的太死了,在c端只能一种用户登录,然后我进了升级
一、登录逻辑差不多是一致的,创建token/sessionKey相关值,然后将对应的数据存放在redis中
二、在springboot的WebMvcConfigurer配置添加拦截器,做好对应c端请求的拦截过滤以及校验,这里拦截器c端的统一以/api开头
三、在拦截器中,根据/api/以后的前缀执行不同的处理,(只要实现ApiSessionKeyFilter中),将不同的登录用户信息从redis中取出,放入到TransmittableThreadLocal线程本地变量中
四、业务代码中直接获取出当前用户信息即可
ApiSessionInterceptor.java
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* ThirdSession拦截器
* 校验每个请求的ThirdSession
*
* @author Zheng
*/
@Component
public class ApiSessionInterceptor implements HandlerInterceptor {
/**
* 判断接口是否添加了需要携带session的注解 @MiniLogin
* 以及请求是否携带sessionKey,并且判断session是否过期
*
* @param request Request
* @param response Response
* @param handler handler
* @return 是否通行
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断静态资源放行
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
//判断访问的control是否添加ApiLogin注解
if (method.hasMethodAnnotation(ApiLoginOpen.class)) {
// 添加open接口得放开访问
return Boolean.TRUE;
}
String requestURI = request.getRequestURI();
Map<String, ApiSessionKeyFilter> apiSessionKeyFilterBeanByNameMap
= SpringUtil.getBeansOfType(ApiSessionKeyFilter.class);
for (Map.Entry<String, ApiSessionKeyFilter> apiSessionKeyFilterEntry : apiSessionKeyFilterBeanByNameMap.entrySet()) {
ApiSessionKeyFilter filter = apiSessionKeyFilterEntry.getValue();
if (requestURI.startsWith(filter.getUrlPrefix())) {
return filter.filter(request, response);
}
}
throw new Exception("无权限");
} else {
return Boolean.TRUE;
}
}
}
ResourcesConfig.class
*/
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiSessionInterceptor).addPathPatterns("/api/**")
.excludePathPatterns("/api/captcha");
}
}
ApiSessionHolder.java
/**
* 全链路追踪
* thirdSession工具类
*
* @author Zheng
*/
@UtilityClass
public class ApiSessionHolder {
private final ThreadLocal<Object> THREAD_LOCAL_THIRD_SESSION = new TransmittableThreadLocal<>();
/**
* TTL 设置 当前登录学员信息
*
* @param obj 登录用户信息
*/
public void setSessionObj(Object obj) {
THREAD_LOCAL_THIRD_SESSION.set(obj);
}
/**
* 获取TTL中的LoginStudent
*
* @return 登录学员信息
*/
public Object getSessionObj() {
return THREAD_LOCAL_THIRD_SESSION.get();
}
/**
* 获取 SessionObj 的id信息
*
* @return id数
*/
public Long getSessionObjId() {
Object sessionObj = getSessionObj();
if (sessionObj == null) {
return null;
}
Object id = ReflectUtil.getFieldValue(sessionObj, "id");
if (id == null) {
return null;
}
if (id instanceof Long) {
return (Long) id;
}
return null;
}
/**
* 清除
*/
public void clear() {
THREAD_LOCAL_THIRD_SESSION.remove();
}
}
ApiSessionKeyFilter.java
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.ruoyi.common.core.domain.AjaxResult;
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* ApiSessionFilter
*
* @author SERVER
* @since 2024/3/29 14:15
*/
public interface ApiSessionKeyFilter {
/**
* 获取apiUrl 模块地址信息
*
* @return 模块名称
*/
String getModelUrl();
/**
* 获取地址信息
*
* @return re
*/
default String getUrlPrefix() {
return "/api" + (StrUtil.isBlank(this.getModelUrl()) ? "" : "/" + this.getModelUrl());
}
/**
* 过滤sessionKey信息过滤
*
* @param request 请求信息
* @param response 返回信息
* @return true/false
*/
boolean filter(HttpServletRequest request, HttpServletResponse response) throws Exception ;
/**
* 向页面返回的
*
* @param response
* @param r
* @throws IOException
*/
default void writerPrint(HttpServletResponse response, AjaxResult r) throws IOException {
//返回超时错误码
response.setCharacterEncoding("UTF-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
writer.print(JSONUtil.parseObj(r));
if (writer != null) {
writer.close();
}
}
}
常量以及key相关值生成工具
ApiSessionUtils.class
import cn.hutool.core.util.StrUtil;
import org.apache.poi.util.StringUtil;
/**
* ApiSessionUtils
*
* @author zheng
* @since 2023/10/25 20:30
*/
public class ApiSessionUtils {
/**
* 创建缓存前缀用于批量删除
*
* @param clazz 类型便阿门
* @param distinguishedContents 区别信息内容
* @return 缓存键前缀
*/
public static String createCacheKeyPrefix(Class<?> clazz, String... distinguishedContents) {
return ApiSessionConst.SESSION_BEGIN +
StrUtil.COLON +
clazz.getSimpleName() +
StrUtil.COLON +
StringUtil.join(distinguishedContents, StrUtil.COLON);
}
/**
* 获取缓存的key信息
*
* @param clazz 数据类型
* @param sessionKey 会话中带的key信息
* @return 缓存的key信息
*/
public static String createCacheKey(Class<?> clazz, String sessionKey) {
return ApiSessionConst.SESSION_BEGIN +
StrUtil.COLON +
clazz.getSimpleName() +
StrUtil.COLON +
sessionKey;
}
/**
* 创建sessionKey
*
* @param distinguishedContents 区分内容信息
* @return SessionKey
*/
public static String createSessionKey(String... distinguishedContents) {
return StringUtil.join(distinguishedContents, StrUtil.COLON);
}
}
ApiSessionConst.java
/**
* @author Zheng
*/
public class ApiSessionConst {
/**
* 小程序登录。存在redis中的学员信息键名拼接前缀
*/
public final static String SESSION_BEGIN = "api:session";
/**
* 小程序登录。存在redis中的学员信息过期时间(单位:小时)
*/
public final static Integer SESSION_TIME_OUT = 24;
/**
* 小程序登录。存在 header 中的参数键
*/
public final static String SESSION_KEY = "session-key";
}
注意
TransmittableThreadLocal 是阿里提供的工具类
它是ThreadLocal的一个增强版,可以在线程池等多线程环境下使用,解决了ThreadLocal在多线程环境下的一些问题。
在多线程环境下,ThreadLocal可以避免线程安全问题,但是在使用线程池等多线程环境时,ThreadLocal可能会出现一些问题。例如,当使用线程池时,线程池中的线程可能会被多个任务共享,如果使用ThreadLocal存储数据,可能会导致数据被错误地共享。>TransmittableThreadLocal解决了这个问题,它可以在多线程环境下正确地传递数据。具体来说,当一个线程从线程池中获取到一个线程时,TransmittableThreadLocal会自动将当前线程的ThreadLocal副本传递给新的线程,从而保证数据的正确性