环境搭建与登录
# 环境搭建与登录
# 项目背景
公司从 2019 年开始业务快速扩张,网点数量从 138 家扩展至 540 家,车辆从 170 台增长到 800 台。同时,原有的系统非常简单,比如车辆的调度靠人工操作、所有的货物分拣依靠人员,核心订单数据手动录入,人效非常低。 随着业务不断演进,技术的不断提升,原有运输管理系统已无法满足现有快速扩展下的业务需求,但针对现有系统评估后发现,系统升级成本远高于重新研发。 因此,公司决定基于现有业务体系进行重新构建,打造一套新的 TMS 运输系统,直接更替原有系统。业务侧重于展示车辆调研、线路规划等核心业务流程,操作智能化,大幅度提升人效及管控效率。
# 组织架构

Java 开发人员所在的一级部门为信息中心,主要负责集团新系统的研发、维护、更新迭代。信息中心下设 3 个 2 级部门,产品部、运维部以及开发部门,开发部门总计 42 人,按照以业务线划分为 4 个组、TMS 项目组之外、WMS 项目组、OMS 项目、CRM 组。
TMS (Transportation Management System 运输管理系统) 项目组目前共 8 人,其中前端 3 人,后端 5 人。后端人员根据以下功能模块拆分进行任务分配,实际业务中也不可能是一人包打天下,分工合作才是常态化操作。
# 产品介绍
神领物流系统类似顺丰速运,是向 C 端用户提供快递服务的系统。竞品有:顺丰、中通、圆通、京东快递等。 项目产品主要有 4 端产品:
- 用户端:基于微信小程序开发,外部客户使用,可以寄件、查询物流信息等。
- 快递员端:基于安卓开发的手机 APP,公司内部的快递员使用,可以接收取派件任务等。
- 司机端:基于安卓开发的手机 APP,公司内部的司机使用,可以接收运输任务、上报位置信息等。
- 后台系统管理:基于 vue 开发,PC 端使用,公司内部管理员用户使用,可以进行基础数据维护、订单管理、运单管理等。
# 行业介绍
从广度上来说,物流系统可以理解为由多个子系统组成,这里我们以一般综合型物流系统举例,在整体框架上可以分为仓储系统 WMS、运配系统 TMS、单据系统 OMS 和计费系统 BMS。 这四大系统本质上解决了物流行业的四大核心问题:怎么存放、怎么运送、怎么跟进、怎么结算。 神领物流系统,是 TMS 运配系统,本质上解决的是怎样运送的问题。
物流运输市场目前上最普遍的有四种行业类别:快递、快运、专线、三方。这四种行业支撑着全国商品货物的流通。
- 快递:物流行业外的人对物流的直接反应就是快递,快递只是物流行业的一种形态,得益于电商的发展把快递行业推到大众视野之中。快递的接收群体大多为个人,也称 C 端。快运、专线、三方的发货和接收群体主要为 B 端,主要为企业与企业之间的合作,也有少量个人。
- 快运:快运承运的多是小批量货物,一般为几立方货物或几十公斤货物,如德邦快递、远成快运,运输对象为单个沙发,桌椅等,配送网络为自建和加盟两种方式。
- 专线:专线衔接的货物多数为大宗商品,货物往往依照吨来结算,送货主要送到仓库,工厂,门店。整个配送网络都是社会化网络,由不同的专线进行配合,货物运输需要经过多次中转、集拼。
- 三方:三方不直接从事货物运输,主要通过与工厂签订运输合同,将货物交给专线或者联系车队、司机将货物送到指定地点,属于轻资产运作模式。

# 系统架构和技术架构
# 系统架构

# 技术架构

# 业务需求
下面将演示四端的主要功能,更多的功能具体查看各端的需求文档。
# 功能架构

# 业务功能流程

