人的知识就好比一个圆圈,圆圈里面是已知的,圆圈外面是未知的。你知道得越多,圆圈也就越大,你不知道的也就越多。

0%

架构原则

数据量尽量少

数据在网络上传输需要时间,且不管是请求数据还是返回数据都需要服务器做处理,而服务器在写网络时通常都要做压缩和字符编码,这些都非常消耗 CPU,所以减少传输的数据量可以显著减少 CPU 的使用。

  • 简化秒杀页面的大小,去掉不必要的页面装修效果
  • JS/CSS压缩,减少流量

请求数要尽量少

浏览器每发出一个请求都多少会有一些消耗,例如建立连接要做三次握手,有的时候有页面依赖或者连接数限制,一些请求(例如 JavaScript)还需要串行加载等。另外,如果不同请求的域名不一样的话,还涉及这些域名的 DNS 解析,可能会耗时更久。因此减少请求数可以显著减少以上这些因素导致的资源消耗。

  • 合并 CSS 和 JavaScript 文件,以减少请求数

路径要尽量短

所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。
每增加一个连接都会增加新的不确定性。从概率统计上来说,假如一次请求经过 5 个节点,每个节点的可用性是 99.9% 的的话,那么整个请求的可用性是:99.9% 的 5 次方,约等99.5%。
所以缩短请求路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化与反序列化),并减少延时(可以减少网络传输耗时)。

  • 将多个相互强依赖的应用合并部署在一起,把远程过程调用(RPC)变成 JVM 内部之间的方法调用。

依赖要尽量少

所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。
要减少依赖,我们可以给系统进行分级,比如 0 级系统、1 级级系统、2 级系统、3 级系统,0 级系统如果是最重要的系统,以此类推。
注意,0 级系统要尽量减少对 1 级系统的强依赖,防止重要的系统被不重要的系统拖垮。例如支付系统是 0 级系统,而优惠券是 1 级系统的话,在极端情况下可以把优惠券给降级,防止支付系统被优惠券这个 1 级系统给拖垮。

  • 次级系统服务降级

不要有单点

单点意味着没有备份,风险不可控,我们设计分布式系统最重要的原则就是“消除单点”。
要避免单点,关键点是避免将服务的状态和机器绑定,即把服务无状态化,这样服务就可以在机器中随意移动。

  • 部署配置中心,实现配置动态化

动静分离

所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。
简单来说,“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有Cookie 等私密数据。
强调一下,我们所说的静态数据,不能仅仅理解为传统意义上完全存在磁盘上的 HTML 页面,它也可能是经过 Java 系统产生的页面,但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。

  1. 把静态数据缓存到离用户最近的地方
  • 用户浏览器
  • 服务器缓存
  • CDN
  1. 静态化改造就是要直接缓存 HTTP 连接
    Web 代理服务器根据请求 URL,直接取出对应的 HTTP 连接而不是仅仅缓存数据。Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。

  2. 选择合适的缓存框架

  • Java
  • Nginx
  • Apache
  • Varnish

流量削峰

削峰的存在,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。

  • 排队
  • 答题
  • 分层过滤

减库存

  1. 基于数据库行锁

    1
    update goods set inventory = inventory-1 where id = #{goodsId} and inventory > 0

    问题:大量锁竞争时,会影响数据库性能。

  2. 使用数据库乐观锁

    1
    update goods set inventory = inventory-1 where id = #{goodsId} and inventory > 0 and version = #{version}

    问题:库存100,且同时只有100人抢购商品时,实际卖出的商品可能少于100。

  3. 使用redis令牌桶
    预先创建n个令牌桶,n等于商品的个数。请求到来时,先获取令牌桶,只有获取了令牌桶的请求才能实际购买商品。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建令牌桶
    List<String> tokens = LongStream.range(0, number)
    .mapToObj(index -> "spike_" + UUID.randomUUID().toString().replace("-", ""))
    .collect(Collectors.toList());
    redisTemplate.opsForList().leftPushAll(String.valueOf(goodsId), tokens);

    // 获取令牌桶
    String token = redisTemplate.opsForList().leftPop(String.valueOf(goodsId));
    if (StringUtils.isEmpty(token)) {
    throw new RuntimeException(String.format("商品:%s已经售空", goodsId));
    }

    问题:实际卖出的商品可能少于100。

  4. 使用redis预减库存

    1
    2
    3
    4
    5
    6
    7
    // 判断redis中库存是否小于0
    Long stock = redisTemplate.opsForValue().decrement("spike_" + goodsId);
    if (stock == null || stock < 0) {
    // 内存标记商品已售空
    goodsOverMap.put(goodsId, true);
    throw new RuntimeException(String.format("商品已经售空,商品id:%s", goodsId));
    }
  5. 异步减库存/创建订单

    1
    2
    3
    4
    5
    6
    7
    String token = redisTemplate.opsForList().leftPop(String.valueOf(goodsId));
    if (StringUtils.isEmpty(token)) {
    throw new RuntimeException(String.format("商品:%s已经售空", goodsId));
    }

    // 异步发送秒杀消息
    sendSpikeMsg(goodsId, userId);

兜底方案

降级

所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。

限流

限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。

拒绝服务

当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的保护方式。

优化

静态资源优化

  • JS/CSS压缩,减少流量
  • CDN就近访问

缓存

  • 页面缓存
  • 对象缓存

其它

  • 隐藏秒杀地址

整体流程

![web安全整体流程](/images/yoyo mall/web安全整体流程.png)
实际流程中,Access Token就是JWT token。

