一、《锋迷商城》项目介绍
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)]
依赖配置说明
- 在父工程的pom文件中一次性添加各个子工程所需的所有依赖
- 在各个子工程中单独添加当前子工程的依赖
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操作
-
创建实体类
@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; }
-
创建DAO接口、定义操作方法
public interface UserDAO { //用户注册 public int insert(User user); //根据用户名查询用户信息 public User query(String name); }
-
创建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业务
-
创建service接口
public interface UserService { //用户注册 public ResultVO userResgit(String name, String pwd); //用户登录 public ResultVO checkLogin(String name, String pwd); }
-
创建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提供接口
-
创建controller,调用service
-
添加接口注解
@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、首页商品分类