流程说明:
- 用户在 **【用户端】** 下单后,生成订单
- 系统会根据订单生成 **【取件任务】,快递员上门取件后成功后生成【运单】**
- 用户对订单进行支付,会产生 **【交易单】**
- 快件开始运输,会经历起始营业部、分拣中心、转运中心、分拣中心、终点营业部之间的转运运输,在此期间会有多个 **【运输任务】**
- 到达终点网点后,系统会生成 **【派件任务】**,快递员进行派件作业
- 最后,用户将进行签收或拒收操作
# 用户端
功能演示操作视频列表:
| 下单操作 | 点击查看 (opens new window) |
|---|---|
| 取消订单 | 点击查看 (opens new window) |
| 地址簿 | 点击查看 (opens new window) |

# 快递员端
功能演示操作视频列表:
| 派件操作流程 | 点击查看 (opens new window) |
|---|---|
| 取件操作流程 | 点击查看 (opens new window) |
| 全部取派操作流程 | 点击查看 (opens new window) |
| 搜索操作流程 | 点击查看 (opens new window) |
| 消息操作流程 | 点击查看 (opens new window) |

# 司机端
# 后台管理系统
功能演示操作视频列表:
| 建立机构 | 点击查看 (opens new window) |
|---|---|
| 新建员工 | 点击查看 (opens new window) |
| 绘制作业范围 | 点击查看 (opens new window) |
| 新建线路 | 点击查看 (opens new window) |
| 启用车辆 | 点击查看 (opens new window) |


# 项目环境搭建
# 开发环境说明

说明:
- 加入开发团队后,首先需要从 git 仓库中拉取项目代码
- 拉取到项目代码后,项目会通过 Maven 私服下载所需要的 jar 包
- 开发工程师按照需求文档进行业务开发,在开发完成后将代码推送到 git 仓库
- 代码推送到 git 后,git 会通知 Jenkins(自动化部署系统)
- Jenkins 接收到通知后,拉取到最新的代码,并且将服务部署到服务器
# 配置虚拟机
虚拟机配置了静态 IP 地址为 192.168.150.101,因此需要 VMware 软件的虚拟网卡采用与虚拟机相同的网段。



虚拟机账号: root ,密码: 123321
修改完 vm 网卡后,开机并测试网络
ping baidu.com
# 服务列表
在虚拟机中提供了一些基础服务,具体如下:
| 名称 | 地址 | 用户名 / 密码 | 端口 |
|---|---|---|---|
| git | http://git.sl-express.com/ | sl/sl123 | 10880 |
| maven | http://maven.sl-express.com/nexus/ | admin/admin123 | 8081 |
| jenkins | http://jenkins.sl-express.com/ | root/123 | 8090 |
| 权限管家 | http://auth.sl-express.com/api/authority/static/index.html | admin/123456 | 8764 |
| RabbitMQ | http://rabbitmq.sl-express.com/ | sl/sl321 | 15672 |
| MySQL | - | root/123 | 3306 |
| nacos | http://nacos.sl-express.com/nacos/ | nacos/nacos | 8848 |
| neo4j | http://neo4j.sl-express.com/browser/ | neo4j/neo4j123 | 7474 |
| xxl-job | http://xxl-job.sl-express.com/xxl-job-admin | admin/123456 | 28080 |
| EagleMap | http://eaglemap.sl-express.com/ | eagle/eagle | 8484 |
| seata | http://seata.sl-express.com/ | seata/seata | 7091 |
| Gateway | http://api.sl-express.com/ | - | 9527 |
| admin | http://admin.sl-express.com/ | - | 80 |
| skywalking | http://skywalking.sl-express.com/ | - | 48080 |
| Redis | - | 123321 | 6379 |
| MongoDB | - | sl/123321 | 27017 |
可以看到,每个服务都有对应的域名地址访问,需要在本机配置 hosts 映射才能访问。
# 配置本地 hosts
修改本地的 hosts 文件,或者使用 uTools 中的 hosts 插件
文件路径问 C:\Windows\System32\drivers\etc\hosts
192.168.150.101 git.sl-express.com
192.168.150.101 maven.sl-express.com
192.168.150.101 jenkins.sl-express.com
192.168.150.101 auth.sl-express.com
192.168.150.101 rabbitmq.sl-express.com
192.168.150.101 nacos.sl-express.com
192.168.150.101 neo4j.sl-express.com
192.168.150.101 xxl-job.sl-express.com
192.168.150.101 eaglemap.sl-express.com
192.168.150.101 seata.sl-express.com
192.168.150.101 skywalking.sl-express.com
192.168.150.101 api.sl-express.com
192.168.150.101 admin.sl-express.com
2
3
4
5
6
7
8
9
10
11
12
13
打开浏览器测试:http://git.sl-express.com/

