文章目录
跳槽去了新公司,研究公司的系统架构,发现一个很有趣的思路:
GateWay 解析前端请求携带的token信息,并向下游微服务传递。
达到下游微服务不用重复解析token,就能获取当前登录账户的基本信息。
其实原理很简单,但记录下实现方式。
在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() {
}
}
}
我正在尝试使用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请求没有正确的命名空间。任何人都可以建议我
我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..
我主要使用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
exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby中使用两个参数异步运行exe吗?我已经尝试过ruby命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何rubygems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除
我有一些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
我正在为一个项目制作一个简单的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"
我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)
最近,当我启动我的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
在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
两者都可以defsetup(options={})options.reverse_merge:size=>25,:velocity=>10end和defsetup(options={}){:size=>25,:velocity=>10}.merge(options)end在方法的参数中分配默认值。问题是:哪个更好?您更愿意使用哪一个?在性能、代码可读性或其他方面有什么不同吗?编辑:我无意中添加了bang(!)...并不是要询问nobang方法与bang方法之间的区别 最佳答案 我倾向于使用reverse_merge方法:option