gateway:网关,我们都知道网关的作用就是对系统的所有请求,网关都会进行拦截,然后做一些操作(例如:设置每个请求的请求头httpHeader,身份认证等等)此时一般会使用到网关过滤器,创建一个过滤器去实现GlobalFilter接口
jwt:JSON-WEB-TOKEN,这里就不过多解释了,同学们可以自行搜索相关文章,它主要包含三个部分,更好的生成token的一种行业规范,主要作用就是令牌校验。
{
header:xxx,
payload:xxx,
singature:xxx
}
redis的主要作用就是为了存放用户信息,该用户信息主要包含以下几个字段
{
userCode:xxx,
language:xxx,主要是为了进行国际化语言配置比如ZH-CN 、EN
menuApu:["xxx","xxx",..."xxx"] 用户对应的菜单权限列表API
jwtToken:xxx 必须拥有这个字段,为了防止同一个用户在不同机器上登录进行操作
}
httpHeader的主要作用就是存放userCode,用于任何请求都可以获取到当前操作的用户名,比如当我要添加一个新增接口或者更新接口,一般会有两个字段,一个是creator,一个是modifier,那么这两个值就可以直接取请求头的userCode。
还有就是存放language,为了进行国际化语言切换。
所以现在我先给大家画一个图,待会写的代码也是按照这个逻辑,方便大家加深理解与记忆。
现在看一下代码逻辑:
我们创建一个gateway全局过滤器:AuthTokenGlobalFilter
实现GlobalFilter接口,重写如下方法:
public Monofilter(ServerWebExchange exchange, GatewayFilterChain chain)
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();//得到前端访问请求时的请求头
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(headers);
//先过滤掉登录请求,登录请求不需要进行拦截,直接放行
if (ObjectUtil.equals(request.getURI().getPath(), "/login")) {
//放行之前将前端发出请求时的请求头拿到,同时设置一下Accept-Language
httpHeaders.setAcceptLanguage(httpHeaders.getAcceptLanguage());
//放行
chain.filter(exchange.mutate().request(decorateRequest(request, httpHeaders)).build());
}
//其他请求
//判读1:请求的请求头中是否携带token
if (CollectionUtils.isEmpty(headers.get("Authorization"))) {
throw new ServiceException(Codes.NO_TOKEN_RECOGNIZED);//未识别到token
}
//判断2:token解析出来的数据是否能在redis中查询到,考虑到token有可能发生过期,或者是登出了,redis中的数据就会删除
String authorization = headers.get("Authorization").get(0);
//通过token 利用jwt反解析出数据 claims
JSONObject jsonObject = JwtUtils.parseJwtToken(authorization, "user", NetworkUtils.getIp(request));
if (ObjectUtil.isNull(jsonObject)) {
throw new ServiceException(Codes.AUTHORIZATION_ERROR);
}
//如果是登出请求/logout,需要在请求头中加上userCode,为了后续放行之后,拿到该userCode作为redis的key的一部分去删除登录时存到redis中的数据
if (ObjectUtil.equals(request.getURI().getPath(), "/logout")) {
httpHeaders.set("userCode", jsonObject.get("userCode").toString());
return chain.filter(exchange.mutate().request(decorateRequest(request, httpHeaders)).build());
}
//拿到userCode
String userCode = (String) jsonObject.get("userCode");
//去redis中获取数据 key=》 User:userCode 比如User:zkw
String key = USER_CACAHE_PREFIX + userCode;
String jsonString = stringRedisTemplate.opsForValue().get(key);
LoginUser loginUser = JSONObject.parseObject(jsonString, LoginUser.class);
if (ObjectUtils.isEmpty(loginUser)) {
throw new ServiceException(Codes.LOGIN_EXPIRED);
}
//走到这里
//1、发出的请求的请求头中携带了token
//2、携带的token有效,没有过期或者是登出
//那么此时还需要进行判断3,判断携带的token与redis中存储的token是否一致,因为有可能有这么一个情况
// A用户在机器172.16.254.1上登录一次,成功之后,就会在redis中存放A对应的token以及其他一些字段数据,
//那么此时在A发出另外一个请求之前,比如查询,此时A用户又在另外一台机器上172.16.254.2登录,成功之后,也会在redis中存放A在172.16.254.2对应的token以及其他一些字段数据,此时就会覆盖前面redis中存放A在172.16.254.1的token数据了,
// 因为生成的token是有根据ip地址的,所以当A用户在机器上172.16.254.1发起请求的时候,携带的token就与此时redis中的token是不对的了,所以对于这种情况我们就不允许存在。
if (!ObjectUtil.equals(authorization, loginUser.getJwtToken())) {
throw new ServiceException(Codes.LOGIN_EXPIRED);
}
//如果token都满足情况了,就代表确确实实身份无误了,那么就需要进行用户的权限列表判断了。
}
上面是认证逻辑,现在是授权逻辑了。
redis中有两个key,一个key就是刚刚关于认证token的,另一个就是授权的。AccessControl:permissions,这个key的主要作用就是看看前端发出的请求是否是系统里面已经配置的菜单权限API,防止随意伪造请求API。
接下来我们看看授权的代码:
//***********************************************授权*************************************************************
//异步获取统里面配置的存在的菜单api,在redis中有存放两个key,一个key就是刚刚前面关于token的,另外一个是关于菜单权限api的
//AccessControl:permissionsMenu 这个值的主要作用就是为了判断你请求的api是否此时系统里面有,如果没有的话,有两个原因:
// 1、你xjb自己随便别写一个请求 2、确确实实开发了这个接口,但是可能在系统菜单表里面忘记添加了
//User:userCode
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
return stringRedisTemplate.opsForValue().get("AccessControl:permissionsMenu");
});
String permissionsMenu = future.join();
//如果不为空,就代表此时系统里面已经配置了菜单权限api
if (permissionsMenu != null) {
String[] urls = permissionsMenu.split(",");
//当前url不存在系统配置的菜单权限api里面 直接放行
if (Arrays.stream(urls).noneMatch(part -> (part.trim()).equals(requestPath))) {
return chain.filter(exchange.mutate().request(decorateRequest(request, httpHeaders)).build());
}
}
//如果菜单为空,证明是第一次请求到,把系统里面配置的存在的api设置到该key上
else {
//通过openFeign远程调用获取系统配置的菜单权限API
List urls = permissionFeign.queryPermissionsMenu();
stringRedisTemplate.opsForValue().set("AccessControl:permissionsMenu", urls.toString());
//当前url不存在系统配置的菜单权限api里面 直接放行
if (urls.stream().noneMatch(part -> (part.trim()).equals(requestPath))) {
return chain.filter(exchange.mutate().request(decorateRequest(request, httpHeaders)).build());
}
}
//如果存在,如果判断当前用户是否拥有这个菜单权限api
boolean flag = sysMenuService.queryUserMenuButton(userCode).stream().anyMatch(arg -> ObjectUtil.equals(arg.getApi(), requestPath));
if (!flag) {
throw new ServiceException(Codes.USER_INSUFFICIENT_PERMISSIONS);
}
return chain.filter(exchange.mutate().request(decorateRequest(request, httpHeaders)).build());
完整代码:
注意:
登录接口是不需要进行拦截的,我们在登录接口的时候,如果登录成功,才会生成一个token(生成token以及代码中的反解析token在我上一篇文章的工具类有,同学们也可以去看一下)还有查询该用户对应的菜单权限API列表,然后会把相关信息存到redis中去。
总结:
最后:
如果大家觉得这篇文章对你们有所帮助的话,麻烦给个免费的赞赞,也祝各位码农在未来的IT道路上越走越远,谢谢!