# 环境说明
101 机器中安装了 nginx,通过反向代理功能访问到各种对应的服务

- 通过浏览器按照域名的访问访问,如:http://git.sl-express.com/ ,先通过本地系统的 hosts 文件找到映射的 ip 地址
- 此时,就会访问到虚拟机环境,由于请求没有加端口号,默认访问 80 端口
- 由于在 101 机器部署安装 nginx 服务并且占用的是 80 端口,请求就会进入 nginx
- nginx 会根据不同的域名将请求转发到不同的服务,例如:git.sl-express.com (opens new window) -> 127.0.0.1:18080 通过
vim /usr/local/src/nginx/conf/nginx.conf命令查看 nginx 的配置文件

# 本地配置 Maven 私服
在 101 机器中提供了 maven 私服,需要本地配置 maven(建议版本为 3.6.x)才能使用私服,由于 maven3.7 + 后不允许 http 仓库,所以必须使用低版本,配置文件参考如下:
settings.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings
xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<!-- 本地仓库 这里要改成你自己的本地仓库位置 -->
<localRepository>D:\compile\maven\repository</localRepository>
<!-- 配置私服中deploy的账号 -->
<servers>
<server>
<id>sl-releases</id>
<username>deployment</username>
<password>deployment123</password>
</server>
<server>
<id>sl-snapshots</id>
<username>deployment</username>
<password>deployment123</password>
</server>
</servers>
<!-- 使用阿里云maven镜像,排除私服资源库 -->
<mirrors>
<mirror>
<id>mirror</id>
<mirrorOf>central,jcenter,!sl-releases,!sl-snapshots</mirrorOf>
<name>mirror</name>
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>sl</id>
<!-- 配置项目deploy的地址 -->
<properties>
<altReleaseDeploymentRepository>
sl-releases::default::http://maven.sl-express.com/nexus/content/repositories/releases/
</altReleaseDeploymentRepository>
<altSnapshotDeploymentRepository>
sl-snapshots::default::http://maven.sl-express.com/nexus/content/repositories/snapshots/
</altSnapshotDeploymentRepository>
</properties>
<!-- 配置项目下载依赖的私服地址 -->
<repositories>
<repository>
<id>sl-releases</id>
<url>http://maven.sl-express.com/nexus/content/repositories/releases/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>sl-snapshots</id>
<url>http://maven.sl-express.com/nexus/content/repositories/snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<!-- 激活配置 -->
<activeProfile>sl</activeProfile>
</activeProfiles>
</settings>
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
目前依赖版本
| 服务名 | 版本号 |
|---|---|
| sl-express-parent | 1.4 |
| sl-express-common | 1.2-SNAPSHOT |
| 其他微服务 | 1.1-SNAPSHOT |
# Git 代码管理
# GitFlow 工作流程
在 gitflow 流程中,master 和 develop 分支属于长期分支,长期分支是相对稳定,所有开发完成或测试通过的提交最终都要合并到这两个分支上,他俩也有一些区别:
- master:发布上线时,基于 master 打 tag,基于 tag 进行发布,不允许在该分支上开发,始终保持该分支的稳定。
- develop:开发阶段使用的分支,提交到该分支代码都是相对稳定的,不能直接基于此分支开发,如果开发新的功能,需要基于此分支创建新的分支进行开发功能,待功能开发、测试通过后合并到 develop 分支。

对于新功能的开发,基于 Develop 分支创建 Feature 分支,功能开发完后合并到 Develop 分支,禁止未开发完成的代码合并到 Develop 分支。

