• 乐优商城


    项目解读

    技术特点

    从上面的数据我们不仅要看到钱,更要看到背后的技术实力。正是得益于电商行业的高强度并发压力,促使了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年可以支持千万用户的使用

    系统架构

    Bz6Uu8.md.png

    系统架构解读

    整个乐优商城可以分为两部分:后台管理系统、前台门户系统。

    • 后台管理:

      • 后台系统主要包含以下功能:
        • 商品管理,包括商品分类、品牌、商品规格等信息的管理
        • 销售管理,包括订单统计、订单退款处理、促销活动生成等
        • 用户管理,包括用户控制、冻结、解锁等
        • 权限管理,整个网站的权限控制,采用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中依然定义了我们所需的一切依赖:

    1530368695265

    我们只需要打开终端,进入项目目录,输入:npm install命令,即可安装这些依赖。

    1530374769782.png

    大概需要几分钟。

    如果安装过程出现以下问题

    1530374827792

    建议删除node_modules目录,重新安装。或者copy其他人的node_modules使用

    运行

    1540706914029

    在package.json文件中有scripts启动脚本配置,可以输入命令:npm run dev或者npm start

    1530374954209

    发现默认的端口是9001。访问:http://localhost:9001

    会自动进行跳转:

    1525958950616

    目录结构

    1525962755237

    调用关系

    1525964023585

    理一下:

    • 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中)

    1543399927909

    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组件是我们的整个页面的布局组件:

    一个典型的三块布局。包含左,上,中三部分:

    1525965779366

    里面使用了Vuetify中的2个组件和一个布局元素:

    • v-navigation-drawer :导航抽屉,主要用于容纳应用程序中的页面的导航链接。

    1532577155616

    跨域问题

    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

    1526460917348

    这是一款带有热加载功能的小型开发服务器。用它来展示你的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
    

    1528480541193

    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中:

    1526643361038

    部分代码截图:

    1526643526973

    首先对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=" 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目录:

    1532757980379

    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;
        }
    }
    

    重启测试:

    发现请求速度得到了极大提升:

    1532758206086

    静态代理后的访问流程

    image-20201114163824157

    阿里短信发送验证码

    java客户端

    我们通过官网提供的帮助来完成java客户端学习:

    1532790118302

    下载SDK工具包

    1527232030334

    下载完成后得到压缩包:

    1532941087871

    解压后目录结构:

    1532941225594

    它这里提供的案例代码比较老,jdk版本也比较低。

    2.2.安装SDK

    我们需要把api_SDK中的两个依赖装入本地maven中,进入api_sdk目录,有两个项目需要处理:

    1532941276340

    然后进入到项目根目录:

    1532941356724

    打开cmd命令行,输入命令:

    mvn install -Dmaven.test.skip=true -Dgpg.skip=true
    

    然后进入另一个项目,上面的操作执行一遍

    2.3.demo

    建议大家直接使用课前资料提供的demo工程:

    1532942155738

    导入到idea中:

    1527235082318

    2.3.1.填写AccessKey

    1527235189991

    这里要填写刚刚申请的AccessKey的id和secret

    2.3.2.填写电话及短信模板

    1527235312969

    这里要修改3个地方:

    • phoneNumber:发送的目标手机
    • signName:签名名称,这个去控制台查看
    • templateCode:模板id,也去控制台查看

    运行main函数测试:

    1527236340771

    短信发送成功了:

    1527236388702

    效果:

    1527236647275

    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

    1533033734163

    GitHub上jwt的java客户端:https://github.com/jwtk/jjwt

    1.4.2.数据格式

    JWT包含三部分数据:

    • Header:头部,通常头部有两部分信息:

      • 声明类型,这里是JWT

      我们会对头部进行base64编码,得到第一部分数据

    • Payload:载荷,就是有效数据,一般包含下面信息:

      • 用户身份信息(注意,这里因为采用base64编码,可解码,因此不要存放敏感信息)
      • 注册声明:如token的签发时间,过期时间,签发人等

      这部分也会采用base64编码,得到第二部分数据

    • Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性

    生成的数据格式:token==个人证件 jwt=个人身份证

    1527322512370

    可以看到分为3段,每段就是上面的一部分数据

    1.4.3.JWT交互流程

    流程图:

    1527305891424

    步骤翻译:

    • 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加密时

    在微服务架构中,我们可以把服务的鉴权操作放到网关中,将未通过鉴权的请求直接拦截,如图:

    1527312464328

    • 1、用户请求登录
    • 2、Zuul将请求转发到授权中心,请求授权
    • 3、授权中心校验完成,颁发JWT凭证
    • 4、客户端请求其它功能,携带JWT
    • 5、Zuul将jwt交给授权中心校验,通过后放行
    • 6、用户请求到达微服务
    • 7、微服务将jwt交给鉴权中心,鉴权同时解析用户信息
    • 8、鉴权中心返回用户数据给微服务
    • 9、微服务处理请求,返回响应

    发现什么问题了?

    每次鉴权都需要访问鉴权中心,系统间的网络请求频率过高,效率略差,鉴权中心的压力较大。

    1.5.2.结合RSA的鉴权

    直接看图:

    1527313765010

    • 我们首先利用RSA生成公钥和私钥。私钥保存在授权中心,公钥保存在Zuul和各个信任的微服务
    • 用户请求登录
    • 授权中心校验,通过后用私钥对JWT进行签名加密
    • 返回jwt给用户
    • 用户携带JWT访问
    • Zuul直接通过公钥解密JWT,进行验证,验证通过则放行
    • 请求到达微服务,微服务直接用公钥解析JWT,获取用户信息,无需访问授权中心

    测试案例

    加入工具类

    2020-11-18 160635

    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("公钥"+'	'+publicKey);
            System.out.println("密钥"+'	'+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());
        }
    }
    
  • 相关阅读:
    极致平台开发技巧介绍1如何利用升级包,快速给客户升级
    如何用极致业务基础平台做一个通用企业ERP系列之三启用期间管理设计
    caca需要用到x11作为图形输出
    spring boot 使用 mybatis 开启事务回滚 的总结
    RabbitMQ --- 直连交换机 【 同步操作,等到消费者处理完后返回处理结果 】
    RabbitMQ --- 直连交换机 【 有回调方法,获取消费结果 】
    RabbitMQ --- 直连交换机 【 无回调方法,不能获取消费结果 】
    spring boot 启动警告 WARN 15684 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : No URLs will be polled as dynamic configuration sources. 解决
    Java基础复习到此结束,统一把源码放到GitHub仓库了,响应开源精神
    用一道题 来 复习 MySQL 的 复杂 sql 语句
  • 原文地址:https://www.cnblogs.com/zgrey/p/14059732.html
Copyright © 2020-2023  润新知