物流项目第三期(统一网关、工厂模式运用)
前两期:
物流项目第一期(登录业务)-CSDN博客
物流项目第二期(用户端登录与双token三验证)-CSDN博客
为什么要有网关?
通过前面的课程我们已经完成了四个端的登录,但是我们并没有对登录后的请求中的token做校验,假设要做的话,是不是四个端每个端都需要实现?这样类似的代码就会重复,显然这并不是一个好的设计。
所以,通过Spring Cloud Gateway就可以接管所有来自客户端的请求,在此处做校验是最为合适的选择。
实际上,在网关中除了需要对token是否有效进行校验外,还需要对用户的是否有权限进行校验,比如说:司机不能登录到快递员端,快递员不能登录到司机端等。
自定义过滤器配置
spring:gateway: routes:- id: sl-express-ms-web-manager #路由标识,需要唯一uri: lb://sl-express-ms-web-manager #最终请求转发的微服务,指定的是微服务名,并开启负载均衡predicates: #断言- Path=/manager/**filters: #配置过滤器- ManagerToken #自定义局部过滤器- StripPrefix=1 #去掉第一个路径,例如:/manager/user/login -转发到下游微服务-> /user/login- AddRequestHeader=X-Request-From, sl-express-gateway #转发到下游微服务携带的请求头,增强安全性
自定义过滤器
在过滤器中主要实现以下几个功能:
- 白名单放行
- 请求头中的token是否有效
- 权限是否匹配(校验角色)
- 向下游微服务传递解析token的数据以及token值
/*** 管理端token校验的过滤器*/
@Component
public class ManagerTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {@Resourceprivate MyConfig myConfig;@Resourceprivate TokenCheckService tokenCheckService;@Value("${role.manager}")private List<Long> managerRoleIds; //获取配置文件中的管理员角色id@Overridepublic GatewayFilter apply(Object config) {return (exchange, chain) -> {//1. 校验请求路径,如果是白名单,直接放行String path = exchange.getRequest().getPath().toString();if (StrUtil.startWithAny(path, this.myConfig.getNoAuthPaths())) {//直接放行return chain.filter(exchange);}//2. 获取请求头中的token,进行校验,如果为空或校验失效,响应401String token = exchange.getRequest().getHeaders().getFirst(Constants.GATEWAY.AUTHORIZATION);if (StrUtil.isEmpty(token)) {//设置响应状态为401exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//拦截请求return exchange.getResponse().setComplete();}//校验tokenAuthUserInfoDTO authUserInfoDTO = null;try {authUserInfoDTO = this.tokenCheckService.parserToken(token);} catch (Exception e) {//token不可用,不做处理}if (ObjectUtil.isEmpty(authUserInfoDTO)) {//token不可用,设置响应状态为401exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//拦截请求return exchange.getResponse().setComplete();}//3. 校验权限,如果是非管理员不能登录AuthTemplate authTemplate = AuthTemplateFactory.get(token);//3.1 获取用户拥有的角色id列表List<Long> roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfoDTO.getUserId()).getData();//3.2 取交集,判断用户拥有的角色是否与预定的角色列表是否有交集Collection<Long> intersection = CollUtil.intersection(roleIds, this.managerRoleIds);if (CollUtil.isEmpty(intersection)) {//无交集,说明没有权限,设置响应状态码为400exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);return exchange.getResponse().setComplete();}//4. 校验通过,向下游传递用户信息和tokenexchange.getRequest().mutate().header(Constants.GATEWAY.USERINFO, JSONUtil.toJsonStr(authUserInfoDTO));exchange.getRequest().mutate().header(Constants.GATEWAY.TOKEN, token);//4.1 校验通过放行return chain.filter(exchange);};}}
代码优化
前面虽然已经实现了管理端的校验工作,同学们可以思考这样一个问题,如果实现司机端的校验,该怎么做呢?是不是意识到问题了?代码逻辑是不是很像?怎么办?没错,抽取共用代码!
优化一
将校验逻辑抽取到独立的类中,这样在每个端的GatewayFilterFactory
中就可以共用了。
package com.sl.gateway.filter;import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.itheima.auth.factory.AuthTemplateFactory;
import com.itheima.auth.sdk.AuthTemplate;
import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.itheima.auth.sdk.service.TokenCheckService;
import com.sl.gateway.config.MyConfig;
import com.sl.transport.common.constant.Constants;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;@Component
public class TokenGatewayFilter implements GatewayFilter {@Resourceprivate MyConfig myConfig;@Resourceprivate TokenCheckService tokenCheckService;@Value("${role.manager}")private List<Long> managerRoleIds; //获取配置文件中的管理员角色id@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//1. 校验请求路径,如果是白名单,直接放行String path = exchange.getRequest().getPath().toString();if (StrUtil.startWithAny(path, this.myConfig.getNoAuthPaths())) {//直接放行return chain.filter(exchange);}//2. 获取请求头中的token,进行校验,如果为空或校验失效,响应401String token = exchange.getRequest().getHeaders().getFirst(Constants.GATEWAY.AUTHORIZATION);if (StrUtil.isEmpty(token)) {//设置响应状态为401exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//拦截请求return exchange.getResponse().setComplete();}//校验tokenAuthUserInfoDTO authUserInfoDTO = null;try {authUserInfoDTO = this.tokenCheckService.parserToken(token);} catch (Exception e) {//token不可用,不做处理}if (ObjectUtil.isEmpty(authUserInfoDTO)) {//token不可用,设置响应状态为401exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//拦截请求return exchange.getResponse().setComplete();}//3. 校验权限,如果是非管理员不能登录AuthTemplate authTemplate = AuthTemplateFactory.get(token);//3.1 获取用户拥有的角色id列表List<Long> roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfoDTO.getUserId()).getData();//3.2 取交集,判断用户拥有的角色是否与预定的角色列表是否有交集Collection<Long> intersection = CollUtil.intersection(roleIds, this.managerRoleIds);if (CollUtil.isEmpty(intersection)) {//无交集,说明没有权限,设置响应状态码为400exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);return exchange.getResponse().setComplete();}//4. 校验通过,向下游传递用户信息和tokenexchange.getRequest().mutate().header(Constants.GATEWAY.USERINFO, JSONUtil.toJsonStr(authUserInfoDTO));exchange.getRequest().mutate().header(Constants.GATEWAY.TOKEN, token);//4.1 校验通过放行return chain.filter(exchange);}
}
使用
package com.sl.gateway.filter;import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;import javax.annotation.Resource;/*** 管理端token校验的过滤器*/
@Component
public class ManagerTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {@Resourceprivate TokenGatewayFilter tokenGatewayFilter;@Overridepublic GatewayFilter apply(Object config) {return this.tokenGatewayFilter;}}
优化二
通过前面的优化一已经将过滤器抽取到一个独立的类中,这样就可以在多个过滤器工厂中通用了,但也是存在一些问题的,例如:
- 不同的终端校验的角色id是不同的
- 用户端是不需要校验角色的
- 用户端校验token的逻辑与其他三端是不一样的
- 用户端请求头中的token参数名与其他三端也不一样
基于这些问题,就能够意识到,单纯的抽取代码是不够的,需要进一步的优化。想一想,该怎么优化呢?
优化的思路就是,在
TokenGatewayFilter
中保留四端通用的逻辑,不同的逻辑抽取到接口中,由四端具体的实现。
package com.sl.gateway.filter;import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.sl.transport.common.constant.Constants;/*** 鉴权业务的回调,具体逻辑由 GatewayFilterFactory 具体完成*/
public interface AuthFilter {/*** 校验token** @param token 请求中的token* @return token中携带的数据*/AuthUserInfoDTO check(String token);/*** 鉴权** @param token 请求中的token* @param authUserInfo token中携带的数据* @param path 当前请求的路径* @return 是否通过*/Boolean auth(String token, AuthUserInfoDTO authUserInfo, String path);/*** 请求中携带token的名称** @return 头名称*/default String tokenHeaderName() {return Constants.GATEWAY.AUTHORIZATION;}}
package com.sl.gateway.filter;import cn.hutool.core.collection.CollUtil;
import com.itheima.auth.factory.AuthTemplateFactory;
import com.itheima.auth.sdk.AuthTemplate;
import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.itheima.auth.sdk.service.TokenCheckService;
import com.sl.gateway.config.MyConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;/*** 管理端token校验的过滤器*/
@Component
public class ManagerTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> implements AuthFilter {@Resourceprivate MyConfig myConfig;@Resourceprivate TokenCheckService tokenCheckService;@Value("${role.manager}")private List<Long> managerRoleIds; //获取配置文件中的管理员角色id@Overridepublic GatewayFilter apply(Object config) {//由于要传递当前对象,所以不能采用spring注入的方式,这里采用手动构造对象的方式传入return new TokenGatewayFilter(this.myConfig, this);}@Overridepublic AuthUserInfoDTO check(String token) {try {return this.tokenCheckService.parserToken(token);} catch (Exception e) {//token不可用,不做处理}return null;}@Overridepublic Boolean auth(String token, AuthUserInfoDTO authUserInfo, String path) {//校验权限,如果是非管理员不能登录AuthTemplate authTemplate = AuthTemplateFactory.get(token);//获取用户拥有的角色id列表List<Long> roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfo.getUserId()).getData();//取交集,判断用户拥有的角色是否与预定的角色列表是否有交集Collection<Long> intersection = CollUtil.intersection(roleIds, this.managerRoleIds);return CollUtil.isNotEmpty(intersection);}
}
package com.sl.gateway.filter;import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.sl.gateway.config.MyConfig;
import com.sl.transport.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;@Slf4j
public class TokenGatewayFilter implements GatewayFilter {private AuthFilter authFilter;private MyConfig myConfig;public TokenGatewayFilter(MyConfig myConfig, AuthFilter authFilter) {this.myConfig = myConfig;this.authFilter = authFilter;}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//1. 校验请求路径,如果是白名单,直接放行String path = exchange.getRequest().getPath().toString();if (StrUtil.startWithAny(path, this.myConfig.getNoAuthPaths())) {//直接放行return chain.filter(exchange);}//2. 获取请求头中的token,进行校验,如果为空或校验失效,响应401String token = exchange.getRequest().getHeaders().getFirst(this.authFilter.tokenHeaderName());if (StrUtil.isEmpty(token)) {//设置响应状态为401exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//拦截请求return exchange.getResponse().setComplete();}//校验tokenAuthUserInfoDTO authUserInfoDTO = null;try {authUserInfoDTO = this.authFilter.check(token);} catch (Exception e) {//token不可用,不做处理}if (ObjectUtil.isEmpty(authUserInfoDTO)) {//token不可用,设置响应状态为401exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//拦截请求return exchange.getResponse().setComplete();}//3. 校验权限Boolean auth = false;try {auth = this.authFilter.auth(token, authUserInfoDTO, path);} catch (Exception e) {log.error("鉴权失败, token = {}", token, e);}if (!auth) {//没有权限,设置响应状态码为400exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);return exchange.getResponse().setComplete();}//4. 校验通过,向下游传递用户信息和tokenexchange.getRequest().mutate().header(Constants.GATEWAY.USERINFO, JSONUtil.toJsonStr(authUserInfoDTO));exchange.getRequest().mutate().header(Constants.GATEWAY.TOKEN, token);//4.1 校验通过放行return chain.filter(exchange);}
}