springboot聚合工程笔记

一、《锋迷商城》项目介绍

1.1 项目背景

锋迷商城——电商平台

  • B2C 商家对客户
  • C2B2C 客户对商家对客户
1.1.1 B2C

平台运营方即商品的卖家 小米商城

  • 商品
  • 用户
1.1.2 C2B2C

平台运营方不卖商品(也可以卖)

卖家是平台的用户

买家也是平台用户

  • 用户(店铺)
  • 用户(买家)
  • 服务
  • 商品
1.1.3 Java

Java语言的应用领域很广,但主要应用于web领域的项目开发,web项目类型分为两类:

  • 企业级开发 (供企业内部使用的系统:企业内部的管理系统CRM\ERP、学校的教务管理系统)
  • 互联网开发(提供给所有互联网用户使用的系统——用户量)—— 电商
1.2 项目功能

https://www.processon.com/view/link/606bde8b1e08534321fd2103

1.3 技术选型

SSM 企业开发框架 基础的开发技术

1.3.1 单体项目

项目的页面和代码都在同一个项目,项目开发完成之后直接部署在一台服务器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-baF4hpO7-1622295230327)(imgs/1618025221720.png)]

单体项目遇到的问题:用户对页面静态资源以及对Java代码的访问压力都会落在Tomcat服务器上。

1.3.2 技术清单
  • 项目架构:前后端分离
  • 前端技术:vue、axios、妹子UI、layui、bootstrap
  • 后端技术:SpringBoot+MyBatis、RESTful、swagger
  • 服务器搭建:Linux、Nginx

二、项目架构的演进

2.1 单体架构
  • 前后端都部署在同一台服务器上(前后端代码都在同一个应用中)
  • 缺点:对静态资源的访问压力也会落在Tomcat上
2.2 前后端分离

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Jr9QKob-1622295230330)(imgs/1618036199285.png)]

  • 前后端分离:前端和后端分离开发和部署(前后端部署在不同的服务器)
  • 优点:将对静态资源的访问和对接口的访问进行分离,Tomcat服务器只负责数据服务的访问
2.3 集群与负载均衡

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R6SH2l7G-1622295230333)(imgs/1618037366380.png)]

  • 优点:提供并发能力、可用性
2.4 分布式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nRJwERLs-1622295230335)(imgs/1618038441781.png)]

  • 基于redis实现 分布式锁
  • 分布式数据库mycat
  • redis集群
  • 数据库中间件
  • 消息中间件
2.5 微服务架构
  • 微服务架构:将原来在一个应用中开发的多个模块进行拆分,单独开发和部署
  • 保证可用性、性能

三、《锋迷商城》项目搭建

基于Maven的聚合工程完成项目搭建,前端采用vue+axios,后端使用SpringBoot整合SSM

3.1 技术储备
  • (√)SpringBoot: 实现无配置的SSM整合
  • (√)Maven聚合工程:实现模块的复用
3.2 创建Maven聚合工程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aBfvBE8W-1622295230338)(imgs/1618363925912.png)]

3.2.1 构建父工程fmmall
  • 创建一个maven工程、packing设置为 pom

  • 父工程继承继承spring-boot-starter-parent

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <!-- spring-boot-starter-parent -->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.4</version>
            <relativePath/> 
        </parent>
    
        <groupId>com.qfedu</groupId>
        <artifactId>fmmall</artifactId>
        <version>2.0.1</version>
    
        <packaging>pom</packaging>
    
    </project>
    
3.2.2 创建common工程
  • 选择fmmall,右键—New—Module (Maven工程)

  • 修改common的pom.xml,设置packing=jar

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>fmmall</artifactId>
            <groupId>com.qfedu</groupId>
            <version>2.0.1</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>common</artifactId>
        <packaging>jar</packaging>
    
    </project>
    
3.2.3 创建beans工程
  • 选择fmmall,右键—New—Module (Maven工程)
  • 修改beans的pom.xml,设置packing ----- jar
3.2.4 创建mapper工程
  • 选择fmmall,右键—New—Module (Maven工程)

  • 修改mapper的pom.xml,设置packing ----- jar

  • 在mapper的pom.xml,依赖beans

    <dependency>
        <groupId>com.qfedu</groupId>
        <artifactId>beans</artifactId>
        <version>2.0.1</version>
    </dependency>
    
