背景
在分布式开发成为主流之后,现在我们经常采用spring redis session作为分布式共享session的解决防范,采用Dubbo+Zookeeper作为RPC调用的解决方案;但是在RPC调用链路中获取不到HttpSession了,这就导致在一些业务场景下,加大RPC代码编写的难度
例如:上图中服务1可以获取到session,但是服务2,和服务3获取不到session,如果业务中需要获取当前登录用户的信息,这时就存在问题了
解决方案
解决方案就是在RPC调用时,在消费端隐式传递session参数,在生产端调用之前根据session参数获取相应的用户信息并绑定到当前线程。
Session 信息统一获取
为了保证RPC方式和Http方式使用一套代码来获取登录用户信息,故业务代码中不应该直接通过request等对象来获取信息,需要单独封装,这里封装为两个类HttpContext和UserManager;其中HttpContext主要用来获取session信息,UserManager用来获取用户信息
HttpContext类
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
public class HttpContext {
public static final String SESSION_KEY_CURRENT_USER_ID="SESSION_KEY_CURRENT_USER_ID";
public static final String SESSION_KEY_CURRENT_USER_NAME="SESSION_KEY_CURRENT_USER_NAME";
public static final String SESSION_KEY_CURRENT_USER_LOGIN_ACCOUNT="SESSION_KEY_CURRENT_USER_LOGIN_ACCOUNT";
//用來存储RPC调用时的session相关信息
public static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<Map<String, Object>>();
public static HttpServletRequest getRequest(){
ServletRequestAttributes servletRequestAttributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(servletRequestAttributes==null)
return null;
HttpServletRequest request =servletRequestAttributes.getRequest();
return request;
}
public static HttpSession getSession(){
if( getRequest()==null) {
return null;
}
HttpSession session = getRequest().getSession();
return session;
}
/**
* 获取sessionId
* @return
*/
public static String getSessionId() {
HttpSession session = getSession();
if(null != session) {
//从http session中获取
return session.getId();
} else {
//从线程本地存储中获取
//判断当前线程是否有数据
Map<String, Object> data = HttpContext.threadLocal.get();
return null != data ? (String)data.get("sessionId") : null;
}
}
}
UserManager类
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import com.uiot.util.SpringContextUtil;
import com.uiot.web.HttpContext;
//UserInfo类根据需要自行替换
public class UserManager extends HttpContext{
public static final String SESSION_KEY_CURRENT_USER="SESSION_KEY_CURRENT_USER";
public static final String SESSION_ID="SESSION_ID";
public static Object getCurrentUser() {
HttpSession session = getSession();
Object userInfo = null;
if(null != session) {
userInfo = (UserInfo) session.getAttribute(SESSION_KEY_CURRENT_USER);
} else {
//判断当前线程是否有数据
Map<String, Object> data = HttpContext.threadLocal.get();
userInfo = null != data ? (UserInfo)data.get(SESSION_KEY_CURRENT_USER) : null;
}
return userInfo;
}
public static String getUserName(){
UserInfo userInfo = getCurrentUser();
if(null != userInfo) {
return userInfo.getUserName();
} else {
return null;
}
}
public static Long getUserID(){
UserInfo userInfo = getCurrentUser();
if(null != userInfo) {
return userInfo.getUserID();
} else {
return null;
}
}
public static String getLoginAccount(){
String account = (String) getSessionAttribute(SESSION_KEY_CURRENT_USER_LOGIN_ACCOUNT);
if (null == account) {
//判断当前线程是否有数据
Map<String, Object> data = HttpContext.threadLocal.get();
account = null != data ? (String)data.get(SESSION_KEY_CURRENT_USER_LOGIN_ACCOUNT) : null;
}
return account;
}
}
Dubbo RPC调用隐式传参
创建两个类ConsumerTraceFilter 和ProviderTraceFilter 分别用于RPC调用的消费端处理和生产端处理
消费端Filter
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcException;
import com.alibaba.dubbo.rpc.RpcInvocation;
import com.uiot.platform.user.UserManager;
import com.uiot.web.HttpContext;
@Activate(group = {Constants.CONSUMER})
public class ConsumerTraceFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(ConsumerTraceFilter.class);
// 调用过程拦截
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
RpcInvocation invocation1 = (RpcInvocation) invocation;
//设置session信息
setSessionAttachment(invocation1);
Result result = invoker.invoke(invocation);
return result;
}catch (RpcException e) {
throw e;
}finally {
}
}
private void setSessionAttachment(RpcInvocation invocation) {
if (null != UserManager.getCurrentUser()) {
String sessionId = HttpContext.getSessionId();
invocation.setAttachment("sessionId", HttpContext.getSessionId());
}
}
}
生产端Filter
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcException;
import com.uiot.platform.redis.session.RedisSessionManager;
import com.uiot.platform.user.IUserInfo;
import com.uiot.platform.user.UserManager;
import com.uiot.web.HttpContext;
@Activate(group = {Constants.PROVIDER})
public class ProviderTraceFilter implements Filter{
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 调用过程拦截
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
//获取session信息
initSession(invocation);
Result result = invoker.invoke(invocation);
return result;
}catch (RpcException e) {
throw e;
}finally {
}
}
private void initSession(Invocation invocation) {
String sessionId = invocation.getAttachment("sessionId");
if (null != sessionId) {
//加载session
String sessionKey ="spring:session:sessions:" + sessionId;
logger.info("sessionKey=>" + sessionKey);
if (RedisSessionManager.redisTemplate != null) {
Map<String, Object> data = new HashMap<String, Object>();
IUserInfo userInfo = (IUserInfo)RedisSessionManager.redisTemplate.opsForHash().get(sessionKey, "sessionAttr:" + UserManager.SESSION_KEY_CURRENT_USER);
if(null != userInfo) {
String account = (String)RedisSessionManager.redisTemplate.opsForHash().get(sessionKey, "sessionAttr:" + UserManager.SESSION_KEY_CURRENT_USER_LOGIN_ACCOUNT);
//用户
data.put(UserManager.SESSION_KEY_CURRENT_USER, userInfo);
//登录账户
data.put(UserManager.SESSION_KEY_CURRENT_USER_LOGIN_ACCOUNT, account);
//sessionId往下传递
data.put("sessionId", sessionId);
HttpContext.threadLocal.set(data);
logger.warn("sessionKey=>" + sessionKey + ",获取到用户信息=>" + userInfo.getUserName());
} else {
logger.warn("没有获取到用户信息sessionKey=>" + sessionKey);
}
}
}
}
}
Dubbo Filter配置
在classpath目录下创建目录"META-INF/dubbo",
然后在该目录下创建文件com.alibaba.dubbo.rpc.Filter
文件内容如下
consumerTrace=com.***.ConsumerTraceFilter
providerTrace=com.***.ProviderTraceFilter