JWT签名

使用RSA非对称加密算法(私钥加密,公钥解密)做jwt签名和校验。

创建KeyPair

  1. 编程方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    // 模数bit位数,1024位足够安全
    generator.initialize(1024);
    KeyPair pair = generator.generateKeyPair();
    RSAPrivateKey privateKey = (RSAPrivateKey) pair.getPrivate();
    RSAPublicKey publicKey = (RSAPublicKey) pair.getPublic();

    RSAPublicKeySpec publicSpec = new RSAPublicKeySpec(new BigInteger(publicKey.getModulus()), new BigInteger(publicKey.getPublicExponent()));
    RSAPrivateKeySpec privateSpec = new RSAPrivateKeySpec(new BigInteger(privateKey.getModulus()), new BigInteger(privateKey.getPrivateExponent()));
    KeyFactory factory = KeyFactory.getInstance("RSA");
    KeyPair keyPair = new KeyPair(factory.generatePublic(publicSpec), factory.generatePrivate(privateSpec));
  2. 使用java keytool
    1
    2
    3
    4
    5
    6
    7
    8
    # 创建证书
    keytool -genkey -alias yoyo-mall-certificate -keyalg rsa -keystore E:\keys.keystore -keysize 1024 -validity 36500

    # 查看证书库
    keytool -list -keystore E:\keys.keystore

    # 导出公钥
    keytool -export -alias yoyo-mall-certificate -keystore E:\keys.keystore -file E:\publickey.cer
    ![创建证书示例](/images/yoyo mall/创建证书示例.png)
    1
    2
    3
    ClassPathResource resource = new ClassPathResource(keystoreLocation);
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, keystorePassword.toCharArray());
    KeyPair keyPair = keyStoreKeyFactory.getKeyPair(certificateAlias, certificatePassword.toCharArray());

XSS攻击

XSS攻击,即跨站脚本攻击XSS(Cross Site Scripting),指恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页面时,嵌入Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
XSS的攻击目标是为了盗取客户端的cookie或者其他网站用于识别客户端身份的敏感信息。获取到合法用户的信息后,攻击者甚至可以假冒最终用户与网站进行交互。
![XSS攻击示例流程](/images/yoyo mall/XSS攻击示例流程.png)

防护措施

  1. response添加header:X-Xss-Protection:1; mode=block,该措施只能防护反射行XSS,且Spring Cloud Gateway已默认添加。

CSRF防护

CSRF,即跨站请求伪造(Cross Site Request Forgery)。示例流程如下:
![csrf示例流程](/images/yoyo mall/csrf示例流程.png)

由于CSRF基于cookie-session,而jwt token是无状态的,所以jwt token天然支持CSRF防护。

Session固定攻击

Session固定攻击(Session Fixation Attack),是利用应用系统在服务器的会话ID固定不变机制,借助他人用相同的会话ID获取认证和授权,然后利用该会话ID劫持他人的会话以成功冒充他人,造成会话固定攻击。示例流程如下:

  1. 攻击者访问网站 http:///www.bank.com ,获取他自己的session id,如:SID=123;
  2. 攻击者给目标用户发送链接,并带上自己的session id,如:http:///www.bank.com/?SID=123
  3. 目标用户点击了 http:///www.bank.com/?SID=123 ,像往常一样,输入自己的用户名、密码登录到网站;
  4. 由于服务器的session id不改变,现在攻击者点击 http:///www.bank.com/?SID=123 ,他就拥有了目标用户的身份,可以为所欲为了。

由于为用到session,所以也不需做额外防护。

集成网关(Spring Cloud Gateway)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@EnableWebFluxSecurity
public class SecurityConfiguration {
/**
* swagger文档相关请求地址无需身份验证
*/
private static final String[] SWAGGER_URLS = new String[]{
"/swagger-ui.html",
"/webjars/springfox-swagger-ui/**",
"/*/v2/api-docs/**",
"/swagger-resources/**"
};

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.pathMatchers(SWAGGER_URLS).permitAll()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer().jwt();
return http.build();
}
}

集成Feign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
private static final String BEARER = "Bearer";

@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(HttpHeaders.AUTHORIZATION, extract());
}

