乐优商城
项目解读
技术特点
从上面的数据我们不仅要看到钱,更要看到背后的技术实力。正是得益于电商行业的高强度并发压力,促使了BAT等巨头们的技术进步。电商行业有些什么特点呢?
- 技术范围广
- 技术新
- 高并发(分布式、静态化技术、缓存技术、异步并发、池化、队列)
- 高可用(集群、负载均衡、限流、降级、熔断)
- 数据量大
- 业务复杂
- 数据安全
常见电商模式
电商行业的一些常见模式:
- B2C:商家对个人,如:亚马逊、当当等
- C2C平台:个人对个人,如:闲鱼、拍拍网、ebay
- B2B平台:商家对商家,如:阿里巴巴、八方资源网等
- O2O:线上和线下结合,如:饿了么、电影票、团购等
- P2P:在线金融,贷款,如:网贷之家、人人聚财等。
- B2C平台:天猫、京东、一号店等
一些专业术语
-
SaaS:软件即服务
-
SOA:面向服务
-
RPC:远程过程调用
-
RMI:远程方法调用
-
PV:(page view),即页面浏览量;
用户每1次对网站中的每个网页访问均被记录1次。用户对同一页面的多次访问,访问量累计
-
UV:(unique visitor),独立访客
指访问某个站点或点击某条新闻的不同IP地址的人数。在同一天内,uv只记录第一次进入网站的具有独立IP的访问者,在同一天内再次访问该网站则不计数。
-
PV与带宽:
- 计算带宽大小需要关注两个指标:峰值流量和页面的平均大小。
- 计算公式是:网站带宽= ( PV * 平均页面大小(单位MB)* 8 )/统计时间(换算到秒)
- 为什么要乘以8?
- 网站大小为单位是字节(Byte),而计算带宽的单位是bit,1Byte=8bit
- 这个计算的是平均带宽,高峰期还需要扩大一定倍数
-
PV、QPS、并发
-
QPS:每秒处理的请求数量。
- 比如你的程序处理一个请求平均需要0.1S,那么1秒就可以处理10个请求。QPS自然就是10,多线程情况下,这个数字可能就会有所增加。
-
由PV和QPS如何需要部署的服务器数量?
- 根据二八原则,80%的请求集中在20%的时间来计算峰值压力:
- (每日PV * 80%) / (3600s * 24 * 20%) * 每个页面的请求数 = 每个页面每秒的请求数量
- 然后除以服务器的QPS值,即可计算得出需要部署的服务器数量
-
项目介绍
- 乐优商城是一个全品类的电商购物网站(B2C)。
- 用户可以在线购买商品、加入购物车、下单
- 可以评论已购买商品
- 管理员可以在后台管理商品的上下架、促销活动
- 管理员可以监控商品销售状况
- 客服可以在后台处理退款操作
- 希望未来3到5年可以支持千万用户的使用
系统架构
系统架构解读
整个乐优商城可以分为两部分:后台管理系统、前台门户系统。
-
后台管理:
- 后台系统主要包含以下功能:
- 商品管理,包括商品分类、品牌、商品规格等信息的管理
- 销售管理,包括订单统计、订单退款处理、促销活动生成等
- 用户管理,包括用户控制、冻结、解锁等
- 权限管理,整个网站的权限控制,采用JWT鉴权方案,对用户及API进行权限控制
- 统计,各种数据的统计分析展示
- 后台系统主要包含以下功能:
后台系统会采用前后端分离开发,而且整个后台管理系统会使用Vue.js框架搭建出单页应用(SPA)。
前台门户
- 前台门户面向的是客户,包含与客户交互的一切功能。例如:
- 搜索商品
- 加入购物车
- 下单
- 评价商品等等
- 前台系统我们会使用Thymeleaf模板引擎技术来完成页面开发。出于SEO优化的考虑,我们将不采用单页应用。
无论是前台还是后台系统,都共享相同的微服务集群,包括:
- 商品微服务:商品及商品分类、品牌、库存等的服务
- 搜索微服务:实现搜索功能
- 订单微服务:实现订单相关
- 购物车微服务:实现购物车相关功能
- 用户中心:用户的登录注册等功能
- Eureka注册中心
- Zuul网关服务
- ...
技术选型
前端:
- 基础的HTML、CSS、JavaScript(基于ES6标准)
- JQuery
- Vue.js 2.0以及基于Vue的框架:Vuetify(UI框架)
- 前端构建工具:WebPack
- 前端安装包工具:NPM
- Vue脚手架:Vue-cli
- Vue路由:vue-router
- ajax框架:axios
- 基于Vue的富文本框架:quill-editor
后端技术:
- 基础的SpringMVC、Spring 5.x和MyBatis3
- Spring Boot 2.0.7版本
- Spring Cloud Finchley.SR2
- Redis-4.0 缓存数据
- RabbitMQ-3.4 消息队列
- Elasticsearch-6.3 分布式全文搜索引擎
- nginx-1.14.2 反向代理
- FastDFS - 5.0.8 分布式文件上传
- MyCat
- Thymeleaf 模板引擎
- mysql 5.6
为了保证开发环境的统一,希望每个人都按照我的环境来配置:
- IDE:我们使用Idea 2020.1 版本
- JDK:统一使用JDK1.8
- 项目构建:maven3.3.9以上版本即可(3.5.2)
- 版本控制工具:git
域名
我们在开发的过程中,为了保证以后的生产、测试环境统一。尽量都采用域名来访问项目。
一级域名:www.leyou.com,leyou.com leyou.cn
二级域名:manage.leyou.com/item , api.leyou.com
我们可以通过switchhost工具来修改自己的host对应的地址,只要把这些域名指向127.0.0.1,那么跟你用localhost的效果是完全一样的。
搭建后台管理前端
安装依赖
你应该注意到,这里并没有node_modules文件夹,方便给大家下发,已经把依赖都删除了。不过package.json中依然定义了我们所需的一切依赖:
我们只需要打开终端,进入项目目录,输入:npm install
命令,即可安装这些依赖。
大概需要几分钟。
如果安装过程出现以下问题:
建议删除node_modules目录,重新安装。或者copy其他人的node_modules使用
运行
在package.json文件中有scripts启动脚本配置,可以输入命令:npm run dev
或者npm start
发现默认的端口是9001。访问:http://localhost:9001
会自动进行跳转:
目录结构
调用关系
理一下:
- index.html:html模板文件。定义了空的
div
,其id为app
。 - main.js:实例化vue对象,并且通过id选择器绑定到index.html的div中,因此main.js的内容都将在index.html的div中显示。main.js中使用了App组件,即App.vue,也就是说index.html中最终展现的是App.vue中的内容。index.html引用它之后,就拥有了vue的内容(包括组件、样式等),所以,main.js也是webpack打包的入口。
- index.js:定义请求路径和组件的映射关系。相当于之前的
<vue-router>
- App.vue中也没有内容,而是定义了vue-router的锚点:
<router-view>
,我们之前讲过,vue-router路由后的组件将会在锚点展示。 - 最终结论:一切路由后的内容都将通过App.vue在index.html中显示。
- 访问流程:用户在浏览器输入路径,例如:http://localhost:9001/#/item/brand --> index.js(/item/brand路径对应pages/item/Brand.vue组件) --> 该组件显示在App.vue的锚点位置 --> main.js使用了App.vue组件,并把该组件渲染在index.html文件中(id为“app”的div中)
UI框架
Vue虽然会帮我们进行视图的渲染,但样式还是由我们自己来完成。这显然不是我们的强项,因此后端开发人员一般都喜欢使用一些现成的UI组件,拿来即用,常见的例如:
- BootStrap
- LayUI
- EasyUI
- ZUI
然而这些UI组件的基因天生与Vue不合,因为他们更多的是利用DOM操作,借助于jQuery实现,而不是MVVM的思想。
而目前与Vue吻合的UI框架也非常的多,国内比较知名的如:
- element-ui:饿了么出品
- i-view:某公司出品
然而我们都不用,我们今天推荐的是一款国外的框架:Vuetify
官方网站:https://vuetifyjs.com/zh-Hans/
项目页面布局
接下来我们一起看下页面布局。
Layout组件是我们的整个页面的布局组件:
一个典型的三块布局。包含左,上,中三部分:
里面使用了Vuetify中的2个组件和一个布局元素:
v-navigation-drawer
:导航抽屉,主要用于容纳应用程序中的页面的导航链接。
跨域问题
https://www.cnblogs.com/zgrey/p/13972270.html
文件上传 fastDFS
https://www.cnblogs.com/zgrey/p/13972567.html
搭建前台页面
3.2.live-server
没有webpack,我们就无法使用webpack-dev-server运行这个项目,实现热部署。
所以,这里我们使用另外一种热部署方式:live-server,
3.2.1.简介
地址;https://www.npmjs.com/package/live-server
这是一款带有热加载功能的小型开发服务器。用它来展示你的HTML / JavaScript / CSS,但不能用于部署最终的网站。
3.2.2.安装和运行参数
安装,使用npm命令即可,这里建议全局安装,以后任意位置可用
npm install -g live-server
运行时,直接输入命令:
live-server
另外,你可以在运行命令后,跟上一些参数以配置:
--port=NUMBER
- 选择要使用的端口,默认值:PORT env var或8080--host=ADDRESS
- 选择要绑定的主机地址,默认值:IP env var或0.0.0.0(“任意地址”)--no-browser
- 禁止自动Web浏览器启动--browser=BROWSER
- 指定使用浏览器而不是系统默认值--quiet | -q
- 禁止记录--verbose | -V
- 更多日志记录(记录所有请求,显示所有侦听的IPv4接口等)--open=PATH
- 启动浏览器到PATH而不是服务器root--watch=PATH
- 用逗号分隔的路径来专门监视变化(默认值:观看所有内容)--ignore=PATH
- 要忽略的逗号分隔的路径字符串(anymatch -compatible definition)--ignorePattern=RGXP
-文件的正则表达式忽略(即.*\.jade
)(不推荐使用赞成--ignore
)--middleware=PATH
- 导出要添加的中间件功能的.js文件的路径; 可以是没有路径的名称,也可以是引用middleware
文件夹中捆绑的中间件的扩展名--entry-file=PATH
- 提供此文件(服务器根目录)代替丢失的文件(对单页应用程序有用)--mount=ROUTE:PATH
- 在定义的路线下提供路径内容(可能有多个定义)--spa
- 将请求从/ abc转换为/#/ abc(方便单页应用)--wait=MILLISECONDS
- (默认100ms)等待所有更改,然后重新加载--htpasswd=PATH
- 启用期待位于PATH的htpasswd文件的http-auth--cors
- 为任何来源启用CORS(反映请求源,支持凭证的请求)--https=PATH
- 到HTTPS配置模块的路径--proxy=ROUTE:URL
- 代理ROUTE到URL的所有请求--help | -h
- 显示简洁的使用提示并退出--version | -v
- 显示版本并退出
3.2.3.测试
我们进入leyou-portal目录,输入命令:
live-server --port=9002
3.3.域名访问
现在我们访问只能通过:http://127.0.0.1:9002
我们希望用域名访问:http://www.leyou.com
第一步,修改hosts文件,添加一行配置:
127.0.0.1 www.leyou.com
第二步,修改nginx配置,将www.leyou.com反向代理到127.0.0.1:9002
server {
listen 80;
server_name www.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://127.0.0.1:9002;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
重新加载nginx配置:nginx.exe -s reload
3.4.common.js
为了方便后续的开发,我们在前台系统中定义了一些工具,放在了common.js中:
部分代码截图:
首先对axios进行了一些全局配置,请求超时时间,请求的基础路径,是否允许跨域操作cookie等
定义了对象 ly ,也叫leyou,包含了下面的属性:
- getUrlParam(key):获取url路径中的参数
- http:axios对象的别名。以后发起ajax请求,可以用ly.http.get()
- store:localstorage便捷操作,后面用到再详细说明
- formatPrice:格式化价格,如果传入的是字符串,则扩大100被并转为数字,如果传入是数字,则缩小100倍并转为字符串
- formatDate(val, pattern):对日期对象val按照指定的pattern模板进行格式化
- stringify:将对象转为参数字符串
- parse:将参数字符串变为js对象
vue-validate
表单验证
乐优商城采用vue-validate
实现表单的验证
项目中的使用示例
<form class="sui-form form-horizontal">
<div class="control-group">
<label class="control-label">用户名:</label>
<div class="controls">
<input type="text" placeholder="请输入你的用户名" class="input-xfat input-xlarge"
v-model.lazy="user.username" name="username" data-vv-as="用户名"
v-validate="'required|alpha_dash|min:4|max:30|useful:1'">
</div>
<span style="color: red;">{{ errors.first('username') }}</span>
</div>
<div class="control-group">
<label class="control-label">登录密码:</label>
<div class="controls">
<input type="password" placeholder="设置登录密码" class="input-xfat input-xlarge"
v-model="user.password" name="password" data-vv-as="密码"
v-validate="'required|alpha_dash|min:4|max:30'">
</div>
<span style="color: red;">{{ errors.first('password') }}</span>
</div>
<div class="control-group">
<label class="control-label">确认密码:</label>
<div class="controls">
<input type="password" placeholder="再次确认密码" class="input-xfat input-xlarge"
v-model="user.confirmPassword" name="confirmPass" data-vv-as="确认密码"
v-validate="{required:true,confirm:user.password}">
</div>
<span style="color: red;">{{ errors.first('confirmPass') }}</span>
</div>
<div class="control-group">
<label class="control-label">手机号:</label>
<div class="controls">
<input type="text" placeholder="请输入你的手机号" class="input-xfat input-xlarge"
v-model="user.phone" name="phone" data-vv-as="手机号"
v-validate="{required:true,regex:/^1[35678]\d{9}$/,useful:2}">
</div>
<span style="color: red;">{{ errors.first('phone') }}</span>
</div>
<div class="control-group">
<label class="control-label">短信验证码:</label>
<div class="controls">
<input type="text" placeholder="短信验证码" class="input-xfat input-xlarge" style="width: 120px;"
v-model="user.code" name="code" v-validate="'required'" data-vv-as="验证码">
<span class="code-span" @click="createVerifyCode">
获取短信验证码
</span>
</div>
<span style="color: red;">{{ errors.first('code') }}</span>
</div>
- 验证
<script src="./js/validate.js"></script>
Vue.use(VeeValidate, {
events: 'blur',
dictionary: {
zh: {
messages: {
required: (field) => field + '不能为空!',
min: (field, args) => field + '长度不能小于' + args[0],
max: (field, args) => field + '长度不能大于' + args[0],
alpha_dash: (field) => field + '只能包含数字、字母或下划线',
regex: (field) => field + "格式不正确",
is: () => "两次密码不一致"
}
}
},
locale: 'zh'
});
关于 vue-validate
的使用方法可以参考 这篇博客 https://www.jianshu.com/p/2a456e31c581
官网 https://vee-validate.logaretm.com/v2/api/directive.html#directive-args
ElasticSearch
https://www.cnblogs.com/zgrey/p/13973528.html#autoid-1-21-0
页面静态化
2.1.简介
2.1.1.问题分析
现在,我们的页面是通过Thymeleaf模板引擎渲染后返回到客户端。在后台需要大量的数据查询,而后渲染得到HTML页面。会对数据库造成压力,并且请求的响应时间过长,并发能力不高。
大家能想到什么办法来解决这个问题?
首先我们能想到的就是缓存技术,比如之前学习过的Redis。不过Redis适合数据规模比较小的情况。假如数据量比较大,例如我们的商品详情页。每个页面如果10kb,100万商品,就是10GB空间,对内存占用比较大。此时就给缓存系统带来极大压力,如果缓存崩溃,接下来倒霉的就是数据库了。
所以缓存并不是万能的,某些场景需要其它技术来解决,比如静态化。
2.1.2.什么是静态化
静态化是指把动态生成的HTML页面变为静态内容保存,以后用户的请求到来,直接访问静态页面,不再经过服务的渲染。
而静态的HTML页面可以部署在nginx中,从而大大提高并发能力,减小tomcat压力。
2.1.3.如何实现静态化
目前,静态化页面都是通过模板引擎来生成,而后保存到nginx服务器来部署。常用的模板引擎比如:
- Freemarker
- Velocity
- Thymeleaf
我们之前就使用的Thymeleaf,来渲染html返回给用户。Thymeleaf除了可以把渲染结果写入Response,也可以写到本地文件,从而实现静态化。
2.2.Thymeleaf实现静态化
2.2.1.概念
先说下Thymeleaf中的几个概念:
- Context:运行上下文
- TemplateResolver:模板解析器
- TemplateEngine:模板引擎
Context
上下文: 用来保存模型数据,当模板引擎渲染时,可以从Context上下文中获取数据用于渲染。
当与SpringBoot结合使用时,我们放入Model的数据就会被处理到Context,作为模板渲染的数据使用。
TemplateResolver
模板解析器:用来读取模板相关的配置,例如:模板存放的位置信息,模板文件名称,模板文件的类型等等。
当与SpringBoot结合时,TemplateResolver已经由其创建完成,并且各种配置也都有默认值,比如模板存放位置,其默认值就是:templates。比如模板文件类型,其默认值就是html。
TemplateEngine
模板引擎:用来解析模板的引擎,需要使用到上下文、模板解析器。分别从两者中获取模板中需要的数据,模板文件。然后利用内置的语法规则解析,从而输出解析后的文件。来看下模板引擎进行处理的函数:
templateEngine.process("模板名", context, writer);
三个参数:
- 模板名称
- 上下文:里面包含模型数据
- writer:输出目的地的流
在输出时,我们可以指定输出的目的地,如果目的地是Response的流,那就是网络响应。如果目的地是本地文件,那就实现静态化了。
而在SpringBoot中已经自动配置了模板引擎,因此我们不需要关心这个。现在我们做静态化,就是把输出的目的地改成本地文件即可!
2.2.2.具体实现
Service代码:
@Service
public class GoodsHtmlService {
@Autowired
private GoodsService goodsService;
@Autowired
private TemplateEngine templateEngine;
private static final Logger LOGGER = LoggerFactory.getLogger(GoodsHtmlService.class);
/**
* 创建html页面
*
* @param spuId
* @throws Exception
*/
public void createHtml(Long spuId) {
PrintWriter writer = null;
try {
// 获取页面数据
Map<String, Object> spuMap = this.goodsService.loadModel(spuId);
// 创建thymeleaf上下文对象
Context context = new Context();
// 把数据放入上下文对象
context.setVariables(spuMap);
// 创建输出流
File file = new File("C:\\project\\nginx-1.14.0\\html\\item\\" + spuId + ".html");
writer = new PrintWriter(file);
// 执行页面静态化方法
templateEngine.process("item", context, writer);
} catch (Exception e) {
LOGGER.error("页面静态化出错:{},"+ e, spuId);
} finally {
if (writer != null) {
writer.close();
}
}
}
}
2.2.3.什么时候创建静态文件
我们编写好了创建静态文件的service,那么问题来了:什么时候去调用它呢
想想这样的场景:
假如大部分的商品都有了静态页面。那么用户的请求都会被nginx拦截下来,根本不会到达我们的leyou-goods-web
服务。只有那些还没有页面的请求,才可能会到达这里。
因此,如果请求到达了这里,我们除了返回页面视图外,还应该创建一个静态页面,那么下次就不会再来麻烦我们了。
所以,我们在GoodsController中添加逻辑,去生成静态html文件:
@GetMapping("{id}.html")
public String toItemPage(@PathVariable("id")Long id, Model model){
// 加载所需的数据
Map<String, Object> map = this.goodsService.loadModel(id);
// 把数据放入数据模型
model.addAllAttributes(map);
// 页面静态化
this.goodsHtmlService.asyncExcute(id);
return "item";
}
注意:生成html 的代码不能对用户请求产生影响,所以这里我们使用额外的线程进行异步创建。
2.2.4.重启测试
访问一个商品详情,然后查看nginx目录:
2.3.nginx代理静态页面
接下来,我们修改nginx,让它对商品请求进行监听,指向本地静态页面,如果本地没找到,才进行反向代理:
server {
listen 80;
server_name www.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /item {
# 先找本地
root html;
if (!-f $request_filename) { #请求的文件不存在,就反向代理
proxy_pass http://127.0.0.1:8084;
break;
}
}
location / {
proxy_pass http://127.0.0.1:9002;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
重启测试:
发现请求速度得到了极大提升:
静态代理后的访问流程
阿里短信发送验证码
java客户端
我们通过官网提供的帮助来完成java客户端学习:
下载SDK工具包
下载完成后得到压缩包:
解压后目录结构:
它这里提供的案例代码比较老,jdk版本也比较低。
2.2.安装SDK
我们需要把api_SDK中的两个依赖装入本地maven中,进入api_sdk目录,有两个项目需要处理:
然后进入到项目根目录:
打开cmd命令行,输入命令:
mvn install -Dmaven.test.skip=true -Dgpg.skip=true
然后进入另一个项目,上面的操作执行一遍
2.3.demo
建议大家直接使用课前资料提供的demo工程:
导入到idea中:
2.3.1.填写AccessKey
这里要填写刚刚申请的AccessKey的id和secret
2.3.2.填写电话及短信模板
这里要修改3个地方:
- phoneNumber:发送的目标手机
- signName:签名名称,这个去控制台查看
- templateCode:模板id,也去控制台查看
运行main函数测试:
短信发送成功了:
效果:
JWT 实现无状态登录
1.无状态登录原理
1.1.什么是有状态?
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,无法进行水平扩展
- 客户端请求依赖服务端,多次请求必须访问同一台服务器
1.2.什么是无状态
微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
带来的好处是什么呢?
- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
- 服务端的集群和状态对客户端透明
- 服务端可以任意的迁移和伸缩
- 减小服务端存储压力
1.3.如何实现无状态
无状态登录的流程:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务的对token进行解密,判断是否有效。
流程图:
整个登录过程中,最关键的点是什么?
token的安全性
token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。
采用何种方式加密才是安全可靠的呢?
我们将采用JWT + RSA非对称加密
1.4.JWT
1.4.1.简介
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io
GitHub上jwt的java客户端:https://github.com/jwtk/jjwt
1.4.2.数据格式
JWT包含三部分数据:
-
Header:头部,通常头部有两部分信息:
- 声明类型,这里是JWT
我们会对头部进行base64编码,得到第一部分数据
-
Payload:载荷,就是有效数据,一般包含下面信息:
- 用户身份信息(注意,这里因为采用base64编码,可解码,因此不要存放敏感信息)
- 注册声明:如token的签发时间,过期时间,签发人等
这部分也会采用base64编码,得到第二部分数据
-
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
生成的数据格式:token==个人证件 jwt=个人身份证
可以看到分为3段,每段就是上面的一部分数据
1.4.3.JWT交互流程
流程图:
步骤翻译:
- 1、用户登录
- 2、服务的认证,通过后根据secret生成token
- 3、将生成的token返回给浏览器
- 4、用户每次请求携带token
- 5、服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
- 6、处理请求,返回响应结果
因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。
1.4.4.非对称加密
加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:
- 对称加密,如AES
- 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
- 优势:算法公开、计算量小、加密速度快、加密效率高
- 缺陷:双方都使用同样密钥,安全性得不到保证
- 非对称加密,如RSA
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 私钥加密,持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
- 优点:安全,难以破解
- 缺点:算法比较耗时
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 不可逆加密,如MD5,SHA
- 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。
RSA算法历史:
1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA
1.5.结合Zuul的鉴权流程
我们逐步演进系统架构设计。需要注意的是:secret是签名的关键,因此一定要保密,我们放到鉴权中心保存,其它任何服务中都不能获取secret。
1.5.1.没有RSA加密时
在微服务架构中,我们可以把服务的鉴权操作放到网关中,将未通过鉴权的请求直接拦截,如图:
- 1、用户请求登录
- 2、Zuul将请求转发到授权中心,请求授权
- 3、授权中心校验完成,颁发JWT凭证
- 4、客户端请求其它功能,携带JWT
- 5、Zuul将jwt交给授权中心校验,通过后放行
- 6、用户请求到达微服务
- 7、微服务将jwt交给鉴权中心,鉴权同时解析用户信息
- 8、鉴权中心返回用户数据给微服务
- 9、微服务处理请求,返回响应
发现什么问题了?
每次鉴权都需要访问鉴权中心,系统间的网络请求频率过高,效率略差,鉴权中心的压力较大。
1.5.2.结合RSA的鉴权
直接看图:
- 我们首先利用RSA生成公钥和私钥。私钥保存在授权中心,公钥保存在Zuul和各个信任的微服务
- 用户请求登录
- 授权中心校验,通过后用私钥对JWT进行签名加密
- 返回jwt给用户
- 用户携带JWT访问
- Zuul直接通过公钥解密JWT,进行验证,验证通过则放行
- 请求到达微服务,微服务直接用公钥解析JWT,获取用户信息,无需访问授权中心
测试案例
加入工具类
public class JwtTest {
private static final String pubKeyPath = "G:\\leyou\\rsa.pub";
private static final String priKeyPath = "G:\\leyou\\rsa.pri";
private PublicKey publicKey;
private PrivateKey privateKey;
/* @Test
public void testRsa() throws Exception {
RsaUtils.generateKey(pubKeyPath, priKeyPath, "ffasjfldsf%……kdaf()");
}*/
@Before
public void testGetRsa() throws Exception {
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
}
@Test
public void testGetKey(){
System.out.println("公钥"+'\t'+publicKey);
System.out.println("密钥"+'\t'+privateKey);
}
@Test
public void testGenerateToken() throws Exception {
// 生成token
String token = JwtUtils.generateToken(new UserInfo(20L, "jack"), privateKey, 5);
System.out.println("token = " + token);
}
@Test
public void testParseToken() throws Exception {
String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6MjAsInVzZXJuYW1lIjoiamFjayIsImV4cCI6MTYwNTY4NTcwMH0.EcGuaw2YYD-sagSBsCmEdZJRmHJ-5hW67meqrZJ__gU0ejpfYJMvqAaNkQmqSpu3i0RfHKVCx0M_mKXwsZfJLlUy6uD39VKxIffVFIbYz0AomdWfH0i3EkXMQ5bR97ptOmtLP5wp6tSjFeQaPQY1GDttujrziqrQ4j1z9baPuJo";
// 解析token
UserInfo user = JwtUtils.getInfoFromToken(token, publicKey);
System.out.println("id: " + user.getId());
System.out.println("userName: " + user.getUsername());
}
}