3.2.5 创建service工程
  • 选择fmmall,右键—New—Module (Maven工程)

  • 修改service的pom.xml,设置packing ----- jar

  • 在service的pom.xml,依赖mapper、commom

    <dependency>
        <groupId>com.qfedu</groupId>
        <artifactId>mapper</artifactId>
        <version>2.0.1</version>
    </dependency>
    <dependency>
        <groupId>com.qfedu</groupId>
        <artifactId>common</artifactId>
        <version>2.0.1</version>
    </dependency>
    
3.2.6 创建api工程
  • 选择fmmall,右键—New—Module (SpringBoot工程)

  • 修改api的pom.xml,继承fmmall,删除自己的groupId 和 version

    <parent>
        <groupId>com.qfedu</groupId>
        <artifactId>fmmall</artifactId>
        <version>2.0.1</version>
    </parent>
    
  • 将spring boot的依赖配置到父工程fmmall的pom.xml

  • 在父工程fmmall的pom.xml的modules添加api

    <!--fmmall  pom.xml-->
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.4</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
    
        <groupId>com.qfedu</groupId>
        <artifactId>fmmall</artifactId>
        <version>2.0.1</version>
        <modules>
            <module>common</module>
            <module>beans</module>
            <module>mapper</module>
            <module>service</module>
            <module>api</module>
        </modules>
        <packaging>pom</packaging>
    
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
    
    </project>
    
  • 在api中,依赖service

    <dependency>
        <groupId>com.qfedu</groupId>
        <artifactId>service</artifactId>
        <version>2.0.1</version>
    </dependency>
    
  • api的pom.xml继承fmmall

3.3 Maven聚合工程依赖分析

如果将依赖添加到父工程的pom中,根据依赖的继承关系,所有的子工程中都会继承父工程的依赖:

  • 好处:当有多个子工程都需要相同的依赖时,无需在子工程中重复添加依赖

  • 缺点:如果某些子工程不需要这个依赖,还是会被强行继承

如果在父工程中没有添加统一依赖,则每个子工程所需的依赖需要在子工程的pom中自行添加

如果存在多个子工程需要添加相同的依赖,则需在父工程pom进行依赖版本的管理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8kbkJEl-1622295230341)(imgs/1618365559362.png)]

依赖配置说明

  1. 在父工程的pom文件中一次性添加各个子工程所需的所有依赖
  2. 在各个子工程中单独添加当前子工程的依赖
3.4 整合MyBatis
3.4.1 common子工程
  • lombok
3.4.2 beans子工程
  • lombok
