一. 中小项目特点和系统设计要点
中小软件项目有以下特点:
1. 中小项目系统通常部署在客户机房或者客户私有云池,少量部署在公有云
2. 系统访问客户为用户的系统管理员(管理配置系统),用户管理层(查看数据统计分析信息),用户员工(通常通过微信公众号和小程序访问特定模块),并发访问量不大,但业务逻辑可能比较复杂
3. 系统集成较多,需要集成大量外部系统和用户采购的硬件。软件后期修改项目较多
从软件架构四个维度分析:
1. 可用性:对数据保存性要求较高,需要做统计分析。对系统可用性要求普通,可承受短期服务不可用。故数据节点考虑备份,应用节点可以采用单点。
2. 可修改性:要求高,需要不停集成新第三方系统和新功能
3. 安全性:普通安全性要求,通过客户云池网闸防火墙控制外部访问,内部服务器安全由运维保证
4. 性能:中低性能要求,访问量不大。
采用第二代微服务技术开发,选择spring cloud作为微服务技术栈,前后端分离设计,后端只提供接口
二. Spring cloud框架相关技术
1. 注册中心eureka:每个微服务只需要知道注册中心的地址,向注册中心发送服务上线信息,并定期拉去服务列表就可以获取其他微服务的ip地址,编码时不用指定其他微服务的IP,可以使用rest template或者feign的方式实现RPC调用
2. API网关spring gateway:和spring cloud切合较好,配合eureka可以自动拉去微服务的rest接口,不需要代码配置。前端和客户端使用统一的网关地址可以访问所有微服务API
3. 配置中心nacos:阿里开源配置中心,相比本地用配置文件进行配置,集中配置中心可以实现可视化配置,集中配置,配置实时修改,配置共享等功能
4. API鉴权JWT token: 使用JWT token不使用cookie,一套接口实现前端,APP,第三方外部系统共用
三. 微服务和系统说明
1. eureka
在pom中增加eureka依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
main方法中增加eureka server注解:
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
其他微服务要注册到eureka需要在pom中配置依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
同时在配置文件中配置eureka地址:
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${eureka.service.port}/eureka/
eureka.instance.prefer-ip-address=true
eureka.instance.hostname =${eureka.instance.hostname}
2. nacos配置中心
官方文档地址:https://nacos.io/zh-cn/docs/what-is-nacos.html
按文档部署好配置中心后,增加3个公共配置:
systemAdmin.properties中配置jwt token的加密密钥和jwt token的过期时间,redis.properties配置redis的连接鉴权信息,dataSource.properites配置mysql数据库的连接鉴权信息
在需要加载配置项的微服务中增加nacos依赖配置:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>0.2.1.RELEASE</version>
</dependency>
在bootstrap.properties中指定配置中心的ip地址以及加载的配置项:
spring.cloud.nacos.config.server-addr=${spring.cloud.nacos.config.server-addr}
spring.cloud.nacos.config.ext-config[0].data-id=systemAdmin.properties
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=dataSource.properties
spring.cloud.nacos.config.ext-config[2].data-id=redis.properties
在代码中使用配置项和普通配置文件配置没有区别使用@Value即可
3. gateway微服务
pom文件中配置spring gateway依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
配置文件中开启自动发现微服务接口和接口名称小写
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
gateway中实现一个拦截器AuthFilter,对前端请求的jwt token进行解析,并调用system-admin服务判断该请求用户是否是合法用户并且具备该API的权限:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
//跳过不需要验证的路径
if(Arrays.asList(skipAuthUrls).contains(url)){
return chain.filter(exchange);
}
//从请求头中取出token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
//未携带token
if (token == null || token.isEmpty()) {
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = "{\"code\": \"401\",\"msg\": \"no jwt token in request header\"}"
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
//取出token包含的身份
String userId = restTemplate.getForEntity(
"http://system-admin/auth/checkPermission?token=" + token + "&&apiUrl= " + url, String.class).getBody();
if(userId == null || userId.isEmpty()){
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = "{\"code\": \"10002\",\"msg\": \"invalid token.\"}"
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
if(userId.equals("NoPermission")) {
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = "{\"code\": \"401\",\"msg\": \"this user do not have the access permission\"}"
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
//将现在的request,添加当前身份
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization-UserId", userId).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
API白名单过滤中,设置登录和获取验证码接口可以不经过鉴权直接访问
4. system-admin鉴权微服务
system-admin主要提供鉴权相关接口,登录,验证码获取,校验权限等功能
(1)获取图片验证码
@GetMapping("/getCode")
@ApiOperation(value = "获取图片验证码(有效期为60秒),返回数据为jpg图片", consumes = "application/x-www-form-urlencoded")
public void getCode(HttpServletResponse response, HttpServletRequest request) throws IOException {
String verifyCode = VerifyCodeUtil.generateVerifyCode(4);
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
stringRedisTemplate.opsForHash().put(Constants.REDIS_PREFIX_IMG_CODE_UUID + uuid, "code", verifyCode);
stringRedisTemplate.expire(Constants.REDIS_PREFIX_IMG_CODE_UUID + uuid, 60, TimeUnit.SECONDS);
response.addHeader("imgcode-uuid", uuid);
VerifyCodeUtil.outputImage(214, 80, response.getOutputStream(), verifyCode);
}
由于不使用cookie,需要在图片验证码返回头中增加一个id,前端登录需要把这个id和图片验证码一起发回
(2)登录接口,校验用户名密码,生产JWT token返回给前端
(3)jwt token刷新接口,刷新token并把老的token放入黑名单
(4)检查权限接口,校验当前用户是否是合法用户并且具备相应的API访问权限
5. service1业务微服务demo
一个业务微服务的demo,打印一个hello world。构建完毕后通过API网关直接访问:localhost:30002/server1/hello,返回无权限。先获取验证码,然后登录获取token后,在访问请求的header中增加
Authorization: [你获取的token]
再次访问hello接口,返回打印的hello world
四. 完整demo源代码
git地址: https://github.com/liuxiang19870216/lxdemo_springcloud