在开发中,新功能的代码已经全部合并到 Develop 分支,此时需要基于 Develop 分支创建 Release 分支,在 Release 分支中不再添加新的功能,只是做 bug 的修复,等测试完成 bug 全部修复之后,需要将 Release 分支合并到 Master 分支和 Develop 分支,并且基于 Master 打出版本的 tag。

如果发布到生成环境的版本出现 bug,比如:生产环境的 v1.0 版本出现 bug 需要尽快修复,此时就需要基于 master 创建 hotfix 分支,并且基于 hotfix 分支修复 bug,待 bug 修复完成后需要将代码合并到 master 和 develop 分支。

# 基于 gogs 服务开发
# 拉取代码
拉取 sl-express-gitflow-web (opens new window) 代码,在本地运行起来,目前项目版本为 1.0

拉取完成后,点击【Cancel】按钮:

导入 Module:

启动后访问 http://127.0.0.1:18099/doc.html

测试功能:

# 创建 develop 分支


推送 develop 分支到 gogs:

推送成功:

# 基于 feature 分支开发新功能
开发新的功能需要基于 develop 分支创建 feature 分支,假设我们需要增加一个相乘的接口。


在 com.sl.gitflow.controller.GitFlowController 中新增相乘的方法:
@ApiOperation("两个数相乘")
@GetMapping("/mul")
@ApiImplicitParams({@ApiImplicitParam(name = "value1", value = "第一个数", example = "1"), @ApiImplicitParam(name = "value2", value = "第两个数", example = "2")})
public R<Object> mul(@RequestParam(value = "value1") Long value1, @RequestParam(value = "value2") Long value2) {
return R.success(value1 * value2);
}
2
3
4
5
6
重启、测试:

测试完成后,将代码合并到 develop 分支:
提交代码(此时,在 feature 分支进行提交):

切换到 develop 分支:


推送 develop 分支到 gogs:

推送成功:

推送完成后,一般情况需要将 feature 分支删除掉,不推送到远程仓库。

# 创建 Release 分支
在 develop 分支开发基本上结束后,将基于 develop 分支创建 release 分支,在此分支进行测试,测试完成后合并到 master 和 develop 分支。


创建分支后,模拟测试和 bug 修复,对代码增加注释改动:

提交代码:

推送到远程仓库:

推送成功:

所有测试完成后,将 release 分支合并回 master 和 develop ,并且推送到远程仓库。

# 创建 tag 标签

推送到远程:

推送成功:


# 项目代码列表
在虚拟机中的 gogs 服务中已经提供了项目中所涉及都的项目代码,目前项目拥有 19 个微服务,1 个网关,1 个 parent 工程,2 个公共依赖工程。如下:
# Jenkins
# 持续集成
持续集成是指,开发人员将代码合并到远程仓库后,需要 **【自动】** 的完成构建、部署等操作。 下面以 Spring Boot web 项目举例,说明使用 Jenkins 进行持续集成的过程。

过程说明:
- 本地开发环境推送代码到远程仓库
- 推送完成后,git 服务会向 Jenkins 发送通知
- Jenkins 接收到通知后,开始触发构建(也可以手动触发构建)
- 【git pull】从 git 仓库中拉取最新的代码
- 【maven package】通过 maven 进行打包,Spring Boot 项目会打成可执行的 jar 包
- 【docker build & push】构建 docker 镜像,一般会将 docker 镜像上传到公司内部私服
- 【ssh remote】通过 ssh 命令登录到远程服务器(部署的目标服务器)
- 【docker pull】通过公司内部私服拉取 docker 镜像
- 【docker run】基于拉取到的镜像,运行容器
- 最后,完成构建
# Jenkins 使用
下面以部署【sl-express-gitflow-web (opens new window)】为例,通过 Jenkins 进行部署。 第一步,打开 Jenkins (opens new window),通过 root/123 登录。

第二步,创建构建任务:


第三步,设置任务内容:

设置一些构建参数:


设置版本号参数:(该参数会在后面部署时使用)

设置微服务注册到 nacos 中的 ip 地址:(该参数会在后面部署时使用) SPRING_CLOUD_NACOS_DISCOVERY_IP

设置端口参数:(该参数会在后面部署时使用)