3.4.3 MyBatis整合
  • mapper子工程的pom文件,新增mybatis所需的依赖

    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    
    <!--spring-boot-starter-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.4.4</version>
    </dependency>
    
    <!--mybatis starter-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>
    
  • mapper子工程resources目录创建application.yml

    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db_2010_mybatis?characterEncoding=utf-8
        username: root
        password: admin123
    mybatis:
      mapper-locations: classpath:mappers/*Mapper.xml
      type-aliases-package: com.qfedu.fmmall.entity
    
  • api子工程的启动类通过@MpperScan声明dao包的路径

    @SpringBootApplication
    @MapperScan("com.qfedu.fmmall.dao")
    public class ApiApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ApiApplication.class, args);
        }
    
    }
    
3.5 基于SpringBoot的单元测试
3.5.1 添加依赖
<!--test starter-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>
3.5.2 测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApiApplication.class)
public class UserDAOTest {

    @Resource
    private UserDAO userDAO;

    @Test
    public void queryUserByName() {
        User user = userDAO.queryUserByName("Lucy");
        System.out.println(user);
    }
}
3.6 整合Druid
3.6.1 添加依赖
  • mapper子工程添加druid-starter

    <!--druid starter-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.22</version>
    </dependency>
    
3.6.2 修改数据源配置
  • 修改mapper子工程application.yml文件

    spring:
      datasource:
        druid:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/db_2010_mybatis?characterEncoding=utf-8
          username: root
          password: admin123
    mybatis:
      mapper-locations: classpath:mappers/*Mapper.xml
      type-aliases-package: com.qfedu.fmmall.entity
    

四、《锋迷商城》数据库设计

4.1 软件开发步骤
  • 问题定义/提出问题

  • 可行性分析(技术、成本、法律法规)

  • 需求分析(需求采集、需求分析)---->甲方

  • 概要设计

    • 架构设计(技术选型、架构模式、项目搭建)
    • 数据库设计
    • UI设计
    • 业务流程设计
  • 详细设计

    • 实现步骤(业务流程的实现细节)
  • 编码

    • 根据设计好的实现步骤进行代码实现
    • 开发过程中开发者要进行单元测试
  • 测试

    • 集成测试
    • 功能测试(黑盒)
    • 性能测试(白盒)
  • 交付/部署实施

4.2 数据库设计流程
  • 根据项目功能分析数据实体(数据实体,就是应用系统中要存储的数据对象)
    • 商品、订单、购物车、用户、评价、地址…
  • 提取数据实体的数据项(数据对象的属性)
    • 商品(商品id、商品名称、商品描述,特征)
    • 地址(姓名、地址、电话…)
  • 使用数据库设计三范式检查数据项是否合理
  • 分析实体关系:E-R图
  • 数据库建模(三线图)、建模工具
  • 建库建表-SQL
4.3 数据库设计分析
4.3.1 PDMan建模工具使用
  • 可视化创建数据表(数据表)

  • 视图显示数据表之间的关系(关系图)

  • 导出SQL指令(模型–导出DDL脚本)

  • 记录数据设计的版本-数据库模型版本的管理(模型版本)

  • 同步数据模型到数据库(开始-数据库连接)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bh8Svyio-1622295230342)(imgs/1618382547448.png)]

4.3.2 分析《锋迷商城》的数据库模型
  • 用户

  • 首页

  • 商品

  • 购物车

  • 订单 和 订单项

  • 评论

4.4 SPU 和 SKU
4.4.1 SPU

SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。

1 荣耀8

2 小米10

4.4.2 SKU

SKU(中文译为最小存货单位,英文全称为Stock Keeping Unit,简称SKU,定义为保存库存控制的最小可用单位)

101 8G / 128G 10 1800 1

102 4G / 128G 20 1500 1

103 8G / 128G 12 2999 2

104 12G / 256G 11 3999 2

4.5 建库建表
4.5.1 创建数据表
  • 从PDMan导出sql,导入到mysql
4.5.2 准备测试数据
  • 首页轮播图 index_img

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ctYA03Qa-1622295230344)(imgs/1618392216009.png)]

  • 首页类别信息 category

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-efMGD6Iz-1622295230345)(imgs/1618392552921.png)]

  • 商品信息

  • sku

五、《锋迷商城》业务流程设计-接口规范

在企业项目开发中,当完成项目的需求分析、功能分析、数据库分析与设计之后,项目组就会按照项目中的功能进行开发任务的分配

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wo7XibdM-1622295230346)(imgs/1618450184396.png)]

5.1 前后端分离与单体架构流程实现的区别

单体架构:页面和控制之间可以进行跳转,同步请求控制器,流程控制由的控制来完成

前后端分离架构:前端和后端分离开发和部署,前端只能通过异步向后端发送请求,后端只负责接收请求及参数、处理请求、返回处理结果,但是后端并不负责流程控制,流程控制是由前端完成

5.1.1 单体架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QJskccxo-1622295230348)(imgs/1618451138871.png)]

5.1.2 前后端分离架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j6rbGHTJ-1622295230351)(imgs/1618451153971.png)]

5.2 接口介绍
5.2.1 接口概念

狭义的理解:就是控制器中可以接受用户请求的某个方法

应用程序编程接口,简称API(Application Programming Interface),就是软件系统不同组成部分衔接的约定

5.2.2 接口规范

作为一个后端开发者,我们不仅要完成接口程序的开发,还要编写接口的说明文档——接口规范

接口规范示例

参考:《锋迷商城》后端接口说明

5.3 Swagger

前后端分离开发,后端需要编写接口说明文档,会耗费比较多的时间

swagger是一个用于生成服务器接口的规范性文档、并且能够对接口进行测试的工具

5.3.1 作用
  • 生成接口说明文档
  • 对接口进行测试
5.3.2 Swagger整合
  • api子工程添加依赖(Swagger2 \ Swagger UI)

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
  • api子工程创建swagger的配置(Java配置方式)

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
    
        /*swagger会帮助我们生成接口文档
        * 1:配置生成的文档信息
        * 2: 配置生成规则*/
    
        /*Docket封装接口文档信息*/
        @Bean
        public Docket getDocket(){
    
            //创建封面信息对象
            ApiInfoBuilder apiInfoBuilder = new ApiInfoBuilder();
            apiInfoBuilder.title("《锋迷商城》后端接口说明")
                    .description("此文档详细说明了锋迷商城项目后端接口规范....")
                    .version("v 2.0.1")
                    .contact( new Contact("亮哥","www.liangge.com","liangge@wang.com") );
            ApiInfo apiInfo =  apiInfoBuilder.build();
    
            Docket docket = new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo) //指定生成的文档中的封面信息:文档标题、版本、作者
                    .select()
                    .apis(RequestHandlerSelectors.basePackage("com.qfedu.fmmall.controller"))
                    .paths(PathSelectors.any())
                    .build();
    
            return docket;
        }
    
    }
    
  • 测试:

    • 启动SpringBoot应用,访问:http://localhost:8080/swagger-ui.html