private String extract() {
String token = ((Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getTokenValue();
return String.format("%s %s", BEARER, token);
}
}
1
hystrix.command.default.execution.isolation.strategy: SEMAPHORE

network

1
docker network create op_net

mysql

linux:

1
2
3
4
5
6
7
sudo docker run -d \
--network op_net \
--hostname op_mysql \
--name mysql \
-e MYSQL_ROOT_PASSWORD=admin123 \
-p 3306:3306 \
mysql:8.0.18

windows:

1
2
3
4
5
6
7
docker run -d `
--network op_net `
--hostname mysql `
--name mysql `
-e MYSQL_ROOT_PASSWORD=admin123 `
-p 3306:3306 `
mysql

rabbitmq

linux:

1
2
3
4
5
6
7
sudo docker run -d \
--network op_net \
--hostname op_rabbitmq \
--name rabbitmq \
--restart=always \
-p 5672:5672 -p 15672:15672 \
rabbitmq:3-management

windows:

1
2
3
4
5
6
7
docker run -d `
--network op_net `
--hostname op_rabbitmq `
--name rabbitmq `
--restart=always `
-p 5672:5672 -p 15672:15672 `
rabbitmq:3-management

redis

linux:

1
2
3
4
5
6
7
8
9
sudo docker run -d \
--network op_net \
--hostname op_redis \
--name redis \
--restart=always \
--privileged=true \
-v /srv/redis/conf:/usr/local/etc/redis/redis.conf \
-p 6379:6379 \
redis redis-server /usr/local/etc/redis/redis.conf

windows:

1
2
3
4
5
6
7
8
9
docker run -d `
--network op_net `
--hostname op_redis `
--name redis `
--restart=always `
--privileged=true `
-v /d/Development/Workspace/Hyper-V/volumes/redis/conf:/usr/local/etc/redis/redis.conf `
-p 6379:6379 `
redis redis-server /usr/local/etc/redis/redis.conf

访问redis

linux:
sudo docker run -it --network op_net --rm redis redis-cli -h redis
windows:
docker run -it --network op_net --rm redis redis-cli -h redis

opnezipkin

linux:

1
2
3
4
5
6
7
8
sudo docker run -d \
--network op_net \
--hostname op_zipkin \
--name zipkin \
--restart=always \
-e RABBIT_ADDRESSES=rabbitmq:5672 \
-p 9411:9411 \
openzipkin/zipkin

windows:

1
2
3
4
5
6
7
8
docker run -d `
--network op_net `
--hostname op_zipkin `
--name zipkin `
--restart=always `
-e RABBIT_ADDRESSES=rabbitmq:5672 `
-p 9411:9411 `
openzipkin/zipkin

zookeeper

linux:

1
2
3
4
5
6
7
8
9
sudo docker run -d \
--network op_net \
--hostname op_zookeeper \
--name zookeeper \
--restart=always \
-p 2181:2181 \
-v /srv/zookeeper/conf:/conf \
-e ZOO_LOG4J_PROP="INFO,ROLLINGFILE" \
zookeeper

windows:

1
2
3
4
5
6
7
8
9
docker run -d `
--network op_net `
--hostname op_zookeeper `
--name zookeeper `
--restart=always `
-p 2181:2181 `
-v /d/Development/Workspace/Hyper-V/volumes/zookeeper/conf:/conf `
-e ZOO_LOG4J_PROP="INFO,ROLLINGFILE" `
zookeeper

访问zookeeper

linux:
sudo docker run -it --network op_net --rm zookeeper zkCli.sh -server zookeeper
windows:
docker run -it --network op_net --rm zookeeper zkCli.sh -server zookeeper

kafka

linux:

1
2
3
4
5
6
7
8
9
10
11
sudo docker run -d \
--network op_net \
--hostname op_kafka \
--name kafka \
--restart=always \
-e KAFKA_BROKER_ID=0 \
-e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 \
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.0.107:9092 \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
-p 9092:9092 \
wurstmeister/kafka

windows:

1
2
3
4
5
6
7
8
9
10
11
docker run -d `
--network op_net `
--hostname op_kafka `
--name kafka `
--restart=always `
-e KAFKA_BROKER_ID=0 `
-e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 `
-e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.0.107:9092 `
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 `
-p 9092:9092 `
wurstmeister/kafka

创建主题

  1. 进入容器:sudo docker exec -it kafka /bin/bash
  2. 创建主题:/opt/kafka/bin/kafka-topics.sh --create --replication-factor 1 --partitions 1 --zookeeper zookeeper:2181 --topic yoyo-mall-log
  3. 查询主题:/opt/kafka/bin/kafka-topics.sh --list --zookeeper zookeeper:2181

配置logstash

  1. 执行sudo docker exec -it elk /bin/bash进入容器
  2. 打开文件:vi /etc/logstash/conf.d/02-beats-input.conf
  3. 修改后配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    input {
    tcp {
    port => 5044
    codec => json_lines
    }

    kafka {
    bootstrap_servers => "192.168.0.107:9092"
    topics => ["yoyo-mall-log"]
    }
    }

    output{
    elasticsearch {
    hosts => ["localhost:9200"]
    index => "yoyo-mall-log"
    }
    }

kafka-manager

1
2
3
4
5
sudo docker run -d \
-e ZK_HOSTS="192.168.0.107:2181" \
-p 9000:9000 \
hlebalbau/kafka-manager:stable \
-Dpidfile.path=/dev/null

elk

linux:

1
2
3
4
5
6
7
sudo docker run -d \
--network op_net \
--hostname op_elk \
--name elk \
--restart=always \
-p 5601:5601 -p 9200:9200 -p 5044:5044 \
sebp/elk:670

windows:

1
2
3
4
5
6
7
docker run -d `
--network op_net `
--hostname op_elk `
--name elk `
--restart=always `
-p 5601:5601 -p 9200:9200 -p 5044:5044 `
sebp/elk:670

修改配置

linux下运行elk镜像,要求vm.max_map_count至少为262144,否则启动会报错:![step42](/images/VMWare install/step42.png)。
在“/etc/sysctl.conf”文件末行添加:vm.max_map_count=262144,然后执行sudo /sbin/sysctl -p使配置立即生效。

配置logstash

  1. 执行sudo docker exec -it elk /bin/bash进入容器
  2. 打开文件:vi /etc/logstash/conf.d/02-beats-input.conf
  3. 修改配置:
    原配置:
    1
    2
    3
    4
    5
    6
    7
    8
    input {
    beats {
    port => 5044
    ssl => true
    ssl_certificate => "/etc/pki/tls/certs/logstash-beats.crt"
    ssl_key => "/etc/pki/tls/private/logstash-beats.key"
    }
    }
    修改后配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    input {
    tcp {
    port => 5044
    codec => json_lines
    }
    }

    output{
    elasticsearch {
    hosts => ["localhost:9200"]
    }
    }
  4. 重启容器:`sudo docker restart elk

nexus

linux:

1
2
3
4
5
6
7
8
sudo docker run -d \
--hostname op_nexus \
--name nexus \
--restart=always \
--privileged=true \
-v /srv/nexus-data:/nexus-data \
-p 18081:8081 \
sonatype/nexus3

windows:

1
2
3
4
5
6
7
8
docker run -d `
--hostname op_nexus `
--name nexus `
--restart=always `
--privileged=true `
-v /d/Development\Workspace\Hyper-V\volumes\nexus-data:/nexus-data `
-p 18081:8081 `
sonatype/nexus3

更改权限

如果启动时报错:
![step30](/images/VMWare install/step30.png)
执行命令:sudo chmod 777 /srv/nexus-data

gitlab

参考:https://docs.gitlab.com/omnibus/docker/

linux:

1
2
3
4
5
6
7
8
9
10
sudo docker run --detach \
--hostname op_gitlab \
--name gitlab \
--restart always \
--privileged=true \
--volume /srv/gitlab/config:/etc/gitlab \
--volume /srv/gitlab/logs:/var/log/gitlab \
--volume /srv/gitlab/data:/var/opt/gitlab \
--publish 10443:443 --publish 10080:10080 --publish 10022:22 \
gitlab/gitlab-ce:latest

windows:

1
2
3
4
5
6
7
8
9
10
docker run --detach `
--hostname op_gitlab `
--name gitlab `
--restart always `
--privileged=true `
--volume /d/Development\Workspace\Hyper-V\volumes\gitlab\config:/etc/gitlab `
--volume /d/Development\Workspace\Hyper-V\volumes\gitlab\logs:/var/log/gitlab `
--volume /d/Development\Workspace\Hyper-V\volumes\gitlab\data:/var/opt/gitlab `
--publish 10443:443 --publish 10080:10080 --publish 10022:22 `
gitlab/gitlab-ce:latest

配置ip&端口

sudo vi /srv/gitlab/config/gitlab.rb
修改以下三行配置:

1
2
3
external_url 'http://172.16.78.80:10080'
gitlab_rails['gitlab_ssh_host'] = 'http://172.16.78.80'
gitlab_rails['gitlab_shell_ssh_port'] = 10022

其中,172.16.78.80为宿主机的ip地址,可以通过ip addr查看;1008010022则分别为8022映射到宿主机的端口。
之后重启:sudo docker restart gitlab

创建ssh key

参考:https://docs.gitlab.com/ee/ssh/README.html#adding-an-ssh-key-to-your-gitlab-account
gitlab启动之后,会提示“You won’t be able to pull or push project code via SSH until you add an SSH key to your profile”。
需要配置ssh key:

  1. 检查是否已有key:cat ~/.ssh/id_rsa.pub
  2. 如果没有,则创建:ssh-keygen -o -t rsa -b 4096 -C "email@example.com",不断回车;
  3. 复制创建的ssh key,然后添加到gitlab中;
    ![step29](/images/VMWare install/step29.png)
  4. 检查:ssh -T git@172.16.78.80(需要前完成步骤4,且之后还提示需要密码,解决办法待研究:https://www.jianshu.com/p/e946acf9f26e)。

安装ssh server

1
2
sudo apt-get install openssh-server
ssh localhost

配置jenkins

linux:

1
2
3
4
5
6
7
8
9
sudo docker run -d \
--hostname op_jenkins \
--name jenkins \
--privileged=true \
-v /srv/jenkins_home:/var/jenkins_home \
-p 18080:8080 -p 50000:50000 \
-p 8761:8761 -p 8180:8180 -p 8181:8181 -p 8182:8182 -p 8183:8183 -p 8184:8184 -p 8185:8185 -p 8186:8186 \
-p 8101:8101 -p 8102:8102 -p 8103:8103 -p 8104:8104 -p 8105:8105 -p 8106:8106 \
jenkins/jenkins

windows:

1
2
3
4
5
6
7
8
9
docker run -d `
--hostname op_jenkins `
--name jenkins `
--privileged=true `
-v /d/Development\Workspace\Hyper-V\volumes\jenkins_home:/var/jenkins_home `
-p 18080:8080 -p 50000:50000 `
-p 8761:8761 -p 8180:8180 -p 8181:8181 -p 8182:8182 -p 8183:8183 -p 8184:8184 -p 8185:8185 -p 8186:8186 `
-p 8101:8101 -p 8102:8102 -p 8103:8103 -p 8104:8104 -p 8105:8105 -p 8106:8106 `
jenkins/jenkins

上面8761及之后的端口都是jenkins中部署的项目所用到的端口。

解锁

第一次进入jenkins,会提示:
![step32](/images/VMWare install/step32.png)
执行命令:sudo docker exec -it jenkins /bin/bash进入docker容器,再执行命令cat /var/jenkins_home/secrets/initialAdminPassword查看密码,然后将密码拷贝到界面继续。

安装插件

选择“安装推荐的插件”

配置jdk/git/maven

进入管理界面:
![step33](/images/VMWare install/step33.png)
![step34](/images/VMWare install/step34.png)
ps: JAVA_HOME地址可以通过命令echo $JAVA_HOME查询
![step35](/images/VMWare install/step35.png)
![step36](/images/VMWare install/step36.png)

  1. 安装maven插件
    ![step37](/images/VMWare install/step37.png)
    ![step38](/images/VMWare install/step38.png)

  2. 新建任务
    ![step39](/images/VMWare install/step39.png)
    ![step40](/images/VMWare install/step40.png)
    ![step41](/images/VMWare install/step41.png)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
SERVER_NAME=yoyo-mall-basic-registry-server
JAR_NAME=yoyo-mall-basic-registry-server-1.0-SNAPSHOT
JAR_PATH=/var/jenkins_home/workspace/yoyo-mall-basic-registry-server/target/
JAR_WORK_PATH=/var/jenkins_home/workspace/yoyo-mall-basic-registry-server/target/

PID=`ps -ef | grep "$SERVER_NAME" | awk '{print $2}'`
for id in $PID
do
kill -9 $id
echo "killed $id"
done

cp $JAR_PATH/$JAR_NAME.jar $JAR_WORK_PATH
cd $JAR_WORK_PATH
chmod 755 $JAR_NAME.jar


BUILD_ID=dontKillMe nohup java -jar $JAR_NAME.jar &

安装VMWare

  1. 下载
    https://my.vmware.com/cn/web/vmware/info/slug/desktop_end_user_computing/vmware_workstation_pro/15_0
    需要登录,没有账号需要注册
  2. 输入许可证
  • YG5H2-ANZ0H-M8ERY-TXZZZ-YKRV8
  • UG5J2-0ME12-M89WY-NPWXX-WQH88
  • UA5DR-2ZD4H-089FY-6YQ5T-YPRX6
  • GA590-86Y05-4806Y-X4PEE-ZV8E0
  • ZF582-0NW5N-H8D2P-0XZEE-Z22VA
  • YA18K-0WY8P-H85DY-L4NZG-X7RAD
  1. 安装

创建虚拟机

  1. 下载
    https://ubuntu.com/download/desktop
    按需选择桌面版或服务器版
  2. 创建虚拟机(Ubuntu Desktop)
    ![step01](/images/VMWare install/step01.png)
    ![step02](/images/VMWare install/step02.png)
    ![step03](/images/VMWare install/step03.png)
    ![step04](/images/VMWare install/step04.png)
    ![step05](/images/VMWare install/step05.png)
    ![step06](/images/VMWare install/step06.png)
    ![step07](/images/VMWare install/step07.png)
    ![step08](/images/VMWare install/step08.png)
    nat模式:无需额外配置,虚拟机即可联网
    ![step09](/images/VMWare install/step09.png)
    ![step10](/images/VMWare install/step10.png)
    ![step11](/images/VMWare install/step11.png)
    ![step12](/images/VMWare install/step12.png)
    ![step13](/images/VMWare install/step13.png)
    ![step14](/images/VMWare install/step14.png)
    ![step15](/images/VMWare install/step15.png)
    ![step16](/images/VMWare install/step16.png)

桥接模式

![step25](/images/VMWare install/step25.png)
![step26](/images/VMWare install/step26.png)
![step27](/images/VMWare install/step27.png)
桥接到的地址选择能联网的网卡:
![step27(2)](/images/VMWare install/step27(2).png)
![step28](/images/VMWare install/step28.png)

启动虚拟机

如果提示:
![step01](/images/VMWare install/step01.png)
去控制面板中停掉Hyper-V
![step02](/images/VMWare install/step02.png)

![step17](/images/VMWare install/step17.png)
![step18](/images/VMWare install/step18.png)
![step19](/images/VMWare install/step19.png)
![step20](/images/VMWare install/step20.png)
![step21](/images/VMWare install/step21.png)
![step22](/images/VMWare install/step22.png)
![step23](/images/VMWare install/step23.png)
![step24](/images/VMWare install/step24.png)

更新&升级

1
2
sudo apt-get update
sudo apt-get upgrade

安装Tools

sudo apt-get install open-vm-tools-desktop
安装之后需重启生效

安装Docker

参考:https://docs.docker.com/install/linux/docker-ce/ubuntu/

  1. sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
  2. curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
  3. sudo apt-key fingerprint 0EBFCD88
  4. sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  5. sudo apt-get update
  6. sudo apt-get install docker-ce docker-ce-cli containerd.io
  7. sudo docker run hello-world

TokenGranter

TokenGranter UML

AuthorizationServerTokenServices

AuthorizationServerTokenServices UML

ResourceServerTokenServices

ResourceServerTokenServices UML

ConsumerTokenServices

ConsumerTokenServices UML

TokenStore

TokenStore

TokenStore UML

ApprovalStore(JWT)

ApprovalStore UML

TokenEnhancer(JWT)

TokenEnhancer UML

AccessTokenConverter(JWT | CHECK_TOKEN)

AccessTokenConverter UML

ClientDetails

ClientDetails

ClientDetails UML

ClientDetailsService

ClientDetailsService UML

客户端模式(Client Credentials Grant)

![OAuth2 客户端模式](/images/springsecurity/OAuth2 客户端模式.png)

请求示例:/oauth/token?grant_type=client_credentials&client_id=client&client_secret=secret&scope=read
![OAuth2 客户端模式示例](/images/springsecurity/OAuth2 客户端模式示例.png)

密码模式(Resource Owner Password Credentials Grant)

![OAuth2 密码模式](/images/springsecurity/OAuth2 密码模式.png)

请求示例:/oauth/token?grant_type=password&client_id=password&client_secret=secret&scope=read&username=admin&password=admin
![OAuth2 客户端模式示例](/images/springsecurity/OAuth2 客户端模式示例.png)

简化模式(Implicit Grant)

![OAuth2 简化模式](/images/springsecurity/OAuth2 简化模式.png)

请求示例:/oauth/authorize?response_type=token&client_id=implicit&redirect_uri=https://www.baidu.com&scope=read
登录之后,返回:
https://www.baidu.com/#access_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjE1NTkwMjY3OTIsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI5ODI0NWY0Yi0wMDg0LTRkODQtYWU4MS1iZjhhNzRjMzM4YzkiLCJjbGllbnRfaWQiOiJpbXBsaWNpdCJ9.IqM0kIrUHX6_U4Xmmpn6VLlPLP_4zhdT-LSEyL6fW8mBQzzGVQzNrEpkvFBJLji1sq-0ZInl9TuWge5Rj0OsXrVC7wZjtFUiaT-ZC1bhWZx_tnuiATw72IUk3betWdVliYBgs979gZE8NXB6MChOIuXCuH5sSI7bj6fbfz0-56ja9RFtboNefntcoflYpGUaIion8emL0qbY09SgR_1D9GKKUal-4rnJWTAPbsaGtWCjOAaYDaYNVS_jE194zyqodSW1s27IfsOoPdr3WQ2ebF83RmH3h9o2Gw-INvfLVAHAh0YccSgeXGBkNBruxYo2tUaO3XlV8RSea30fXqqfAA&token_type=bearer&expires_in=86399&jti=98245f4b-0084-4d84-ae81-bf8a74c338c9

授权码模式

![OAuth2 授权码模式](/images/springsecurity/OAuth2 授权码模式.png)

流程示例:
![OAuth2 授权码模式示例](/images/springsecurity/OAuth2 授权码模式示例.png)

备注

  • 四种模式,默认都要求用户授权,通过设置autoApprove为true,可以实现自动授权。
  • 四种模式,默认都要求用户登录。客户端模式|密码模式下,通过配置security.allowFormAuthenticationForClients(),可以跳过登录步骤(使用ClientCredentialsTokenEndpointFilter)。
  • 客户端模式|密码模式,直接请求到TokenEndpoint;授权码模式,先请求到AuthorizationEndpoint,然后转发请求到TokenEndpoint;简化模式,只会请求到AuthorizationEndpoint
  • 如果未指定scope,则scope为client的所有scopes。
  • 密码模式|授权码模式,支持refresh token。客户端模式,通过设置ClientCredentialsTokenGranter.allowRefresh为true,可以返回refresh token。
  • 应用场景:
    • 客户端模式:为后台api服务消费者设计
    • 密码模式:为遗留系统设计
    • 简化模式:为web浏览器应用设计
    • 授权码模式:标准模式

TokenEndpoint

![TokenEndpoint 时序图1](/images/springsecurity/TokenEndpoint 时序图1.png)
![TokenEndpoint 时序图2](/images/springsecurity/TokenEndpoint 时序图2.png)
![TokenEndpoint 时序图3](/images/springsecurity/TokenEndpoint 时序图3.png)
![TokenEndpoint 时序图4](/images/springsecurity/TokenEndpoint 时序图4.png)

AuthorizationEndpoint

![AuthorizationEndpoint 时序图1](/images/springsecurity/AuthorizationEndpoint 时序图1.png)
![AuthorizationEndpoint 时序图2](/images/springsecurity/AuthorizationEndpoint 时序图2.png)

SecurityFilterChain

Spring Security 过滤器链 UML

  • 表单认证
    Spring Security过滤器(表单认证)

  • Http Basic认证
    Spring Security过滤器(Http Basic认证)

AuthenticationManager

AuthenticationManager UML

Authentication

Authentication UML

  • UsernamePasswordAuthenticationToken

    • JaasAuthenticationToken
  • AnonymousAuthenticationToken

  • RememberMeAuthenticationToken

  • PreAuthenticatedAuthenticationToken

  • BearerTokenAuthenticationToken

  • OAuth2AuthenticationToken

  • OAuth2LoginAuthenticationToken

  • OAuth2AuthorizationCodeAuthenticationToken

  • OpenIDAuthenticationToken

  • CasAuthenticationToken

  • CasAssertionAuthenticationToken

  • RunAsUserToken

  • TestingAuthenticationToken

  • AbstractOAuth2TokenAuthenticationToken

    • JwtAuthenticationToken
    • OAuth2IntrospectionAuthenticationToken

AuthenticationProvider

AuthenticationProvider UML

UsernamePasswordAuthenticationToken:

  • AbstractUserDetailsAuthenticationProvider

    • DaoAuthenticationProvider
  • AbstractJaasAuthenticationProvider

    • JaasAuthenticationProvider
    • DefaultJaasAuthenticationProvider
  • RemoteAuthenticationProvider

  • AbstractLdapAuthenticationProvider

    • ActiveDirectoryLdapAuthenticationProvider
    • LdapAuthenticationProvider
  • AnonymousAuthenticationProvider

  • RememberMeAuthenticationProvider

PreAuthenticatedAuthenticationToken:

  • PreAuthenticatedAuthenticationProvider
  • GoogleAccountsAuthenticationProvider

BearerTokenAuthenticationToken:

  • JwtAuthenticationProvider
  • OAuth2IntrospectionAuthenticationProvider

OAuth2LoginAuthenticationToken:

  • OAuth2LoginAuthenticationProvider

  • OidcAuthorizationCodeAuthenticationProvider

  • OAuth2AuthorizationCodeAuthenticationProvider

  • OpenIDAuthenticationProvider

  • CasAuthenticationProvider

  • RunAsImplAuthenticationProvider

  • TestingAuthenticationProvider

SecurityContext

SecurityContext UML

UserDetails

UserDetails

UserDetailsService

UserDetailsService UML

AuthenticationEntryPoint

AuthenticationEntryPoint

AuthenticationEntryPoint UML

form login -> LoginUrlAuthenticationEntryPoint
basic login -> BasicAuthenticationEntryPoint
undefined -> Http403ForbiddenEntryPoint
resource server -> OAuth2AuthenticationEntryPoint

在Java类库中,任务执行的主要抽象不是Thread,而是Executor,虽然它是个接口,却为灵活且强大的异步任务执行框架提供了基础。

两级调度模型

在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到处理器上。这种两级调度模型的示意图如下图所示:
Executor两级调度模型
从图中可以看出,应用程序通过Executor框架控制上层的调度;而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。
Executor基于生产者-消费者模式,提交任务的操作相当于生产者(生成待完成工作单元),执行任务的线程则相当于消费者(执行这些工作单元)。如果要在程序中实现生产者-消费者的设计,那么最简单的方式通常就是使用Executor。

Executor框架类结构图

Executor框架类结构图
由上图可知,Java线程池正是Executor框架的核心成员,应用程序向线程池提交任务,线程池内部执行任务。

ExecutorService类图

ExecutorService类图
如上图,Executor接口只有一个execute方法,表示执行任务,ExecutorService在其基础上扩展了以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* 提交任务并返回一个Future对象
* 一旦任务执行成功,调用Futrue的get方法将会任务执行结果
*
* @param task 提交的任务
* @return a Future representing pending completion of the task
*/
<T> Future<T> submit(Callable<T> task);

/**
* 提交任务并返回一个Future对象
* 一旦任务执行成功,调用Futrue的get方法将会返回给定result
*
* @param task 提交的任务
* @param result 返回值
* @return a Future representing pending completion of the task
*/
<T> Future<T> submit(Runnable task, T result);

/**
* 提交任务并返回一个Future对象
* 一旦任务执行成功,调用Futrue的get方法将会返回null
*
* @param task 提交的任务
* @return a Future representing pending completion of the task
*/
<T> Future<T> submit(Runnable task);

/**
* 关闭线程池
* 不再接受新的任务,同时等待已经提交的任务执行完成——包括那些还未开始执行的任务
*/
void shutdown();

/**
* 关闭线程池
* 将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务
*/
void shutdownNow();

/**
* @return 线程池是否关闭
*/
boolean isShutdown();

/**
* @return 线程池关闭后,是否所有的任务都执行完毕
*/
boolean isTerminated();

/**
* 当前线程阻塞,直到
* 等所有已提交的任务(包括正在运行的和队列中等待的)执行完
* 或者等超时时间到
* 或者线程被中断,抛出InterruptedException
* @return true(shutdown请求后所有任务执行完毕)或false(已超时)
*/
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

/**
* 执行给定任务列表,并当所有任务都执行完毕后,返回Future对象列表
* @param tasks 提交的任务列表
* @return a list of Futures representing the tasks, in the same sequential order as produced by the iterator for the
* given task list, each of which has completed
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

/**
* 执行给定任务列表,并当所有任务都执行完毕后,返回Future对象列表
* 如果超时,还没有执行完毕的任务会被取消
* @param tasks 提交的任务列表
* @return a list of Futures representing the tasks, in the same sequential order as produced by the iterator for the
* given task list. If the operation did not time out, each task will have completed. If it did time out, some
* of these tasks will not have completed.
*/
T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException;

/**
* 执行给定任务列表,并当其中一个任务执行完毕后,返回Future对象
* @param tasks 提交的任务列表
* @return the result returned by one of the tasks
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

/**
* 执行给定任务列表,并当其中一个任务执行完毕后,返回Future对象
* 如果超时,抛出TimeoutException
* @param tasks 提交的任务列表
* @return the result returned by one of the tasks
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;

Executor框架的使用

调用Exexutors类中的静态方法来创建线程池,然后向线程池提交任务。
Executors类图.png

不过目前大厂的编码规范中基本上都不建议使用Executors了,最重要的原因是:Executors提供的很多方法默认使用的都是无界的LinkedBlockingQueue,高负载情境下,无界队列很容易导致OOM,而OOM会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。

之前我有反映目前后端框架中存在哪些问题,但没有系统的梳理;也有表示自己在基于Spring Boot对后端框架做改造,但也没有具体的展示。本文就是简单的概要我认为后端框架中有哪些不足之处,做了哪些扩展,又为什么要引入Spring Boot,至于更详细的阐述,将在后续的文章中陆续展开。

不足之处

  • xml配置繁琐
    相信大家在创建新web项目的时候,都会从其它项目中拷贝applicationContext.xml、applicationContext-dubbo.xml和applicationContext-security.xml等配置文件,这些配置文件都是必须的,但里面的内容除了数据库名、web context等地方外,绝大部分都是我们不需要修改的。基于Spring Boot,我们可以引入或定制各类即插即用的starters,再也不需要配置xml!
  • 依赖库版本不一致
    在我们应用中,同一个第三方库可能有好几个版本,这样很容易出现版本冲突问题,原因在于缺少全局性的pom文件来统一指定版本号。
  • 库与库紧耦合
    库与库耦合度过高,就可能会出现了某个库做了修改,导致其它依赖了它的库都要做相应的更改,这类问题存在于我们好多类库中。
    比如我们有个excel导入导出的库icop-pubapp-excel,它里面依赖了另外两个库:icop-database和icop-refer,这是必要的吗?
    之所以会用到icop-database,只是因为用到了它里面的异常工具类ExceptionUtils,且不说这个类是否有必要,我们至于因为一个异常类就引入整个icop-database吗?
    之所以会用到icop-refer,则是因为导出的时候也会模拟序列化操作。首先导出应该只负责接收数据并将其导出到excel,至于数据怎么来的它不需要关心;其次导出通常是导出大量数据,如果在某个时刻缓存失效,那么每一条数据都去查n次数据库,岂不是会非常的慢?
    在设计类库的时候要分析库的边界。
  • 库内部低内聚
    库应该是由相关性很强的代码组成,只负责某一类功能。可是我们看下库icop-pubapp-platform,它里面即包含持久化,又包含导入导出和打印,还做了es数据同步等,这意味着我们要修改其中的任意一项功能,都可能会影响到其它的功能。
  • 库依赖冗余
    很多库并没有用到其他库,却又引入了它们。最典型的就是库icop-core,它里面定义了大量乱七八糟却没用到的库。
  • 三层架构类膨胀
    三层架构,即表现层、业务逻辑层和数据访问层。通常只需要1个contronller、2个service(接口+实现)、2个dao(接口加实现),共计5个类,可是我们现在即使做个简单的增删改查业务,都会创建15个类。
  • 代码生成
    代码生成可以帮我们快速开发,比如上面说的15个类就是通过代码生成来快速创建的,但问题在于如果我们后期要对生成的代码做修改和扩展,还是需要一个个类的去调整。如果是普遍性的实现就应该考虑封装起来,Don’t Repeat Yourself !
  • 引用值序列化
    引用,即我们所说的“参照”。目前的序列化可以快速将vo中某个外键序列化为包含了id\name\code等属性的json串,但它的问题在于如果调用者基于我们的api文档做开发,他识别不了每个外键到底是返回字符串、JSON对象还是JSON数组,且如果有vo用到了id\code\name之外的冗余字段,那么在序列化后的值里,这些冗余字段的取值很可能是null。

扩展

  • 对于上面提到的不足之处,我按自己的理解都做了修复
  • 引入了swagger2,自动生成后端接口文档(RESTful)
  • 审批状态回写支持使用jpa
  • 弃审校验增加时间维度校验:引用实体的创建日期大于被引用实体的审批日期,被引用实体不能弃审
  • 提供返回值统一处理,不必再在代码各处都手动创建JsonBackData对象,也增加了接口文档的可读性
  • 新建导入导出类库,提供更多场景下的支持
  • 增加子表非空和子表不能重复两个数据校验注解

Why Spring Boot

  • Spring官方推荐
  • Github上星数达到34.4k(Spring Framework星数26.7k)
  • 外面公司都在用Spring Boot
  • 外面招人都要求会Spring Boot
  • 多年以前,Spring Framework成为web框架事实上的标准;现在,Spring Boot就是web框架事实上的标准
  • 现在,最火的不是Spring Framework,不是Spring Boot,是Spring cloud,而Spring Cloud基于Spring Boot(Spring Boot基于Spring Framework)

上面是讲大环境,下面看看Spring Boot具体的特性:

  • 快速构建Spring应用,很快
  • 进可开箱即用,退可按需配置
  • 提供指标、健康检查等非功能特性
  • 不用生成代码,没有XMl配置

在Spring官网有这样的描述:
Spring

  • Spring Boot构建一切
  • Spring Cloud协调一切
  • Spring Cloud Data Flow 连接一切

结合后端框架现有功能,我重新划分了以下几个模块:
模块划分结构图

整体流程

在审批操作前后,我们系统中都提供了相应的扩展接口,并且在弃审前,也会帮我们自动做数据引用校验,先看看目前的整体流程:
审批流程时序图(改造前)
整体流程没问题,只是有4个小地方我认为可以改进下:

  • bpm是从数据库中查找回调地址(/icopbpm/updateBillState)的,这就要求我们每创建个单据,都要去数据库添加一行几乎相同记录,很麻烦也没必要。
    我认为要么代码中固定为/icopbpm/updateBillState,要么这个回调地址是由前端通过参数传递为bpm(类似cas登陆验证)。
  • IBusinessService的查找是通过Spring监听器,可以改为使用InitializingBean和DisposableBean接口。
  • 弃审前和审批后扩展接口可以由2个合并成1个,另外提供弃审后扩展接口。
  • 扩展接口都是返回JsonBackData对象,以提示操作成功或失败,有点多余。成功继续执行,失败直接抛异常好了。

改造后的整体流程:
审批流程时序图(改造后)

弃审校验

目前的弃审校验流程:
弃审校验时序图(改造前)
我主要是认为没必要请求support,自定义配置可以配置到元数据或配置文件里,多一次请求意味着多一点时间开销,多一次意外情况。另外我们公有云之前有遇到按引用实体的创建时间大于被引用实体的审批时间来做弃审校验,对于这点我做了支持。
改造后的弃审校验流程:
弃审校验时序图(改造后)
还是请求了一次support,因为没有现成的接口可以直接获取元数据信息。

兼容性

在公有云合约系统中,像dr、billState字段都是在公共表里,但现在更新状态都是直接去更新合同表,所以我在各类合同表里也存了份字段。不过如果升级Hibernate到最新版本,又有新的问题:hibernate不会再自动同步更新公共表和实体表,
所以在改造后的实现中,我会依据属性ijz.bill.persistenceType的值(jpa|jdbc)来决定是使用EntityManager还是JdbcTemplate更新表字段。