设置服务名称参数:(该参数会在后面部署时使用)

设置 git 仓库信息:

设置分支:


设置构建步骤:

chmod a+rw /var/run/docker.sock

clean package -Dmaven.test.skip=true -U
设置部署脚本:

#!/bin/bash
# 微服务名称
SERVER_NAME=${serverName}
# 服务版本
SERVER_VERSION=${version}
# 服务端口
SERVER_PORT=${port}
# 源jar名称,mvn打包之后,target目录下的jar包名称
JAR_NAME=$SERVER_NAME-$SERVER_VERSION
# jenkins下的目录
JENKINS_HOME=/var/jenkins_home/workspace/$SERVER_NAME
#进入jenkins目录
cd $JENKINS_HOME
# 修改文件权限
chmod 755 target/$JAR_NAME.jar
#输出docker版本
docker -v
echo "---------停止容器($SERVER_NAME)---------"
docker stop $SERVER_NAME
echo "---------删除容器($SERVER_NAME)---------"
docker rm $SERVER_NAME
echo "---------删除镜像($SERVER_NAME:$SERVER_VERSION)---------"
docker rmi $SERVER_NAME:$SERVER_VERSION
echo "---------构建新镜像($SERVER_NAME:$SERVER_VERSION)---------"
docker build -t $SERVER_NAME:$SERVER_VERSION .
echo "---------运行服务---------"
docker run -d -p $SERVER_PORT:8080 --name $SERVER_NAME -e SERVER_PORT=8080 -e SPRING_CLOUD_NACOS_DISCOVERY_IP=${SPRING_CLOUD_NACOS_DISCOVERY_IP} -e SPRING_CLOUD_NACOS_DISCOVERY_PORT=${port} $SERVER_NAME:$SERVER_VERSION
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
开始构建:

构建日志:




测试:http://192.168.150.101:18099/doc.html

可以看到已经部署成功。
# 自动构建
想要实现在代码推送到 git 仓库后自动开始构建,需要分别在 gogs 和 Jenkins 中进行设置。
# gogs 设置
点击 repo【仓库设置】:

添加 web 钩子:


url 格式: http(s)://<< jenkins-server >>/gogs-webhook/?job=<< jobname >>
添加成功:

# Jenkins 设置

# 模拟推送
可以在 gogs 中模拟发起推送通知,来测试是否可以自动构建。

查看执行记录:

# 开发任务
# 任务描述
接下来是你加入到开发一组后接到的第一个任务,具体内容是: 后台管理系统只允许管理员登录,非管理员(司机或快递员)是没有权限登录的,现在的情况是,任何角色的人都能登录到后台管理系统,应该是当非管理员登录时需要提示没有权限。 这个可以算是一个 bug 修复的工作。接下来,你需要思考下,该如何解决这个问题。 解决步骤:
- 先确定鉴权工作是在哪里完成的
- 通过前面的系统架构,可以得知是在网关中完成的
- 拉取到网关的代码
- 阅读鉴权的业务逻辑
- 了解权限系统
- 动手编码解决问题
- 部署,功能测试
# 部署后台管理系统
后台管理系统的部署是使用 101 机器的 Jenkins 部署的,具体参考《前端部署文档 (opens new window)》。部署完成后,就可以看到登录页面。 地址:http://admin.sl-express.com/

页面是可以正常看到,只是没有获取到验证码,是因为验证码的获取是需要后端服务支撑的,目前并没有启动后端服务,需要我们在 Jenkins 中手动启动这几个服务,目前只有 itcast-auth-server 自启动,其几个服务需要手动启动

Jenkins 地址:http://jenkins.sl-express.com/
进入指定的服务中,手动构建

分别启动以下几个服务
sl-express-ms-web-manager
sl-express-ms-base-service
sl-express-gateway
2
3

并在 Nacos 中查看服务是否注册成功
Nacos 地址:http://nacos.sl-express.com/nacos/

此时刷新后台管理系统页面,即可成功看到验证码

使用默认账号,shenlingadmin/123456 即可完成登录