5.3.3 Swagger注解说明

swagger提供了一套注解,可以对每个接口进行详细说明

@Api 类注解,在控制器类添加此注解,可以对控制器类进行功能说明

@Api(value = "提供商品添加、修改、删除及查询的相关接口",tags = "商品管理")

@ApiOperation方法注解:说明接口方法的作用

@ApiImplicitParams@ApiImplicitParam 方法注解,说名接口方法的参数

@ApiOperation("用户登录接口")
@ApiImplicitParams({
    @ApiImplicitParam(dataType = "string",name = "username", value = "用户登录账号",required = true),
    @ApiImplicitParam(dataType = "string",name = "password", value = "用户登录密码",required = false,defaultValue = "111111")
})
@RequestMapping(value = "/login",method = RequestMethod.GET)
public ResultVO login(@RequestParam("username") String name,
                      @RequestParam(value = "password",defaultValue = "111111") String pwd){
    return userService.checkLogin(name,pwd);
}

@ApiModel@ApiModelProperty 当接口参数和返回值为对象类型时,在实体类中添加注解说明

@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "User对象",description = "用户/买家信息")
public class User {

    @ApiModelProperty(dataType = "int",required = false)
    private int userId;

    @ApiModelProperty(dataType = "String",required = true, value = "用户注册账号")
    private String userName;

    @ApiModelProperty(dataType = "String",required = true, value = "用户注册密码")
    private String userPwd;

    @ApiModelProperty(dataType = "String",required = true, value = "用户真实姓名")
    private String userRealname;

    @ApiModelProperty(dataType = "String",required = true, value = "用户头像url")
    private String userImg;
}

@ApiIgnore接口方法注解,添加此注解的方法将不会生成到接口文档中

5.3.4 Swagger-ui 插件
  • 导入插件的依赖

    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>swagger-bootstrap-ui</artifactId>
        <version>1.9.6</version>
    </dependency>
    
  • 文档访问

    http://ip:port/doc.html

5.4 RESTful

前后端分离开发的项目中,前后端之间是接口进行请求和响应,后端向前端提供请求时就要对外暴露一个URL;URL的设计不能是随意的,需要遵从一定的设计规范——RESTful

RESTful 是一种Web api的标准,也就是一种url设计风格/规范

  • 每个URL请求路径代表服务器上的唯一资源

    传统的URL设计:
    	http://localhost:8080/goods/delete?goodsId=1    商品1
        http://localhost:8080/goods/delete?goodsId=2    商品2
    
    RESTful设计:
    	http://localhost:8080/goods/delete/1    商品1
    	http://localhost:8080/goods/delete/2    商品2
    
    @RequestMapping("/delete/{gid}")
    public ResultVO deleteGoods(@PathVariable("gid") int goodsId){
        System.out.println("-----"+goodsId);
        return new ResultVO(10000,"delete success",null);
    }
    
  • 使用不同的请求方式表示不同的操作

    SpringMVC对RESTful风格提供了很好的支持,在我们定义一个接口的URL时,可以通过@RequestMapping(value="/{id}",method=RequestMethod.GET)形式指定请求方式,也可使用特定请求方式的注解设定URL

    @PostMapping("/add")

    @DeleteMapping("/{id}")

    @PutMapping("/{id}")

    @GetMapping("/{id}")

    • post 添加
    • get 查询
    • put 修改
    • delete 删除
    • option (预检)
    根据ID删除一个商品:
    //http://localhost:8080/goods/1   [delete]
    @RequestMapping(value = "/{id}",method = RequestMethod.DELETE)
    public ResultVO deleteGoods(@PathVariable("id") int goodsId){
        System.out.println("-----"+goodsId);
        return new ResultVO(10000,"delete success",null);
    }
    
    根据ID查询一个商品:
    //http://localhost:8080/goods/1    [get]
    @RequestMapping(value = "/{id}",method = RequestMethod.GET)
    public ResultVO getGoods(@PathVariable("id") int goodsId){
            return null;
    }
    
  • 接口响应的资源的表现形式采用JSON(或者XML)

  • 在控制类或者每个接口方法添加@ResponseBody注解将返回的对象格式为json

  • 或者直接在控制器类使用@RestController注解声明控制器

  • 前端(Android\ios\pc)通过无状态的HTTP协议与后端接口进行交互

