草庐IT

GateWay——向其他服务传递参数数据(思路)

专注写bug 2023-04-22 原文

文章目录

前言

跳槽去了新公司,研究公司的系统架构,发现一个很有趣的思路:

GateWay 解析前端请求携带的token信息,并向下游微服务传递。

达到下游微服务不用重复解析token,就能获取当前登录账户的基本信息

其实原理很简单,但记录下实现方式。

GateWay 增加 filter

在gateway网关服务中,增加filter 过滤器,主要实现获取请求接口中携带的token信息解析token将解析数据继续存放至当前请求对象中

具体实现方式如下所示:

import com.alicp.jetcache.Cache;
import com.alicp.jetcache.anno.CreateCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Date;

@Slf4j
@Component
public class UASFilter implements GlobalFilter, Ordered {

	@CreateCache(name = "uas:user:login:")
	private Cache<String, String> tokenCache;

	/**
	 * 1.首先网关检查token是否有效,无效直接返回401,不调用签权服务
	 * 2.调用签权服务器看是否对该请求有权限,有权限进入下一个filter,没有权限返回401
	 *
	 * @param exchange
	 * @param chain
	 * @return
	 */
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		ServerHttpRequest request = exchange.getRequest();
		URI uri = request.getURI();
		// 判断是否属于白名单中
		if(white(uri.getPath())){
			return chain.filter(exchange);
		}

		log.debug("**********UASFilter start: " + new Date());
		try {
//			ServerHttpRequest request = exchange.getRequest();
			// 获取原始token信息
			String authentication = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
			String method = request.getMethodValue();
			String url = request.getPath().value();
			log.debug("url:{},method:{},headers:{}", url, method, request.getHeaders());
			// 根据环境判断是否校验
			if (permissionService.ignoreAuthenticationByModule() || isSwaggerUrl(exchange.getRequest().getPath().value())) {
				return chain.filter(exchange);
			}

			//不需要网关签权的url
			if (permissionService.ignoreAuthentication(url)) {
				return chain.filter(exchange);
			}

			//登出判断
			if (isLogout(authentication)) {
				log.error("已经登出或者在其他设备登录,请重新登录!");
				return unauthorized(exchange, "已经登出或者在其他设备登录,请重新登录!");
			}
			
			// 核心!!!!!
			//调用签权服务看用户是否有权限,若有权限进入下一个filter
			if (permissionService.hasPermission(authentication, url, method)) {
				ServerHttpRequest.Builder builder = request.mutate();
				// 原始jwt token
				builder.header(GatewayConstans.X_CLIENT_TOKEN, authentication);
				//将jwt token中的用户信息传给服务
				builder.header(GatewayConstans.X_CLIENT_TOKEN_USER, permissionService.getUserTokenBase64(authentication));
				return chain.filter(exchange.mutate().request(builder.build()).build());
			}
			return unauthorized(exchange);
		} finally {
			log.debug("**********UASFilter end: " + new Date());
		}
	}

	/**
	 * @param token
	 * @return boolean
	 * @throws
	 * @description 登出判断
	 */
	private boolean isLogout(String token) {
		if (StringUtil.isNotEmpty(token) && token.startsWith("Bearer")) {
			token = token.replace("Bearer", "").trim();
			String loginToken = tokenCache.get(MD5Util.standardMD5(token));
			return StringUtil.isBlank(loginToken);
		}
		return true;
	}
	
	/**
	 * 网关拒绝,返回401
	 *
	 * @param msg
	 */
	protected Mono<Void> unauthorized(ServerWebExchange serverWebExchange, String... msg) {
		DataBuffer buffer = serverWebExchange.getResponse()
				.bufferFactory().wrap(JSON.toJSONBytes(
						CommonResult.error(HttpStatus.UNAUTHORIZED.value(), "未授权!" + (msg.length > 0 ? msg[0] : ""))));
		serverWebExchange.getResponse().getHeaders()
				.add("Content-Type", "json/plain;charset=UTF-8");
		serverWebExchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
		log.error("未授权!", msg);
		return serverWebExchange.getResponse().writeWith(Flux.just(buffer));
	}

	@Override
	public int getOrder() {
		// 调用优先级, 数字小 优先级高
		return 20;
	}
}

