前言
跳槽去了新公司,研究公司的系统架构,发现一个很有趣的思路:
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() {
}
}
}