六、《锋迷商城》设计及实现—用户管理

6.1 实现流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K1qkjsEz-1622295230354)(imgs/1618471168742.png)]

6.2 后端接口开发
6.2.1 完成DAO操作
  1. 创建实体类

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @ApiModel(value = "User对象",description = "用户/买家信息")
    public class User {
        
        private int userId;
        private String username;
        private String password;
        private String nickname;
        private String realname;
        private String userImg;
        private String userMobile;
        private String userEmail;
        private String userSex;
        private Date userBirth;
        private Date userRegtime;
        private Date userModtime;
    
    }
    
  2. 创建DAO接口、定义操作方法

    public interface UserDAO {
    
        //用户注册
        public int insert(User user);
        
        //根据用户名查询用户信息
        public User query(String name);
    
    }
    
  3. 创建DAO接口的mapper文件并完成配置

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.qfedu.fmmall.dao.UserDAO">
    
        <insert id="insertUser">
            insert into users(username,password,user_regtime,user_modtime)
            values(#{username},#{password},#{userRegtime},#{userModtime})
        </insert>
    
        <resultMap id="userMap" type="User">
            <id column="user_id" property="userId"/>
            <result column="username" property="username"/>
            <result column="password" property="password"/>
            <result column="nickname" property="nickname"/>
            <result column="realname" property="realname"/>
            <result column="user_img" property="userImg"/>
            <result column="user_mobile" property="userMobile"/>
            <result column="user_email" property="userEmail"/>
            <result column="user_sex" property="userSex"/>
            <result column="user_birth" property="userBirth"/>
            <result column="user_regtime" property="userRegtime"/>
            <result column="user_modtime" property="userModtime"/>
        </resultMap>
    
        <select id="queryUserByName" resultMap="userMap">
            select
                user_id,
                username,
                password,
                nickname,
                realname,
                user_img,
                user_mobile,
                user_email,
                user_sex,
                user_birth,
                user_regtime,
                user_modtime
            from users
            where username=#{name}
        </select>
    
    </mapper>
    
6.2.2 完成Service业务
  1. 创建service接口

    public interface UserService {
    
        //用户注册
        public ResultVO userResgit(String name, String pwd);
    
        //用户登录
        public ResultVO checkLogin(String name, String pwd);
    
    }
    
  2. 创建service接口实现类,完成业务实现

    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDAO userDAO;
    
        @Transactional
        public ResultVO userResgit(String name, String pwd) {
            synchronized (this) {
                //1.根据用户查询,这个用户是否已经被注册
                User user = userDAO.queryUserByName(name);
    
                //2.如果没有被注册则进行保存操作
                if (user == null) {
                    String md5Pwd = MD5Utils.md5(pwd);
                    user = new User();
                    user.setUsername(name);
                    user.setPassword(md5Pwd);
                    user.setUserRegtime(new Date());
                    user.setUserModtime(new Date());
                    int i = userDAO.insertUser(user);
                    if (i > 0) {
                        return new ResultVO(10000, "注册成功!", null);
                    } else {
                        return new ResultVO(10002, "注册失败!", null);
                    }
                } else {
                    return new ResultVO(10001, "用户名已经被注册!", null);
                }
            }
        }
    
        @Override
        public ResultVO checkLogin(String name, String pwd) {
            User user = userDAO.queryUserByName(name);
            if(user == null){
                return new ResultVO(10001,"登录失败,用户名不存在!",null);
            }else{
                String md5Pwd = MD5Utils.md5(pwd);
                if(md5Pwd.equals(user.getPassword())){
                    return new ResultVO(10000,"登录成功!",user);
                }else{
                    return new ResultVO(10001,"登录失败,密码错误!",null);
                }
            }
        }
    }
    
6.2.3 完成Controller提供接口
  1. 创建controller,调用service

  2. 添加接口注解

    @RestController
    @RequestMapping("/user")
    @Api(value = "提供用户的登录和注册接口",tags = "用户管理")
    public class UserController {
    
       @Resource
       private UserService userService;
    
    
       @ApiOperation("用户登录接口")
       @ApiImplicitParams({
               @ApiImplicitParam(dataType = "string",name = "username", value = "用户登录账号",required = true),
               @ApiImplicitParam(dataType = "string",name = "password", value = "用户登录密码",required = true)
       })
       @GetMapping("/login")
       public ResultVO login(@RequestParam("username") String name,
                             @RequestParam(value = "password") String pwd){
           ResultVO resultVO = userService.checkLogin(name, pwd);
           return resultVO;
       }
    
       @ApiOperation("用户注册接口")
       @ApiImplicitParams({
               @ApiImplicitParam(dataType = "string",name = "username", value = "用户注册账号",required = true),
               @ApiImplicitParam(dataType = "string",name = "password", value = "用户注册密码",required = true)
       })
       @PostMapping("/regist")
       public ResultVO regist(String username,String password){
           ResultVO resultVO = userService.userResgit(username, password);
           return resultVO;
       }
    
    }
    