此时我们使用非管理员账号进行测试,例如:gzsj/123456(司机账号) 或 hdkdy001/123456(快递员账号) 进行测试,发现依然是可以登录的

# 拉取代码
在本地创建 sl-express 文件夹,该目录存放项目课程期间所有的代码

登录 git 服务,找到 sl-express-gateway 工程,拷贝地址,在 idea 中拉取代码(注意存储路径)
http://git.sl-express.com/sl/sl-express-gateway.git

拉取到代码后,设置 sl-express 父工程的 jdk 版本为 11

并添加 sl-express-gateway 工程的 pom 文件为 Maven 项目
# 权限管家
在神领物流项目中,快递员、司机、管理人员都是在权限管家中进行管理的,所以他们的登录都是需要对接权限管家完成的。 具体权限管家的介绍说明参见【权限管家使用说明 (opens new window)】,我们将基于权限管家在 sl-express-gateway 中进行测试用户的登录以及对于 token 的校验
引入依赖
<dependency>
<groupId>com.itheima.em.auth</groupId>
<artifactId>itcast-auth-spring-boot-starter</artifactId>
</dependency>
2
3
4
该依赖已经上传到 maven 中央仓库,可以直接下载,地址:https://mvnrepository.com/artifact/com.itheima.em.auth/itcast-auth-spring-boot-starter
在 sl-express-gateway 项目中的 bootstrap-local.yml 配置文件中配置了 nacos 配置中心,一些参数存放到了 nacos 中,这些参数一般都是不同环境不一样配置的,分本地开发环境、生产环境和测试环境。
sl-express-gateway.properties 如下:
#权限系统的配置
authority.host = 192.168.150.101
authority.port = 8764
authority.timeout = 10000
#应用id
authority.applicationId = 981194468570960001
#管理系统角色id
role.manager = 986227712144197857,989278284569131905,996045142395786081,996045927523359809
#快递员角色
role.courier = 989559057641637825
#司机角色
role.driver = 989559028277315009
#生成token的密钥
sl.jwt.user-secret-key = sl
2
3
4
5
6
7
8
9
10
11
12
13
14
其中 applicationId、角色 id 都是需要在权限系统中找到;http://auth.sl-express.com/api/authority/static/index.html#/adhibition

角色 id 需要在数据库表中查询,表为: itcast_auth.itcast_auth_role

