在前几篇中已经将微信,网关与鉴权微服务全部打通,这次我们进行用户上下文打通,与微服务之间的调用。
用户上下文打通:
首先先思考一下,当我们成功登录的时候,网关会获取到当前用户相关的信息,比如说用户名等等,现在我们需要在catalogue服务中获取到当前用户的信息,我们应该怎么做呢,以前的思路就是,在catalogue服务中再进行一次获取不就好了。理论上可以,但是当微服务多了,难道每个微服务都需要进行获取吗,那么网关的意义也就不大了。
所以我们的目的是在网关层,将拦截的信息处理好,分发给每个微服务,做统一的处理,那么就引出了我们今天实现的第一个目的,打通用户的上下文。
新增包与实现类:
我们在gateway服务中新增filter包,新增一个类为LoginFilter
LoginFilter:
代码如下:
package com.yizhiliulianta.gateway.filter;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.alibaba.nacos.api.utils.StringUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 登录拦截器
*/
@Component
@Slf4j
public class LoginFilter implements GlobalFilter {
@Override
@SneakyThrows
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
log.info("LoginFilter.filter.url:{}", url);
if (url.equals("/user/doLogin")) {
return chain.filter(exchange);
}
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
//根据token获取对应的用户id
String loginId = (String) tokenInfo.getLoginId();
if (StringUtils.isEmpty(loginId)){
throw new Exception("未获取到用户信息");
}
mutate.header("loginId",loginId);
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
}
这是一个网关的登录过滤器,该类实现了全局过滤,整体的目的就是将当前访问的用户的唯一id,放入到转发的请求头中,在后续的服务里,如果需要,直接在请求头中获取即可,下面我来解释一下。
首先实现了GlobalFilter全局过滤器接口,重写filter方法。
ServerWebExchange封装了HTTP请求和响应的上下文,所以我们可以通过getRequest()获取当前的请求,然后我们将该次请求mutate()一个可更改的副本,下面就是判断是否为登录,因为登录是没有用户信息的,我们直接放行。
下面首先获取本次登录的tokeninfo信息,然后获取里面的用户id,然后将其放入到请求副本的请求头中,最后放行。放行的内容:首先构建一个ServerWebExchange的副本,这里封装的请求为,我们刚刚写入用户id的请求request,.build()就是将副本构建。
通过这段代码我们可以完成,通过网关将请求重写,放入用户id,方便转发的微服务进行获取。
上下文对象的封装:
首先我们来改变一下依赖,对不起哈哈哈我的原因,欠考虑了,依赖关系还是有点一点瑕疵,我们需要将controller层的spring-boot-starter-web依赖,转移到common层,然后将以前的日志spring-boot-starter-log4j2依赖删除,否则会冲突。后面可能还会有问题,到时候我们再慢慢整理,我们将公共的依赖全部抽到common层中,这个以后再说。
我们来到catalogue服务中,在commen层中新建以下包和类:
我们需要封装一个登录的上下文对象,代码如下:
package com.yizhiliulianta.movie.common.context;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* 登录上下文对象
*/
public class LoginContextHolder {
private static final InheritableThreadLocal<Map<String,Object>> THREAD_LOCAL = new InheritableThreadLocal<>();
public static void set(String key,Object val){
Map<String, Object> map = getThreadLocalMap();
map.put(key,val);
}
public static String getLoginId(){
return (String) getThreadLocalMap().get("loginId");
}
public static Object get(String key){
Map<String, Object> threadLocalMap = getThreadLocalMap();
return threadLocalMap.get(key);
}
public static void remove(){
THREAD_LOCAL.remove();
}
public static Map<String,Object> getThreadLocalMap(){
Map<String, Object> map = THREAD_LOCAL.get();
if (Objects.isNull(map)){
map = new ConcurrentHashMap<>();
THREAD_LOCAL.set(map);
}
return map;
}
}
在看这段代码之前,需要大家知道ThreadLocal是什么。
我对ThreadLocal的理解是当前线程的局部变量,其他线程无法访问,InheritableThreadLocal是对ThreadLocal的扩展,允许子线程访问父线程的变量,大家感兴趣可以自行查阅,因为后面我们可能使用多线程,所以这里就提前使用了,目前用ThreadLocal也是可以的。
首先我们new一个InheritableThreadLocal,规定其存储的内容为map集合,方便我们后续通过对应键来获取值。
先来看getThreadLocalMap(),这是实现ThreadLocal里变量唯一不变的关键,首先我们通过ThreadLocal的get方法获取该变量存储的内容,如果返回为null证明为空,证明该变量还没有内容,这时候我们再去new一个map集合,并将这个map集合放入到ThreadLocal里。
这里的ConcurrentHashMap也是map,但是它是线程安全的,看作map表就好
剩下的方法就是对map集合的获取和放入了,大家应该都可以看懂了,我就不去详细说了,其中的getLoginId就是在map集合中,通过loginid来获取用户id的方法封装。remove就是将ThreadLocal进行删除,确保在每次任务处理完之后,清理线程上下文,防止数据污染。
拦截器:
下面在common层中新增以下包和类:
我们需要在catalogue服务中实现一个登录拦截器,代码如下:
package com.yizhiliulianta.movie.common.interceptor;
import com.yizhiliulianta.movie.common.context.LoginContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String loginId = request.getHeader("loginId");
if (StringUtils.isNotBlank(loginId)) {
LoginContextHolder.set("loginId", loginId);
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
LoginContextHolder.remove();
}
}
首先我们实现HandlerInterceptor接口,去拦截访问该服务的请求,因为我们在网关中已经将当前用户的id放入到了请求头中,所以我们在进入该服务之前,我们先获取该请求头的信息,从中获取loginid,然后将其放入到我们封装的上下文对象中,然后放行。
工具类:
我们再封装一个工具类,方便我们获取当前用户的id,结构和代码如下:
package com.yizhiliulianta.movie.common.util;
import com.yizhiliulianta.movie.common.context.LoginContextHolder;
/**
* 用户登录util
*/
public class LoginUtil {
public static String getLoginId(){
return LoginContextHolder.getLoginId();
}
}
mvc拦截器配置:
我们已经将拦截器等准备完毕,最后只需要将拦截器配置到mvc中就可以了,代码和结构如下:
package com.yizhiliulianta.movie.common.config;
import com.yizhiliulianta.movie.common.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* mvc全局处理
*/
@Configuration
public class GlobalConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**");
}
}
重写addInterceptors方法,调用拦截器链,将我们的拦截器添加到里面,拦截所有路径
测试:
下面我们来测试一下,我们能不能在catalogue服务中获取到当前登录的用户id,我们在domain层新增一条日志,如下:
下面我们使用apipost来进行测试:
还记得我们的流程吗,先微信获取验证码,然后登录,获取token,然后将token添加到请求头,去进行服务访问
通过网关结课访问catalogue服务,查询电影信息
成功了!我们通过日志可以看到,我们成功获取了当前登录的用户id
Feign的使用:
前言:
首先将catalogue服务的common层的东西,也就是用户上下文打通的包与类,也粘贴到auth服务里,偷懒了,我不再去展示了
在使用之前我先更改一下表结构,给用户表增加一列,为用户的用户名,username是用户id,作为唯一标识,nickname为用户名称
再添加两个方法,一个是修改用户的用户名,一个是查询用户信息
代码我不去细说了,简单的增删改查,大家知道实现了什么就好,看下aippost的效果:
下面我们开始改造我们现在的服务
我们明确一下我们的目标,我们需要在catalogue服务中,获取当前用户的详细信息,也就是说,我们需要在catalogue服务中调取auth服务的方法,这是微服务之间的调用,那么引入我们的Feign
一,依赖:
我们在api层引入需要的依赖,如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>3.0.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>
然后在auth服务的common层中将api引入:
<dependency>
<groupId>com.yizhiliulianta</groupId>
<artifactId>movie-auth-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
二,重构结构:
我们将对应的枚举,和AuthUserDTO放入到api层中,结构如下
然后我们将controller层的导包修改一下,不然会报错,修改成api层的实体类。
三:提供者:
接着我们在api包内添加一个接口,代码如下:
package com.yizhiliulianta.auth.api;
import com.yizhiliulianta.auth.entity.AuthUserDTO;
import com.yizhiliulianta.auth.entity.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient("movie-auth")
public interface UserFeignService {
@RequestMapping("/user/getUserInfo")
Result<AuthUserDTO> getUserInfo(@RequestBody AuthUserDTO authUserDTO);
}
在使用feign之前我们需要知道什么rpc,rpc是远程过程调用。
主要作用是,实现调用远程方法就像调用本地方法一样的体验,所以我认为,feign是一个rpc框架。而微服务之间的调用,实际上就是服务与服务之间,通过网络链接,比如http来请求,获取其接口的内容,所以feign或rpc就是简化了这个过程。
下面来看一下feign是如何使用的,首先我们需要构建一个feignapi接口,该接口下的方法是暴露给其他服务使用的,所以auth是提供者。那么在feign中,提供者提供暴露的接口,消费者就是调用其接口的服务。
我们在api接口中为暴露的接口使用requestmapping进行映射,外部调用该方法,实际上就是向映射的地址发起了一次请求。
最后我们在接口上打上@FeignClient注解,里面为auth服务注册到nacos里的名称。
四,消费者:
下面我们在catalogue服务中的common层里,新建一个rpc的包,和调用auth服务的实现类,在实体类包中添加一个userInfo的实体类,结构如下:
首先我们进行一层防腐,比如说我们想获取的只是用户的名称,不需要用户的id等信息,我们在实体包中构建我们想要的用户类,代码如下:
package com.yizhiliulianta.movie.common.entity;
import lombok.Data;
@Data
public class UserInfo {
private String nickName;
}
下面我们开始编写userRpc,代码如下:
package com.yizhiliulianta.movie.common.rpc;
import com.yizhiliulianta.auth.api.UserFeignService;
import com.yizhiliulianta.auth.entity.AuthUserDTO;
import com.yizhiliulianta.auth.entity.Result;
import com.yizhiliulianta.movie.common.entity.UserInfo;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class UserRpc {
@Resource
private UserFeignService userFeignService;
public UserInfo getUserInfo(String userName) {
AuthUserDTO authUserDTO = new AuthUserDTO();
authUserDTO.setUserName(userName);
Result<AuthUserDTO> result = userFeignService.getUserInfo(authUserDTO);
UserInfo userInfo = new UserInfo();
if (!result.getSuccess()) {
return userInfo;
}
AuthUserDTO data = result.getData();
userInfo.setNickName(data.getNickName());
return userInfo;
}
}
首先我们将UserFeignService注入,然后新建一个方法来获取用户信息,实际上就是根据用户的id去封装成一个auth服务需要的userdto,然后发起请求,会返回给我们一个用户信息结果,我们再对结果进行一次封装,将我们需要的userinfo返回。
最后我们需要在启动类上加上注解
@EnableFeignClients(basePackages = "com.yizhiliulianta")
通过该注解,启用 Feign 客户端的功能。basePackages
属性用于指定要扫描的包,Spring 会在这些包中查找使用了 @FeignClient
注解的接口
五:测试:
下面我们还是在catalogue服务的domain层中进行测试,代码如下:
package com.yizhiliulianta.movie.domain.service.impl;
import com.alibaba.fastjson.JSON;
import com.yizhiliulianta.movie.common.entity.UserInfo;
import com.yizhiliulianta.movie.common.rpc.UserRpc;
import com.yizhiliulianta.movie.common.util.LoginUtil;
import com.yizhiliulianta.movie.domain.bo.MovieBO;
import com.yizhiliulianta.movie.domain.convert.MovieBOConverter;
import com.yizhiliulianta.movie.domain.service.MovieDomainServcie;
import com.yizhiliulianta.movie.infra.entity.TMovie;
import com.yizhiliulianta.movie.infra.service.TMovieService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class MovieDomainServiceImpl implements MovieDomainServcie{
@Resource
private TMovieService movieService;
@Resource
private UserRpc userRpc;
@Override
public MovieBO selectMovie(MovieBO movieBO) {
TMovie tmovie = MovieBOConverter.INSTANCE.convertBoToMovie(movieBO);
TMovie movie = movieService.queryById(tmovie.getId());
String loginId = LoginUtil.getLoginId();
UserInfo userInfo = userRpc.getUserInfo(loginId);
log.info("用户为:{}",userInfo.getNickName());
MovieBO bo = MovieBOConverter.INSTANCE.convertToMovieBO(movie);
if (log.isInfoEnabled()) {
log.info("MovieController.selectMovie.bo:{}",
JSON.toJSONString(bo));
}
return bo;
}
}
没什么变化,就是将UserRpc注入,然后调用其方法,通过satoken框架获取登录的id,然后传入,获取其用户的名称
下面我们启动所有服务,打开apipost,进行测试,查看是否能成功调用:
我们去后台看一下日志:
调用成功!到这里我们的微服务的调用就结束了
微服务的使用到这里就差不多了,后面应该是定时任务,消息队列,和多线程的一些使用
谢谢大家的观看!
地址:
gitee:movieCloud: 微服务的练习
movieCloud: 微服务的练习 (gitee.com)