6.2.4 接口测试
  • 基于swagger进行测试
6.3 前端跨域访问
6.3.1 跨域访问概念
  • 什么时跨域访问?

    AJAX 跨域访问是用户访问A网站时所产生的对B网站的跨域访问请求均提交到A网站的指定页面

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mFGarm8C-1622295230357)(imgs/1618561898108.png)]

6.3.2 如何解决跨域访问?
  • 前端使用JSONP设置
  • 后端使用@CrossOrigin — 就是设置响应头允许跨域
6.4 前端页面之间的传值
6.4.1 cookie
  • 工具方法封装:

    var operator = "=";
    
    function getCookieValue(keyStr){
    	var value = null;
    	var s = window.document.cookie;
    	var arr = s.split("; ");
    	for(var i=0; i<arr.length; i++){
    		var str = arr[i];
    		var k = str.split(operator)[0];
    		var v = str.split(operator)[1];
    		if(k == keyStr){
    			value = v;
    			break;
    		}
    	}
    	return value;
    }
    
    function setCookieValue(key,value){
    	document.cookie = key+operator+value;
    }
    
  • A页面

    setCookieValue("username",userInfo.username);
    setCookieValue("userimg",userInfo.userImg);
    
  • B页面

    var name = getCookieValue("username");
    var img = getCookieValue("userimg");
    
6.4.2 localStorage
  • A页面

    localStorage.setItem("user",JSON.stringify(userInfo));
    
  • B页面

    var jsonStr = localStorage.getItem("user");
    var userInfo = eval("("+jsonStr+")");
    
    //移出localStorage键值对
    localStorage.removeItem("user");			
    

七、前后端分离用户认证-JWT

7.1 基于session实现单体项目用户认证

在单体项目中如何保证受限资源在用户未登录的情况下不允许访问?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jl3hfp6U-1622295230359)(imgs/1619158116136.png)]

在单体项目中,视图资源(页面)和接口(控制器)都在同一台服务器,用户的多次请求都是基于同一个会话(session),因此可以借助session来进行用户认证判断:

1.当用户登录成功之后,将用户信息存放到session

2.当用户再次访问受限资源时,验证session中是否存在用户信息,可以根据session有无用户信息来判断用户是否登录

7.2 基于token实现前后端分离用户认证

由于在前后端分离项目开发中,前后端之间是通过异步交互完成数据访问的,请求是无状态的,因此不能基于session实现用户的认证。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ke5r076T-1622295230362)(imgs/1619149656171.png)]

7.3 基于token的用户认证的实现
7.3.1 登录认证接口生成token
// UserController
@GetMapping("/login")
public ResultVO login(@RequestParam("username") String name,
                      @RequestParam(value = "password") String pwd){
    ResultVO resultVO = userService.checkLogin(name, pwd);
    return resultVO;
}
// UserServiceImpl
public ResultVO checkLogin(String name, String pwd) {
    Example example = new Example(Users.class);
    Example.Criteria criteria = example.createCriteria();
    criteria.andEqualTo("username", name);
    List<Users> users = usersMapper.selectByExample(example);

    if(users.size() == 0){
        return new ResultVO(ResStatus.NO,"登录失败,用户名不存在!",null);
    }else{
        String md5Pwd = MD5Utils.md5(pwd);
        if(md5Pwd.equals(users.get(0).getPassword())){
            //如果登录验证成功,则需要生成令牌token(token就是按照特定规则生成的字符串)
            String token = Base64Utils.encode(name+"QIANfeng6666");
            return new ResultVO(ResStatus.OK,token,users.get(0));
        }else{
            return new ResultVO(ResStatus.NO,"登录失败,密码错误!",null);
        }
    }
}
7.3.2 登录页面接收到token存储到cookie
// login.html
doSubmit:function(){
    if(vm.isRight){
        var url = baseUrl+"user/login";
        axios.get(url,{
            params:{
                username:vm.username,
                password:vm.password
            }
        }).then((res)=>{
            var vo = res.data;
            if(vo.code == 10000){
                //如果登录成功,就把token存储到cookie
                setCookieValue("token",vo.msg);

                window.location.href = "index.html";
            }else{
                vm.tips = "登录失败,账号或密码错误!";
            }
        });

    }else{
        vm.tips = "请正确输入帐号和密码!";
    }
}
7.3.3 购物车页面加载时访问购物车列表接口
  • 获取token
  • 携带token访问接口
