使用线程本地变量存储会员信息
线程本地变量
功能:JWT单点登录时,后端通过前端请求header中的token解析出会员信息,这样就可以不通过前端传递就可以获取当前会员的信息。
线程本地变量
import com.guaigen.train.common.resp.MemberLoginResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoginMemberContext {
private static final Logger LOG = LoggerFactory.getLogger(LoginMemberContext.class);
private static ThreadLocal<MemberLoginResp> member = new ThreadLocal<>();
public static MemberLoginResp getMember() {
return member.get();
}
public static void setMember(MemberLoginResp member) {
LoginMemberContext.member.set(member);
}
public static Long getId() {
try {
return member.get().getId();
} catch (Exception e) {
LOG.info("获取会员登录信息异常:{}", e);
throw e;
}
}
}
定义了一个名为 LoginMemberContext
的工具类,该类使用了 ThreadLocal
来存储和获取当前会员的登录信息(MemberLoginResp
)。ThreadLocal
确保每个线程都有自己独立的 MemberLoginResp
实例,这在多线程环境中非常有用,例如在 Web 应用程序中,每个请求由单独的线程处理。
代码的主要功能如下:
-
获取当前会员信息:
getMember()
方法用于获取当前线程中存储的会员登录信息。
-
设置当前会员信息:
setMember(MemberLoginResp member)
方法用于将会员信息存储到当前线程中。
-
获取会员ID:
getId()
方法用于获取当前线程中存储的会员ID。该方法包含了异常处理,如果在获取会员ID时发生异常,将记录错误信息并抛出异常。
这种设计确保了每个线程的登录上下文是独立的,不会相互影响,适合在需要线程隔离的场景中使用。
拦截器拦截线程本地变量
拦截器
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.guaigen.train.common.util.JwtUtil;
import com.guaigen.train.common.context.LoginMemberContext;
import com.guaigen.train.common.resp.MemberLoginResp;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
*/
@Component
public class MemberInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(MemberInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取header的token参数
String token = request.getHeader("token");
if (StrUtil.isNotBlank(token)) {
LOG.info("获取会员登录token:{}", token);
JSONObject loginMember = JwtUtil.getJSONObject(token);
LOG.info("当前登录会员:{}", loginMember);
MemberLoginResp member = JSONUtil.toBean(loginMember, MemberLoginResp.class);
LoginMemberContext.setMember(member);
}
return true;
}
}
主要功能:
-
获取 Token:
- 在每次请求处理之前,拦截器会从请求的
header
中获取token
参数。 StrUtil.isNotBlank(token)
用于检查token
是否非空且非空白字符。
- 在每次请求处理之前,拦截器会从请求的
-
解析 Token:
- 如果
token
存在且有效,拦截器使用JwtUtil.getJSONObject(token)
方法将token
解析为JSONObject
对象。 - 日志记录:拦截器会将获取到的
token
以及解析后的会员信息记录到日志中,便于跟踪和调试。
- 如果
-
保存会员登录信息:
- 解析后的
JSONObject
对象会被转换为MemberLoginResp
对象。 - 调用
LoginMemberContext.setMember(member)
方法,将该会员信息存储到当前线程的上下文中,以便在后续请求处理中使用。
- 解析后的
-
继续处理请求:
- 如果拦截器成功获取并处理了
token
,会返回true
,表示可以继续处理该请求。否则,请求可能会被阻止或进一步处理。
这个拦截器通常用于登录验证,确保请求中包含有效的
token
,并将登录信息加载到上下文中供后续使用。这样的设计在需要用户认证和权限管理的场景中非常常见。 - 如果拦截器成功获取并处理了
可能的扩展:
- 权限校验:除了登录验证,还可以扩展该拦截器,添加权限校验功能,确保用户有权访问某些资源或执行特定操作。
- 日志记录:记录更多的请求细节,如请求路径、请求参数等,以便进行审计和问题排查。
配置文件
config
import com.guaigen.train.common.interceptor.MemberInterceptor;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
MemberInterceptor memberInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(memberInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/member/hello",
"/member/member/sendCode",
"/member/member/login"
);
}
}
代码解析:
-
@Configuration
注解:- 这个类被标注为一个配置类,意味着它定义了一些 Spring 容器的配置信息。
-
WebMvcConfigurer
接口:SpringMvcConfig
实现了WebMvcConfigurer
接口,这是 Spring MVC 提供的一个扩展点,允许你自定义配置 MVC 的行为,而不需要继承WebMvcConfigurerAdapter
(在 Spring 5 中已废弃)。
-
@Resource
注解:@Resource
注解用于自动注入MemberInterceptor
实例。Spring 将自动扫描并注入已定义的MemberInterceptor
Bean。
-
addInterceptors
方法:addInterceptors(InterceptorRegistry registry)
方法用于注册拦截器,并配置它应用于哪些请求路径。
-
配置拦截器路径:
registry.addInterceptor(memberInterceptor)
将MemberInterceptor
添加到拦截器链中。.addPathPatterns("/**")
指定拦截器应用于所有路径(/**
表示匹配所有路径)。.excludePathPatterns("/member/hello", "/member/member/sendCode", "/member/member/login")
指定某些路径不被拦截器拦截,这通常是登录、验证码发送等公共接口。
功能概述:
-
全局拦截:
- 这个配置类将
MemberInterceptor
应用于所有请求路径,但排除了一些指定的公共路径。这意味着除了登录和发送验证码的路径外,所有其他路径都将经过MemberInterceptor
的处理。
- 这个配置类将
-
典型场景:
- 这种配置方式通常用于保护应用的业务逻辑部分,确保只有通过认证的用户才能访问特定资源。同时,也保留了一些不需要认证的公共接口供外部使用。
可扩展性:
- 根据需要,增加或减少
excludePathPatterns
中的路径,以控制哪些请求路径不需要经过拦截器。 - 如果有多个拦截器,也可以通过
registry.addInterceptor(...)
继续添加和配置其他拦截器。
后端使用
后端Service调用
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import com.guaigen.train.common.context.LoginMemberContext;
import com.guaigen.train.common.util.SnowUtil;
import com.guaigen.train.member.domain.Passenger;
import com.guaigen.train.member.mapper.PassengerMapper;
import com.guaigen.train.member.req.PassengerSaveReq;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class PassengerService {
@Resource
private PassengerMapper passengerMapper;
public void save(PassengerSaveReq req) {
DateTime now = DateTime.now();
Passenger passenger = BeanUtil.copyProperties(req, Passenger.class);
passenger.setMemberId(LoginMemberContext.getId());
passenger.setId(SnowUtil.getSnowflakeNestId());
passenger.setCreatedTime(now);
passenger.setModifiedTime(now);
passengerMapper.insert(passenger);
}
}
其中passenger.setMemberId(LoginMemberContext.getId());就是通过线程本地变量获取memberId会员信息