微服务间如何进行用户信息传递?只能依靠接口调用时显式通过参数传递吗?能否在传递过程中无感知呢?
本章代码已分享至Gitee: https://gitee.com/lengcz/springcloudalibaba01.git
微服务由于跨服务了,我们如何在中间的某个服务知道当前的用户信息呢?
例如下面,用户下单的请求,用户的请求经过了 gateway>> order >> product,如果在order的时候,我们怎么样知道用户是谁?难道要请求接口传参时传递参数(用户身份信息)吗?
请求流程
我们知道一个微服务的请求有入口,也有出口(调用)其它微服务,请求进入时,可以通过filter进行拦截,
而调用其它请求时,我们可以将用户信息传递给被调用者(微服务),我们只需要通过AOP的思想,在入口处捕获用户信息,在出口时将用户信息传递出去,这样就保证了全链路都能获取到客户端的身份信息。当我们所有的微服务都在前后插入了接收和传出的操作,那么全链路就能够获取到用户信息了。
我们的用户信息传递是所有微服务的,因为所有微服务模块都需要进行用户信息传递,因为我们将实现定义在common模块
order,product,user均引用common
Feign的远程调用是Http的方式,所以我们只需要Filter 在入口取数据,在RequestInterceptor 出口时传递数据即可。
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>4.0.1version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
package com.lcz.userinfo;
public class UserInfoContext {
private static ThreadLocal<UserInfo> userInfo = new ThreadLocal<>();
public static UserInfo getUser() {
return (UserInfo) userInfo.get();
}
public static void setUser(UserInfo user) {
userInfo.set(user);
}
public static void remove(){
userInfo.remove();
}
}
package com.lcz.userinfo;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserInfo implements Serializable {
private Integer uid; //用户id
private String username;//用户名
}
package com.lcz.userinfo;
public class UserInfoConstant {
public static final String DUBBO_USER_KEY="DUBBO_USER_INFO";
public static final String Authorization="USER_INFO";
}
package com.lcz.userinfo;
import com.alibaba.fastjson.JSON;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@Slf4j
public class TransmitUserInfoFeighClientIntercepter implements RequestInterceptor {
public TransmitUserInfoFeighClientIntercepter() {
}
@Override
public void apply(RequestTemplate requestTemplate) {
//从应用上下文中取出user信息,放入Feign的请求头中
UserInfo user = UserInfoContext.getUser();
log.info("传递用户信息:"+JSON.toJSONString(user));
if (user != null) {
try {
String userJson = JSON.toJSONString(user);
requestTemplate.header(UserInfoConstant.Authorization,new String[]{URLDecoder.decode(userJson,"UTF-8")});
} catch (UnsupportedEncodingException e) {
log.error("用户信息设置错误",e);
}finally {
UserInfoContext.remove();
}
}
}
}
package com.lcz.userinfo;
import com.alibaba.fastjson.JSON;
import com.lcz.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@Slf4j
public class TransmitUserInfoFilter implements Filter {
public TransmitUserInfoFilter() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.initUserInfo((HttpServletRequest)request);
chain.doFilter(request,response);
}
private void initUserInfo(HttpServletRequest request){
String userJson = request.getHeader(UserInfoConstant.Authorization);
log.info("接收用户信息:"+userJson);
if (StringUtils.isNotBlank(userJson)) {
try {
userJson = URLDecoder.decode(userJson,"UTF-8");
UserInfo userInfo = (UserInfo) JSON.parseObject(userJson,UserInfo.class);
//将UserInfo放入上下文中
UserInfoContext.setUser(userInfo);
} catch (UnsupportedEncodingException e) {
log.error("init userInfo error",e);
}
}
}
}
package com.lcz.userinfo;
import org.springframework.context.annotation.Bean;
public class EnableUserInfoTransmitterAutoConfiguration {
public EnableUserInfoTransmitterAutoConfiguration() {
}
@Bean
public TransmitUserInfoFeighClientIntercepter transmitUserInfo2FeighHttpHeader(){
return new TransmitUserInfoFeighClientIntercepter();
}
@Bean
public TransmitUserInfoFilter transmitUserInfoFromHttpHeader(){
return new TransmitUserInfoFilter();
}
}
8 . 编写Enable注解,实现注解式注入
package com.lcz.userinfo;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({EnableUserInfoTransmitterAutoConfiguration.class})
public @interface EnableUserInfoTransmitter {
}
在order,product,user模块开启用户信息传递,使用@EnableUserInfoTransmitter
注意: common公共模块修改了代码,注意将common模块重新maven install发布到本地仓库
回到我们的流程图,下单流程有这些微服务参与了,我们启动这些服务。只要我们在product中能够获取到用户信息,就证明用户信息传递成功了。
发起下单请求,由于我们的身份信息通过header传递,我们需要先生成一个格式正确的身份,再写入到header进行传递
@Test
public void testUser() throws UnsupportedEncodingException {
UserInfo user=new UserInfo();
user.setUid(111);
user.setUsername("xiaowang");
String str= URLEncoder.encode(JSONObject.toJSONString(user),"UTF-8");
System.out.println(str);
}
生成的信息
%7B%22uid%22%3A111%2C%22username%22%3A%22xiaowang%22%7D
我们再product微服务的日志中,可以输出用户信息,表明了用户信息从api-gateway >> order >> product 这个流程调用中,用户信息传递成功了。
在第三节,我们的改造只适合微服务之间基于feign的调用,才能传递用户信息到其它微服务。而事实上,我们现在很多微服务之间的调用是基于dubbo的,微服务之间如何通过dubbo 传递用户信息呢?
我们的解决思路依然一样的,需要微服务的前后进行插板,这样就能保证微服务通过dubbo调用时,也能实现跨服务传递用户身份了。
通过官方的代码架构调用链路图,我们可以知道dubbo远程调用都需要经过Filter(全类名org.apache.dubbo.rpc.Filter),因此我们只需要在消费方和生产方两边的Filter进行附带数据传输,即可实现信息的隐式传递。
我们利用org.apache.dubbo.rpc.Filter来拦截信息,并通过其传递用户信息,而接收方可以通过org.apache.dubbo.rpc.Filter接收信息
基于前面的代码,我们继续改造。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-dubboartifactId>
dependency>
package com.lcz.userinfo;
import com.alibaba.dubbo.common.extension.Activate;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.rpc.*;
@Slf4j
@Activate(group = {UserInfoConstant.CONSUMER})
public class UserInfoConsumerFilter implements org.apache.dubbo.rpc.Filter {
@Override
public Result invoke(Invoker<?> invoker, org.apache.dubbo.rpc.Invocation invocation) throws RpcException {
// log.info("消费方:------");
try{
UserInfo userInfo = UserInfoContext.getUser();
if (null == userInfo) {
return invoker.invoke(invocation);
}
invocation.getObjectAttachments().put(UserInfoConstant.DUBBO_USER_KEY, userInfo);
return invoker.invoke(invocation);
}finally {
UserInfoContext.remove();
}
}
}
package com.it.userinfo;
import com.alibaba.dubbo.common.extension.Activate;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcException;
@Slf4j
@Activate(group = CommonConstants.PROVIDER)
public class UserInfoProviderFilter implements org.apache.dubbo.rpc.Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// log.info("生成方:------");
Object userInfo = invocation.getObjectAttachment(UserInfoConstant.DUBBO_USER_KEY);
if (null != userInfo) {
UserInfoContext.setUser((UserInfo) userInfo);
}
return invoker.invoke(invocation);
}
}
userInfoConsumerFilter=com.lcz.userinfo.UserInfoConsumerFilter
userInfoProviderFilter=com.lcz.userinfo.UserInfoProviderFilter
基于我们上面的改造,各微服务不管使用Feign调用还是Dubbo调用,都可以实现用户信息的跨服务传递,解决了无法获取用户身份的问题。让跨服务之间,可以直接使用UserInfoContext.getUser(),具有单体应用的信息获取能力
UserInfo userInfo = UserInfoContext.getUser(); //获取用户信息
同时,直接传递用户身份信息,基于这个用户信息的传递思想,我们可以实现链路跟踪(Sleuth),也可以在gateway网关封装好某些内容,传递一些request信息,比如客户端IP,用户的权限。
当然了,上面的demo直接传递用户信息是不具有防伪性,我们可能使用JWT相关的技术进行鉴权等处理,在穿越网关时,将其转换为明文可操作的UserInfo,这样就保证了传递的用户信息的安全可靠性。
如果打包common时,提示repackage failed: Unable to find main class
可以配置
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<mainClass>nonemainClass>
<classifier>executeclassifier>
configuration>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>