<script type="text/javascript">
    var baseUrl = "http://localhost:8080/";

    var vm = new Vue({
        el:"#container",
        data:{
            token:""
        },
        created:function(){
            //当进入到购物车页面时,就要查询购物车列表(访问购物车列表接口)
            this.token = getCookieValue("token");
            console.log("token:"+this.token);
            axios({
                method:"get",
                url:baseUrl+"shopcart/list",
                params:{
                    token:this.token
                }
            }).then(function(res){
                console.log(res);
            });
        }
    });

</script>
7.3.4 在购物车列表接口校验token
@GetMapping("/list")
@ApiImplicitParam(dataType = "string",name = "token", value = "授权令牌",required = true)
public ResultVO listCarts(String token){
    //1.获取token
    //2.校验token
    if(token == null){
        return new ResultVO(ResStatus.NO,"请先登录",null);
    }else{
        String decode = Base64Utils.decode(token);
        if(decode.endsWith("QIANfeng6666")){
            //token校验成功
            return new ResultVO(ResStatus.OK,"success",null);
        }else{
            return new ResultVO(ResStatus.NO,"登录过期,请重新登录!",null);
        }
    }
}
7.4 JWT

如果按照上述规则生成token:

1.简易的token生成规则安全性较差,如果要生成安全性很高的token对加密算法要求较高;

2.无法完成时效性的校验(登录过期)

7.4.1 JWT简介
  • JWT: Json Web Token

  • 官网:https://jwt.io

  • jwt的结构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vWbaadM0-1622295230364)(imgs/1619161525439.png)]

7.4.2 生成JWT
  • 添加依赖

    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.10.3</version>
    </dependency>
    
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    
  • 生成token

    String token = builder.setSubject(name)                 //主题,就是token中携带的数据
        .setIssuedAt(new Date())                            //设置token的生成时间
        .setId(users.get(0).getUserId() + "")               //设置用户id为token  id
        .setClaims(map)                                     //map中可以存放用户的角色权限信息
        .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) //设置过期时间
        .signWith(SignatureAlgorithm.HS256, "QIANfeng6666")     //设置加密方式和加密密码
        .compact();
    
7.4.3 JWT校验
  • 如果token正确则正常解析,如果token不正确或者过期,则通过抛出的异常进行识别

    try {
        //验证token
        JwtParser parser = Jwts.parser();
        parser.setSigningKey("QIANfeng6666"); //解析token的SigningKey必须和生成token时设置密码一致
        //如果token正确(密码正确,有效期内)则正常执行,否则抛出异常
        Jws<Claims> claimsJws = parser.parseClaimsJws(token);
        Claims body = claimsJws.getBody();  //获取token中用户数据
        String subject = body.getSubject(); //获取生成token设置的subject
        String v1 = body.get("key1", String.class); //获取生成token时存储的Claims的map中的值
    
        return new ResultVO(ResStatus.OK,"success",null);
    }catch (ExpiredJwtException e){
        return new ResultVO(ResStatus.NO,"登录过期,请重新登录!",null);
    }catch (UnsupportedJwtException e){
        return new ResultVO(ResStatus.NO,"Tonken不合法,请自重!",null);
    }catch (Exception e){
        return new ResultVO(ResStatus.NO,"请重新登录!",null);
    }
    