# 测试
测试用例在 AuthTemplateTest 中:
@Test
public void testLogin() {
//登录
Result<LoginDTO> result = this.authTemplate.opsForLogin()
.token("zhangsan", "123456");
String token = result.getData().getToken().getToken();
System.out.println("token为:" + token);
UserDTO user = result.getData().getUser();
System.out.println("user信息:" + user);
//查询角色
Result<List<Long>> resultRole = AuthTemplateFactory.get(token).opsForRole()
.findRoleByUserId(user.getId());
System.out.println(resultRole);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
token 校验测试:
@Test
public void checkToken() {
//上面方法中生成的token
String token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMDAyNjIxMzAwOTkwMDc2NzA1IiwiYWNjb3VudCI6InpoYW5nc2FuIiwibmFtZSI6IuW8oOS4iSIsIm9yZ2lkIjoxMDAyNjE5NTU4MzU3NDI1OTUzLCJzdGF0aW9uaWQiOjk4MTIyMzcwMzMzNTQxMDYyNSwiYWRtaW5pc3RyYXRvciI6ZmFsc2UsImV4cCI6MTY1OTEzNDA0MH0.WBZaeBvmuw202raw7JvvHnIMpST28d0gv6ufVDenL_iGQwdClucUfd3YPLg9BLoiosaP16SEuB1nM_-HWl8rUA";
AuthUserInfoDTO authUserInfo = this.tokenCheckService.parserToken(token);
System.out.println(authUserInfo);
System.out.println(JSONUtil.toJsonStr(authUserInfo));
}
2
3
4
5
6
7
8
9
权限管家生成的 token 采用的是 RSA 非对称加密方式,项目中配置的公钥一定要与权限系统中使用的公钥一致,否则会出现无法校验 token 的情况

项目中的公钥文件:

# 阅读鉴权代码
以管理员请求流程为例,其他的流程类似

AuthFilter
该接口定义了 2 个方法,分别是 check() 、 auth() 方法,前者用户校验 token,后者用户鉴权,执行流程是先校验 token 后鉴权
package com.sl.gateway.filter;
import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
/**
* 鉴权业务的回调,具体逻辑由 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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
TokenGatewayFilter
该过滤器是整个校验 / 鉴权流程的具体实现,由于存在不同的终端,导致具体的校验和鉴权逻辑不一样,所以具体的由 AuthFilter 实现类完成。 在向下游服务转发请求时,会携带 2 个头信息,分别是 userInfo 和 token,也就是会将用户信息和 token 传递下去
package com.sl.gateway.filter;
@Slf4j
public class TokenGatewayFilter implements GatewayFilter, Ordered {
private MyConfig myConfig;
private AuthFilter authFilter;
public TokenGatewayFilter(MyConfig myConfig, AuthFilter authFilter) {
this.myConfig = myConfig;
this.authFilter = authFilter;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().toString();
if (StrUtil.startWithAny(path, myConfig.getNoAuthPaths())) {
//无需校验,直接放行
return chain.filter(exchange);
}
//获取header的参数
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StrUtil.isEmpty(token)) {
//没有权限
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//校验token
AuthUserInfoDTO authUserInfoDTO = null;
try{ //捕获token校验异常
authUserInfoDTO = this.authFilter.check(token);
}catch (Exception e){
log.error("权限校验失败,e:",e);
}
if (ObjectUtil.isEmpty(authUserInfoDTO)) {
//token失效 或 伪造
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
//鉴权
Boolean result = false;
try { //捕获鉴权异常
result = this.authFilter.auth(token, authUserInfoDTO, path);
}catch (Exception e){
log.error("鉴权失败,e:",e);
}
if (!result) {
//没有权限
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
//增加参数
exchange.getRequest().mutate().header("userInfo", JSONUtil.toJsonStr(authUserInfoDTO));
exchange.getRequest().mutate().header("token", token);
//校验通过放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
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
管理员校验实现
package com.sl.gateway.filter;
import com.itheima.auth.sdk.common.AuthSdkException;
import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.itheima.auth.sdk.service.TokenCheckService;
import com.sl.gateway.config.MyConfig;
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> implements AuthFilter {
@Resource
private MyConfig myConfig;
@Resource
private TokenCheckService tokenCheckService;
@Override
public GatewayFilter apply(Object config) {
return new TokenGatewayFilter(this.myConfig, this);
}
@Override
public AuthUserInfoDTO check(String token) {
try {
//校验token
return tokenCheckService.parserToken(token);
} catch (AuthSdkException e) {
// 校验失败
}
return null;
}
@Override
public Boolean auth(String token, AuthUserInfoDTO authUserInfoDTO, String path) {
return true;
}
}
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
由于 auth() 方法直接返回 true,导致所有角色都能通过校验,也就是所有角色的用户都能登录到后台管理系统,这里就是 bug 原因的根本所在
# 解决方案
对 auth () 方法进行实现了,从而区分管理员角色的用户通过,而非管理员角色不能通过
package com.sl.gateway.filter;
import cn.hutool.core.collection.CollUtil;
import com.itheima.auth.sdk.AuthTemplate;
import com.itheima.auth.factory.AuthTemplateFactory;
import com.itheima.auth.sdk.common.AuthSdkException;
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 {
@Resource
private MyConfig myConfig;
@Resource
private TokenCheckService tokenCheckService;
// 规则配置储存在nacos中
@Value("${role.manager}")
private List<Long> managerRoles;
@Override
public GatewayFilter apply(Object config) {
return new TokenGatewayFilter(this.myConfig, this);
}
@Override
public AuthUserInfoDTO check(String token) {
try {
//校验token
return tokenCheckService.parserToken(token);
} catch (AuthSdkException e) {
// 校验失败
}
return null;
}
@Override
public Boolean auth(String token, AuthUserInfoDTO authUserInfoDTO, String path) {
//TODO day01 权限认证
// 获取AuthTemplate对象
AuthTemplate authTemplate = AuthTemplateFactory.get(token);
// 查询登陆用户对应角色ids
List<Long> roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfoDTO.getUserId()).getData();
// 和配置的访问角色 取交集
Collection<Long> intersection = CollUtil.intersection(roleIds, this.managerRoles);
// 判断是否有交集即可判断出是否有权限
return CollUtil.isNotEmpty(intersection);
}
}
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
# 测试
测试分两种方法,分别是接口测试和功能测试,我们首先进行接口测试(swagger 接口),然后再进行功能测试。
测试步骤:
- 首先,测试管理员的登录,获取到 token
- 接着测试管理员请求接口资源(期望结果:正常获取到数据)
- 更换成司机用户进行登录,并且测试请求接口资源(期望结果:响应 400,没有权限)
将本地 Gateway 服务启动起来,访问 http://127.0.0.1:9527/manager/doc.html 即可看到【管理后台微服务接口文档】
随便测试个接口,会发现响应 401:

原因是因为没有进行登录就进行功能测试
测试登录接口,需要先获取验证码再进行登录:

记住验证码答案和 key,并切换到登录接口,填写到对应的 key 和 code 参数中,发送请求获取 token

请求结果中获取到 token:

切换到文档管理中的全局参数设置请求头: Authorization ,最好设置完成后刷新一下网页

再进行功能测试

更换成司机账户测试,并替换司机账号的 token:

更换成司机账户后会响应 400,符合我们的预期,测试无误后,可以将代码提交到 git 中,并在 Jenkins 的 sl-express-gateway 重新部署服务

重新到后台网页中测试司机账号,会提示权限不足,无法登录到系统

# 司机端的鉴权
司机端的鉴权和快递员端的鉴权类似,只是角色 id 不同。如果想要通过 App 进行登录测试,请参考前端部署文档 (opens new window)
与管理端类似,只是规则替换为司机端中的规则
DriverTokenGatewayFilterFactory
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.common.AuthSdkException;
import com.itheima.auth.sdk.dto.AuthUserInfoDTO;
import com.itheima.auth.sdk.service.TokenCheckService;
import com.sl.gateway.config.MyConfig;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
public class DriverTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> implements AuthFilter {
@Resource
private MyConfig myConfig;
@Resource
private TokenCheckService tokenCheckService;
@Value("${role.driver}")
private List<Long> driverRoles;
@Override
public GatewayFilter apply(Object config) {
return new TokenGatewayFilter(this.myConfig, this);
}
@Override
public AuthUserInfoDTO check(String token) {
try {
//校验token
return this.tokenCheckService.parserToken(token);
} catch (AuthSdkException e) {
// 校验失败
}
return null;
}
@Override
public Boolean auth(String token, AuthUserInfoDTO authUserInfoDTO, String path) {
//TODO day01 权限认证 作业02
// 获取AuthTemplate对象
AuthTemplate authTemplate = AuthTemplateFactory.get(token);
// 查询登陆用户对应角色ids
List<Long> roleIds = authTemplate.opsForRole().findRoleByUserId(authUserInfoDTO.getUserId()).getData();
// 和配置的访问角色 取交集
Collection<Long> intersection = CollUtil.intersection(roleIds, this.driverRoles);
// 判断是否有交集即可判断出是否有权限
return CollUtil.isNotEmpty(intersection);
}
}
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
# 用户端的鉴权
- 由于用户端没有对接权限管家,所以需要对 token 做解析,可以参考【sl-express-ms-web-customer】项目中生成 token 逻辑的代码进行 token 校验
- 代码补充需要用到常量类:com.sl.gateway.constant.JwtClaimsConstant
- 当完善代码之后发现登录接口需要 code 和 phoneCode,这两个参数是在前端生成的。故可以参考前端部署文档 (opens new window)部署用户端,同时将 baseUrl 网关改为本地,在微信开发者工具中登录。这样点击登录,前端生成 code 和 phoneCode 之后就传到本地了!
- 微信小程序整合登录官方文档:点击查看 (opens new window)
