0、项目结构
0.1公共模块
里面包含一些基础的根据类,全局的参数定义,公共的数据结构等,可把service里的entity放到此处,所有模块共享,也在这里写入一些公共依赖,其他模块通过pom文件引入
<dependency>
<groupId>***.***</groupId>
<artifactId>rosemanor-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>=
0.2网关模块
做所有请求的转发,外部请求都打到网管模块,在此处做权限验证等工作
0.3服务模块
在此处实现具体业务
1、nacos服务注册和服务发现
nacos版本要和springboot和springcloud对应,官网说明 服务注册后,在控制台看不到问题
得点击才能看到,在这个地方卡了几个小时,误以为没有注册上
nacos配置管理
把nacos相关配置放到bootstrap下,其优先级比application高
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
group: ROSEMANOR
#加载配置,一定要配置config信息
config:
server-addr: 127.0.0.1:8848 # Nacos 作为配置中心地址
file-extension: yaml #指定yaml格式的配置 yml会报错,nacos识别yaml
# 加载配置才需要以下配置
group: ROSEMANOR
application:
name: rosemanor-store
server:
port: 4033
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.0.4</version>
<!--和springboot版本对应-->
</dependency>
2、网关配置
spring:
cloud:
gateway:
# 自定义参数,用于后续去掉公共前缀
api-prefix: /rosemanor-api/rosemanor
discovery:
locator:
enabled: true
routes:
- id: rosemanor-third-service
uri: lb://rosemanor-third-service
predicates:
# 匹配的格式
- Path=/rosemanor-api/rosemanor/wechat/**
filters:
# 转发的时候去掉前缀
- RewritePath=/rosemanor-api/rosemanor(?<segment>/?.*), $\{segment}
3、网关权限处理
3.1 拦截流程
定义拦截器,对请求进行拦截,将请求分为3类,一类内部请求,不做转发,一类公开请求,不做拦截,直接转发,一类带权限请求,进行权限判断
服务启动时,将服务下的所有接口信息写入redis,同时带上接口信息,对接口版本自增
网关判断前先比对url版本信息,版本信息没变,就用缓存的接口信息,避免每次都得从redis读取大量接口缓存,否则更新url信息
对url进行匹配,匹配后根据接口类型,对权限进行判断,做对应出来
3.2 拦截器代码
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private int version = 0;
private static final int innerPermission = -2;
private static final int skipPermission = -1;
@Value("${spring.cloud.gateway.api-prefix:/rosemanor-api/rosemanor}")
private String prefix;
// redis操作相关,根据自己的工具类来即可
@Autowired
private RedisUtil redisUtil;
private Map<String, Integer> pathMap = new HashMap<>();
@PostConstruct
public void init() {
updateUrl(-1);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
int newVersion = (int) redisUtil.get(GlobalConstant.URL_VERSION);
if (newVersion != version) {
updateUrl(newVersion);
}
} catch (Exception ignored) {
}
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
for (Map.Entry<String, Integer> e : pathMap.entrySet()) {
if (antPathMatcher.match(e.getKey(), path)) {
if (e.getValue() == innerPermission) {
System.out.println("reject");
return reject(exchange.getResponse());
} else if (e.getValue() == skipPermission) {
System.out.println("skip");
return chain.filter(exchange);
} else {
// 加上自己的逻辑
System.out.println("permission");
return chain.filter(exchange);
}
}
}
return reject(exchange.getResponse());
}
private Mono<Void> reject(ServerHttpResponse response) {
//拦截
response.setStatusCode(HttpStatus.FORBIDDEN);
JsonObject message = new JsonObject();
message.addProperty("success", false);
message.addProperty("code", 28004);
message.addProperty("data", "鉴权失败");
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
response.writeWith(Mono.just(buffer));
return response.setComplete();
}
//影响的是全局过滤器的执行顺序,值越小优先级越高
@Override
public int getOrder() {
return 0;
}
public synchronized void updateUrl(int newVersion) {
// 只有第一个进来的进行更新,后续的直接返回
if (newVersion == version)
return;
try {
version = Integer.parseInt(String.valueOf(redisUtil.get(GlobalConstant.URL_VERSION)));
} catch (Exception e) {
version = 0;
}
// GlobalConstant.URL_SET_NAME 自定义字符串标识,全局统一即可
Map<Object, Object> map = redisUtil.hGetAll(GlobalConstant.URL_SET_NAME);
Map<String, Integer> newMap = new HashMap<>();
for (Map.Entry<Object, Object> e : map.entrySet()) {
if (e.getValue().toString().equals(GlobalConstant.URL_INNER)) {
newMap.put(prefix + e.getKey().toString(), innerPermission);
} else if (e.getValue().toString().equals(GlobalConstant.URL_SKIP_AUTH)) {
newMap.put(prefix + e.getKey().toString(), skipPermission);
} else {
try {
newMap.put(prefix + e.getKey().toString(), Integer.parseInt(String.valueOf(e.getValue().toString())));
} catch (Exception exception) {
newMap.put(prefix + e.getKey().toString(), 0);
}
}
}
// 直接赋值新的map信息,之前的map弃用,防止多线程之间出现冲突
pathMap = newMap;
}
}
3.3 服务注册接口信息代码
springboot新版本使用了PathPattern代替了AntPathPattern,需要在配置文件内选择路径匹配的方式
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
@Component
public class InitRunner implements ApplicationRunner {
@Autowired
private WebApplicationContext webApplicationContext;
@Resource
private RedisUtil redisUtil;
@Override
public void run(ApplicationArguments args) {
// springboot3会有多个bean冲突,这里指定bean名称
RequestMappingHandlerMapping mapping = webApplicationContext.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
Annotation[] annotationArr;
boolean isInnerInterface, isPermission, isSkipAuth;
int permissionCode = 1;
Set<String> path;
for (Map.Entry<RequestMappingInfo, HandlerMethod> e : map.entrySet()) {
isInnerInterface = false;
isSkipAuth = false;
isPermission = false;
annotationArr = e.getValue().getMethod().getDeclaredAnnotations();
for (Annotation annotation : annotationArr) {
if (annotation.annotationType().getName().equals(InnerInterface.class.getName())) {
// 内部请求
isInnerInterface = true;
break;
} else if (annotation.annotationType().getName().equals(SkipAuthentication.class.getName())) {
// 不做权限验证的请求
isSkipAuth = true;
break;
} else if (annotation.annotationType().getName().equals(Permission.class.getName())) {
// 需要验证权限的请求
isPermission = true;
permissionCode = ((Permission) annotation).permissionCode();
break;
}
}
if (isInnerInterface) {
// 需选择ant_path_matcher,否则新版本的springboot获取到的为空
if (e.getKey().getPatternsCondition() != null) {
path = e.getKey().getPatternsCondition().getPatterns();
for (String tempPath : path)
redisUtil.hSet(GlobalConstant.URL_SET_NAME, tempPath, GlobalConstant.URL_INNER);
}
} else if (isSkipAuth) { // 不登录即可访问
if (e.getKey().getPatternsCondition() != null) {
path = e.getKey().getPatternsCondition().getPatterns();
for (String tempPath : path)
redisUtil.hSet(GlobalConstant.URL_SET_NAME, tempPath, GlobalConstant.URL_SKIP_AUTH);
}
} else if (isPermission) {
if (e.getKey().getPatternsCondition() != null) {
path = e.getKey().getPatternsCondition().getPatterns();
for (String tempPath : path)
redisUtil.hSet(GlobalConstant.URL_SET_NAME, tempPath, String.valueOf(permissionCode));
}
} else { // 普通用户可以访问的请求,和跳过权限认证的区别是得进行身份认证(必须先登录)
if (e.getKey().getPatternsCondition() != null) {
path = e.getKey().getPatternsCondition().getPatterns();
for (String tempPath : path)
redisUtil.hSet(GlobalConstant.URL_SET_NAME, tempPath, "0");
}
}
}
redisUtil.incr(GlobalConstant.URL_VERSION, 1);
}
}
接口示例
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SkipAuthentication {
int type() default 1;
}
4、请求参数接收
"@RequestMapping" -> "@RequestParam"
"@PostMapping" -> "@RequestBody"
url参数 "/get-user/{uid}" -> "@PathVariable('uid') int uid"