7.4.4 拦截器校验Token
  • 创建拦截器

    @Component
    public class CheckTokenInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            String token = request.getParameter("token");
            if(token == null){
                ResultVO resultVO = new ResultVO(ResStatus.NO, "请先登录!", null);
                //提示请先登录
                doResponse(response,resultVO);
            }else{
                try {
                    //验证token
                    JwtParser parser = Jwts.parser();
                    //解析token的SigningKey必须和生成token时设置密码一致
                    parser.setSigningKey("QIANfeng6666"); 
                    //如果token正确(密码正确,有效期内)则正常执行,否则抛出异常
                    Jws<Claims> claimsJws = parser.parseClaimsJws(token);
                    return true;
                }catch (ExpiredJwtException e){
                    ResultVO resultVO = new ResultVO(ResStatus.NO, "登录过期,请重新登录!", null);
                    doResponse(response,resultVO);
                }catch (UnsupportedJwtException e){
                    ResultVO resultVO = new ResultVO(ResStatus.NO, "Token不合法,请自重!", null);
                    doResponse(response,resultVO);
                }catch (Exception e){
                    ResultVO resultVO = new ResultVO(ResStatus.NO, "请先登录!", null);
                    doResponse(response,resultVO);
                }
            }
            return false;
        }
    
        private void doResponse(HttpServletResponse response,ResultVO resultVO) throws IOException {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            PrintWriter out = response.getWriter();
            String s = new ObjectMapper().writeValueAsString(resultVO);
            out.print(s);
            out.flush();
            out.close();
        }
    
    }
    
  • 配置拦截器

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
    
        @Autowired
        private CheckTokenInterceptor checkTokenInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(checkTokenInterceptor)
                    .addPathPatterns("/**")
                    .excludePathPatterns("/user/**");
        }
    }
    
7.5 请求头传递token

前端但凡访问受限资源,都必须携带token发送请求;token可以通过请求行(params)、请求头(header)以及请求体(data)传递,但是习惯性使用header传递

7.5.1 axios通过请求头传值
axios({
    method:"get",
    url:baseUrl+"shopcart/list",
    headers:{
        token:this.token
    }
}).then(function(res){
    console.log(res);
});
7.5.2 在拦截器中放行options请求
@Component
public class CheckTokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		//放行options请求
        String method = request.getMethod();
        if("OPTIONS".equalsIgnoreCase(method)){
            return true;
        }

        String token = request.getHeader("token");

        System.out.println("-------------"+token);
        if(token == null){
            ResultVO resultVO = new ResultVO(ResStatus.NO, "请先登录!", null);
            //提示请先登录
            doResponse(response,resultVO);
        }else{
            try {
                //验证token
                JwtParser parser = Jwts.parser();
                //解析token的SigningKey必须和生成token时设置密码一致
                parser.setSigningKey("QIANfeng6666"); 
                //如果token正确(密码正确,有效期内)则正常执行,否则抛出异常
                Jws<Claims> claimsJws = parser.parseClaimsJws(token);
                return true;
            }catch (ExpiredJwtException e){
                ResultVO resultVO = new ResultVO(ResStatus.NO, "登录过期,请重新登录!", null);
                doResponse(response,resultVO);
            }catch (UnsupportedJwtException e){
                ResultVO resultVO = new ResultVO(ResStatus.NO, "Token不合法,请自重!", null);
                doResponse(response,resultVO);
            }catch (Exception e){
                ResultVO resultVO = new ResultVO(ResStatus.NO, "请先登录!", null);
                doResponse(response,resultVO);
            }
        }
        return false;
    }

   private void doResponse(HttpServletResponse response,ResultVO resultVO) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        String s = new ObjectMapper().writeValueAsString(resultVO);
        out.print(s);
        out.flush();
        out.close();
    }

}

开发任务

1、登录注册功能用户
  • vue+axios实现
  • 首页显示用户头像和昵称
2、首页轮播图

ponse(response,resultVO);
}else{
try {
//验证token
JwtParser parser = Jwts.parser();
//解析token的SigningKey必须和生成token时设置密码一致
parser.setSigningKey(“QIANfeng6666”);
//如果token正确(密码正确,有效期内)则正常执行,否则抛出异常
Jws claimsJws = parser.parseClaimsJws(token);
return true;
}catch (ExpiredJwtException e){
ResultVO resultVO = new ResultVO(ResStatus.NO, “登录过期,请重新登录!”, null);
doResponse(response,resultVO);
}catch (UnsupportedJwtException e){
ResultVO resultVO = new ResultVO(ResStatus.NO, “Token不合法,请自重!”, null);
doResponse(response,resultVO);
}catch (Exception e){
ResultVO resultVO = new ResultVO(ResStatus.NO, “请先登录!”, null);
doResponse(response,resultVO);
}
}
return false;
}

private void doResponse(HttpServletResponse response,ResultVO resultVO) throws IOException {
response.setContentType(“application/json”);
response.setCharacterEncoding(“utf-8”);
PrintWriter out = response.getWriter();
String s = new ObjectMapper().writeValueAsString(resultVO);
out.print(s);
out.flush();
out.close();
}

}






## 开发任务

#### 1、登录注册功能用户

- vue+axios实现
- 首页显示用户头像和昵称

#### 2、首页轮播图

#### 3、首页商品分类
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值