其中最为核心的部分在于:

if (permissionService.hasPermission(authentication, url, method)) {
	// 获取当前的请求对象信息
	ServerHttpRequest.Builder builder = request.mutate();
	// 原始jwt token
	builder.header(GatewayConstans.X_CLIENT_TOKEN, authentication);
	// 向header中设置新的key,存储解析好的token对应基本信息
	builder.header(GatewayConstans.X_CLIENT_TOKEN_USER, permissionService.getUserTokenBase64(authentication));
	// exchange.mutate().request(builder.build()).build() 将其继续转化为请求对象
	// 向下游传递
	return chain.filter(exchange.mutate().request(builder.build()).build());
}

这样,只要请求携带了token,并能够成功解析,就会在请求对象的header数据部分,打上x-client-token-user解析后的数据。

其他服务解析

当gateway网关验证完毕后,合法的请求将会继续向内执行,当进入到对应的模块时,此时只需要从请求中获取x-client-token-user对应的登录账户解析数据,并将其保存至ThreadLocal中即可。

一样可以使用filter 过滤器,使用拦截器也可以!

核心代码如下所示:

import org.springframework.web.filter.OncePerRequestFilter;

@Order(-1000)
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
	
	// 重写  doFilterInternal  方法即可
	protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
		// 获取请求中header的 x-client-token-user信息
        String userToken = httpServletRequest.getHeader("x-client-token-user");
        if (userToken != null) {
            String json = EncryptUtil.decodeUTF8StringBase64(userToken);
            JSONObject jsonObject = JSON.parseObject(json);
            HashMap profile = (HashMap)JSON.parseObject(jsonObject.getString("user_name"), HashMap.class);
            // 这里是 ThreadLocal 的封装,将获取到的数据存放其中
            UserContextHolder.getInstance().setContext(profile);
        } else {
            UserContextHolder.getInstance().setContext(this.getParamMap());
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

在需要使用的地方,采取下列方式获取即可:

public static Profile getProfile() {
    Map<String, String> data = UserContextHolder.getInstance().getContext();
    if (null == data) {
        throw new RuntimeException("当前请求没有通过网关监控,无法加载登录用户信息!");
    } else {
        Profile profile = (Profile)BeanUtil.toBean(data, Profile.class);
        logger.debug("当前登陆用户信息:{}", profile.toString());
        return profile;
    }
}

几个工具类

UserContextHolder .java

import java.util.Map;

public class UserContextHolder {
    private ThreadLocal<Map<String, String>> threadLocal;

    private UserContextHolder() {
        this.threadLocal = new ThreadLocal();
    }

    public static UserContextHolder getInstance() {
        return UserContextHolder.SingletonHolder.sInstance;
    }

    public void setContext(Map<String, String> map) {
        this.threadLocal.set(map);
    }

    public Map<String, String> getContext() {
        return (Map)this.threadLocal.get();
    }

    public void clear() {
        this.threadLocal.remove();
    }

    private static class SingletonHolder {
        private static final UserContextHolder sInstance = new UserContextHolder();

        private SingletonHolder() {
        }
    }
}

有关GateWay——向其他服务传递参数数据(思路)的更多相关文章

  1. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  2. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  3. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  4. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  5. ruby - RSpec - 使用测试替身作为 block 参数 - 2

    我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere

  6. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

    我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

  7. ruby - 检查方法参数的类型 - 2

    我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)

  8. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

    最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

  9. ruby-on-rails - s3_direct_upload 在生产服务器中不工作 - 2

    在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo

  10. ruby-on-rails - 在默认方法参数中使用 .reverse_merge 或 .merge - 2

    两者都可以defsetup(options={})options.reverse_merge:size=>25,:velocity=>10end和defsetup(options={}){:size=>25,:velocity=>10}.merge(options)end在方法的参数中分配默认值。问题是:哪个更好?您更愿意使用哪一个?在性能、代码可读性或其他方面有什么不同吗?编辑:我无意中添加了bang(!)...并不是要询问nobang方法与bang方法之间的区别 最佳答案 我倾向于使用reverse_merge方法:option

随机推荐