9.4 KiB
上小节 中,我们完成了退出登录接口的开发工作。但是,获取当前请求对应的用户 ID,是通过在 Controller 层的方法上添加 @RequestHeader 注解来完成的,难道每次都需要手动添加它,那也太不方便了!
本小节中,我们就将使用 过滤器 + ThreadLocal ,更加方便地获取当前用户 ID 。
添加全局变量
首先,编辑 xiaoha-common 公共模块,在 /constant 包下创建一个 GlobalConstants 全局常量类,用于放置一些各个模块中常用的变量,如请求头用户 ID 键 userId , 网关服务中有用到,等会认证服务中也需要用到,干脆提取到公共模块中,省得到处都定义一份。代码如下:
package com.quanxiaoha.framework.common.constant;
public interface GlobalConstants {
/**
* 用户 ID
*/
String USER_ID = "userId";
}
Tip
: 该变量创建完毕后,就可以将网关中的变量重构一下,统一用上面这个了。
创建过滤器
接着,编辑 xiaohashu-auth 认证服务,创建 /filter 包,并创建过滤器类 HeaderUserId2ContextFilter , 代码如下:
package com.quanxiaoha.xiaohashu.auth.filter;
import com.quanxiaoha.framework.common.constant.GlobalConstants;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* @author: 犬小哈
* @date: 2024/4/15 14:01
* @version: v1.0.0
* @description: 提取请求头中的用户 ID 保存到上下文中,以方便后续使用
**/
@Component
@Slf4j
public class HeaderUserId2ContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 从请求头中获取用户 ID
String userId = request.getHeader(GlobalConstants.USER_ID);
log.info("## HeaderUserId2ContextFilter, 用户 ID: {}", userId);
// 将请求和响应传递给过滤链中的下一个过滤器。
chain.doFilter(request, response);
}
}
解释一波:
HeaderUserId2ContextFilter继承自OncePerRequestFilter,确保每个请求只会执行一次过滤操;request.getHeader(GlobalConstants.USER_ID);: 从请求头中获取用户 ID;- 打印一行日志,看看等会测试的时候,能否拿的到请求头中的用户 ID;
chain.doFilter(request, response);: 将请求和响应传递给过滤链中的下一个过滤器。如果没有下一个过滤器,则请求会到达控制器。
测试一波
过滤器添加完毕后,重启认证服务。通过 Apipost 工具请求登出接口,看看过滤器是否工作正常:
如上图所示,成功打印了请求头中的用户 ID,说明过滤器工作正常。
封装上下文工具类
然后,在 /filter 包下创建 LoginUserContextHolder 类,用于设置与获取上下文数据,代码如下:
package com.quanxiaoha.xiaohashu.auth.filter;
import com.quanxiaoha.framework.common.constant.GlobalConstants;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author: 犬小哈
* @date: 2024/4/9 18:19
* @version: v1.0.0
* @description: 登录用户上下文
**/
public class LoginUserContextHolder {
// 初始化一个 ThreadLocal 变量
private static final ThreadLocal<Map<String, Object>> LOGIN_USER_CONTEXT_THREAD_LOCAL
= ThreadLocal.withInitial(HashMap::new);
/**
* 设置用户 ID
*
* @param value
*/
public static void setUserId(Object value) {
LOGIN_USER_CONTEXT_THREAD_LOCAL.get().put(GlobalConstants.USER_ID, value);
}
/**
* 获取用户 ID
*
* @return
*/
public static Long getUserId() {
Object value = LOGIN_USER_CONTEXT_THREAD_LOCAL.get().get(GlobalConstants.USER_ID);
if (Objects.isNull(value)) {
return null;
}
return Long.valueOf(value.toString());
}
/**
* 删除 ThreadLocal
*/
public static void remove() {
LOGIN_USER_CONTEXT_THREAD_LOCAL.remove();
}
}
解释一下上述核心的部分代码:
ThreadLocal.withInitial(HashMap::new):使用ThreadLocal的withInitial方法,传入一个HashMap的构造方法引用,初始化每个线程的ThreadLocal变量时都会创建一个新的HashMap实例。之所以用HashMap数据类型,是为了方便后续扩展,如果还有新的数据,只接往里面添加即可。什么是
ThreadLocal?
ThreadLocal是 Java 中用于创建线程局部变量的工具,每个线程都有自己的独立变量副本,不会相互干扰。
remove(): 移除当前线程的ThreadLocal变量。
过滤器中设置用户 ID 到上下文
工具类编写完毕后,继续完善 HeaderUserId2ContextFilter 过滤器的逻辑,代码如下:
package com.quanxiaoha.xiaohashu.auth.filter;
import com.quanxiaoha.framework.common.constant.GlobalConstants;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* @author: 犬小哈
* @date: 2024/4/15 14:01
* @version: v1.0.0
* @description: 提取请求头中的用户 ID 保存到上下文中,以方便后续使用
**/
@Component
@Slf4j
public class HeaderUserId2ContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 从请求头中获取用户 ID
String userId = request.getHeader(GlobalConstants.USER_ID);
log.info("## HeaderUserId2ContextFilter, 用户 ID: {}", userId);
// 判断请求头中是否存在用户 ID
if (StringUtils.isBlank(userId)) {
// 若为空,则直接放行
chain.doFilter(request, response);
return;
}
// 如果 header 中存在 userId,则设置到 ThreadLocal 中
log.info("===== 设置 userId 到 ThreadLocal 中, 用户 ID: {}", userId);
LoginUserContextHolder.setUserId(userId);
try {
chain.doFilter(request, response);
} finally {
// 一定要删除 ThreadLocal ,防止内存泄露
LoginUserContextHolder.remove();
log.info("===== 删除 ThreadLocal, userId: {}", userId);
}
}
}
解释一波:
- 拿到请求头中的用户 ID 后,先判空,若为空,则直接放行;
- 如果
Header头中存在userId,则设置到ThreadLocal中;- 最后,执行完请求后,通过
remove();方法删除ThreadLocal,防止内存泄露;
重构代码
删除掉 /logout 登出接口中的 @RequestHeader 注解,如下图所示:
代码如下:
// 省略...
@PostMapping("/logout")
@ApiOperationLog(description = "账号登出")
public Response<?> logout() {
return userService.logout();
}
// 省略...
相对应的,之前service 层 logout() 方法的入参也需要删除掉:
public interface UserService {
// 省略...
/**
* 退出登录
* @return
*/
Response<?> logout();
}
在其实现方法中,就可以直接通过封装好的工具方法来获取用户 ID 啦:
// 省略...
/**
* 退出登录
*
* @return
*/
@Override
public Response<?> logout() {
Long userId = LoginUserContextHolder.getUserId();
log.info("==> 用户退出登录, userId: {}", userId);
// 退出登录 (指定用户 ID)
StpUtil.logout(userId);
return Response.success();
}
// 省略...
更方便了,有木有~ 重启项目,自测一波,看看 logout() 方法中添加的日志,是否能够正确打印当前登录用户的 ID, 如下图所示,一切正常,并且在请求执行完成后,删除了 ThreadLocal 变量:
思考
最后,我们来想一下,使用 ThreadLocal 来传递上下文,就真的能一劳永逸吗?它有什么样的问题呢?小伙伴们可以先思考一下,下小节中来着重说一说~