开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

全网最全MySQL知识点万字整理 一、SQL介绍 二、表和O

发表于 2021-07-17

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

一、SQL介绍

1.1、SQL概述

人和人交流需要语言,人和数据库交流也需要语言,而这个专门特定为程序员和数据库打交道的语言就是 SQL 语言。


SQL:结构化查询语言(Structured Query Language)。是关系型数据库标准语言。 特点:简单,灵活,功能强大。

1.2、SQL包含的6个部分

1.2.1、数据查询语言(DQL)

其语句,也称为“数据检索语句”,用以从表中获得数据,确定数据怎样在应用程序给出。保留字 `SELECT` 是DQL(也是所有SQL)用得最多的动词,其他DQL常用的保留字有`WHERE`,`ORDER BY`,`GROUP BY`和`HAVING`。这些 DQL 保留字常与其他类型的SQL语句一起使用。

1.2.2、数据操作语言(DML)

其语句包括动词 `INSERT`,`UPDATE`和`DELETE`。它们分别用于添加,修改和删除表中的行。也称为动作语言。

1.2.3、数据定义语言(DDL)

其语句包括动词 CREATE 和 DROP。在数据库中创建新表或删除表(`CREAT TABLE` 或 `DROP TABLE`);为表加入索引等。DDL包括许多与人数据库目录中获得数据有关的保留字。它也是动作查询的一部分。

1.2.4、事务处理语言(TPL)

它的语句能确保被DML语句影响的表的所有行及时得以更新。TPL语句包括`BEGIN TRANSACTION`,`COMMIT`和`ROLLBACK`。

1.2.5、数据控制语言(DCL)

它的语句通过`GRANT`或`REVOKE`获得许可,确定单个用户和用户组对数据库对象的访问。某些RDBMS可用`GRANT`或`REVOKE`控制对表单个列的访问。

1.2.6、指针控制语言(CCL)

它的语句,像`DECLARE CURSOR`,`FETCH INTO`和`UPDATE WHERE CURRENT`用于对一个或多个表单独行的操作。

1.3、书写规则

  1. 数据库中,SQL 语句大小写不敏感. 如: select、SELECT.、SeleCt,为了提高可读性,一般关键字大写,其他小写。
  2. SQL 语句可单行或多行书写,用分号来分辨是否结束。
  3. 合理利用空格和缩进使程序易读

二、表和ORM

2.1、表

​ 二维表是 同类实体 的各种 属性的集合,每个实体对应于表中的一行,在关系中称为元组,相当于通常的一条记录; 表中的列表示属性,称为Field,相当于通常记录中的一个数据项,也叫列、字段。 行: 表示一个实体,一条记录 列: 字段,数据项。

2.2、表和对象的关系(ORM)

​ ORM: Oject Reraltional Mapping : 对象表的映射

​ 在开发中,我们需要将表中的数据查询出来保存到内存中,或者把内存中的数据保存到数据库中,此时就需要将数据表的数据和Java中的对象进行映射关联起来。这种映射关联就称为 ORM 思想。

在这里插入图片描述
在这里插入图片描述

三、MySQL服务

3.1、MySQL服务

​ 打开数据库连接之前:一定要保证 MySQL 服务已经开启了。

​ net start命令开启一个服务,如:net start MySQL。

​ net stop 命令关闭一个服务器,如:net stop MySQL

3.2、连接MySQL

方式一

​ 进入 MySQL 自带的客户端, 在命令行中输入密码。

方式二

​ 在运行(win + r 进入cmd )中输入命令。

​ 格式:mysql -u账户 -p密码 -h数据库服务器安装的主机 -P数据库端口

1
markdown复制代码mysql -uroot -padmin -h127.0.0.1 -P3306
若连接的数据库服务器在本机上,并且端口是 3306。 则可以简写: mysql -uroot -padmin。

四、数据库基础

4.1、数据库基本操作

  1. 查看数据库服务器存在哪些数据库.:SHOW DATABASES。
  2. 使用指定的数据库.:USE database_name。
  3. 查看指定的数据库中有哪些数据表:SHOW TABLES。
  4. 创建指定名称的数据库.:CREATE DATABASE database_name。
  5. 删除数据库:DROP DATABASE database_name。

4.2、存储引擎

MySQL 中的数据用各种不同的技术存储在文件(或者内存)中。这些技术中的每一种技术都**使用不同的存储机制、索引技巧、锁定水平并且最终提供不同的功能和能力**。 通过选择不同的技术,你能够获得额外的速度或者功能,从而改善你的应用的整体功能。

​ 简单来说,存储引擎是表的存储方式。

​ MySQL常用存储引擎:

  • MyISAM:拥有较高的插入,查询速度,但不支持事务,不支持外键。
  • InnoDB:支持事务,支持外键,支持行级锁定,性能较低。最安全
InnoDB 存储引擎提供了具有**提交、回滚和崩溃恢复能力的事务安全**。但对比MyISAM,处理效率差,且会占用更多的磁盘空间以保留数据和索引。**一个系统,特别是金融系统,没有事务是很恐怖的事情,一般都要选择 InnDB。**

在这里插入图片描述

五、MySQL列的常用类型

5.1、最常用的类型

MsSQL Java
INT int
BIGINT long
DECIMAL BigDecimal
DATE/DATETIME java.util.Date
VARCHAR String

5.2、整数类型

整数类型有宽度指示器,作用是指定位宽。


例如:某字段类型为 INT(3),保证少于3个值,从数据库检索出来时能够自动地用 0 填充,需设置填充,默认不填充。

​ 宽度指示器不影响列存值得范围。一般不指定位宽。
20201230114133531
.png)]

5.3、小数 类型

​ FLOAT[(s,p)] 或DOUBLE[(s,p)]: 小数类型,可存放实型和整型 ,精度 (p) 和范围 (s)。

​ DECIMAL : 高精度类型,金额货币优先选择。

在这里插入图片描述

5.4、字符类型

  • ​ char(size) : 定长字符,0 - 255字节,size 指 N 个字符数,若插入字符数超过设定长度,会被截取并警告。
  • ​ varchar(size): 变长字符,0 - 255字节,从 MySQL5 开始支持 65535 个字节,若插入字符数超过设定长度,在非严格模式下会被截取并警告。

在这里插入图片描述

一般存储大量的字符串,比如文章的纯文本,可以选用 TEXT 系列类型,这个系列都是变长的。


**注意: 在 MySQL 中,字符类型必须指定长度,值要使用 单引号引起来。 相当于Java中字符(String,StringBuilder/StringBuffer);**

在这里插入图片描述

5.5、日期类型

常用日期和时间类型: DATE、DATETIME。


**注意: 在 MySQL 中,日期时间值使用单引号引起来。 相当于 Java中 Date,Calender。**

在这里插入图片描述

5.6、二进制类型

二进制类型主要用于存放图形、声音和影像,二进制对象,0-4GB。


开发中,我们一般存储二进制文件保存路径,所以以上的类型非特殊需求不会使用。


BIT,一般存储 0 或 1,存储是 Java 中的 boolean/Boolean 类型的值(需要使用)。

在这里插入图片描述

六、表的操作(DDL)

表的操作主要是使用 DDL 来创建表和删除表等操作

6.1、创建表

6.1.1、语法

1
2
3
4
5
6
7
sql复制代码CREATE TABLE 表名 (
列名1 列的类型 [约束],
列名2 列的类型 [约束],
....
列名N 列的类型 约束
);
-- 注意:最后一行没有逗号

6.1.2、例子

创建一张学生表(t\_student) 有id、name、email、age。
1
2
3
4
5
6
SQL复制代码CREATE TABLE t_student (
id BIGINT,
name VARCHAR(15),
email VARCHAR(25),
age INT
);

6.1.3、注意

创建表时,不能使用 MySQL 的关键字、保留字。


解决办法:
1
2
3
markdown复制代码# 1. 尽量避免使用关键字,可以使用其他的单词或单词组合来代替。
# 2. 一般情况下,创建表的时候习惯使用 t_ 做表名的开头。
# 3. 使用反引号(``) 将表名括起来就 ok (`order`)。

6.2、删除表

6.2.1、语法

1
sql复制代码DROP TABLE 表名;

6.2.2、例子

1
2
sql复制代码-- 删除订单表
DROP TABLE `order`;

6.2.3、注意

**如果表名是数据库的关键字或保留字需要加上反引号 (`)**

6.3、表的复制和批量插入

6.3.1、表的复制

表的复制本质上是将查询结果当做表创建出来。
1
mysql复制代码create table 表名 as select语句;

6.3.2、表的批量插入

表的批量插入本质上是将查询结果插入到另一张表中。
1
mysql复制代码insert into dept1 select * from dept;

6.5、表的约束

约束是为了保证表中的数据的合法性、有效性和完整性,我们一般对表会有约束。
  1. 非空约束:NOT NULL,不允许某列的内容为空。
  2. 设置列的默认值:DEFAULT。
  3. 唯一约束:UNIQUE,在该表中,该列的内容必须唯一。
  4. 主键约束:PRIMARY KEY, 非空且唯一。
  5. 主键自增长:AUTO_INCREMENT,从 1 开始,步长为 1。
  6. 外键约束:FOREIGN KEY,A表中的外键列. A表中的外键列的值必须参照于B表中的某一列(B表主
    键)。

6.5.1、主键约束

主键值是这行记录在这张表中的唯一标识,就如同身份证号。一张表的主键约束只能有一个。

​ 主键约束(primary key)不能重复且不能为NULL。

6.5.1.1、主键的分类

  1. 业务主键:使用有业务含义的列作为主键 (不推荐使用);
  2. 自然主键:使用没有业务含义的列作为主键 (推荐使用);

6.5.1.2、如何设计主键

对于主键,我们有以下两种的主键设计原则:
  1. 单字段主键,单列作为主键,建议使用。
  2. 复合主键,使用多列充当主键,不建议。

6.5.1.3、结论

使用单字段的自然主键。

6.5.1.4、例子

创建学生表,id为主键自增,name唯一,email不为空,age默认18。
1
2
3
4
5
6
7
8
mysql复制代码-- 移除存在的表
DROP TABLE IF EXISTS `t_student`;
CREATE TABLE t_student(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(25) UNIQUE,
email VARCHAR(25) NOT NULL,
age INT DEFAULT 18
);

6.5.2、外键约束(foreign key)

\*\*外键是另一张表的主键。\*\*例如员工表与部门表之间就存在关联关系,其中员工表中的部门编号字段就是外键,是相对部门表的外键。


外键可以为NULL,且不一定是另一张的主键,但是必须具有唯一性,一般情况下会引用另一张表的主键。
1
2
3
4
5
6
mysql复制代码create table t_student(
sno it,
sname varchar(255),
classno ,int
foreign key (classno) references t_class(no) -- 对t_student的classno字段添加外键约束,引用的是t_calss的no字段
);

6.5.3、唯一性约束(unique)

唯一约束修饰的字段具有唯一性,不可以重复,但是**可以为NULL,也可以同时为NULL。**
1
2
3
4
5
mysql复制代码create table t_user(
id int,
username varchar(255) unique,-- 列级约束
pwd varchar(255)
)
我们也可以同时给两个列或者多个列添加唯一约束。
1
2
3
4
5
6
7
mysql复制代码-- 这样表示两个字段连起来不能重复,两个字段添加一个约束。表级约束
create table t_user(
id,int,
username varchar(255),
pwd varchar(255),
unique(username,pwd)
)
1
2
3
4
5
6
mysql复制代码-- 这样表示两个字段都不能重复,两个字段加两个约束。
create table t_user(
id,int,
username varchar(255) unique,
pwd varchar(255) unique
)

6.6、表与表之间的关系

6.6.1、一对一

例如t\_person表和t\_card表,即人和身份证。这种情况需要找出主从关系,即谁是主表,谁是从表。人可以没有身份证,但身份证必须要有人才行,所以人是主表,而身份证是从表。设计从表可以有两种方案:
  1. 在t_card表中添加外键列(相对t_user表),并且给外键添加唯一约束;
  2. 给t_card表的主键添加外键约束(相对t_user表),即t_card表的主键也是外键。

6.6.2、一对多(多对一)

一对多(多对一):最为常见的就是一对多!一对多和多对一,这是从哪个角度去看得出来的。t\_user和t\_section的关系,从t\_user来看就是一对多,而从t\_section的角度来看就是多对一!这种情况都是在多方创建外键!

6.6.3、多对多

例如t\_stu和t\_teacher表,即一个学生可以有多个老师,而一个老师也可以有多个学生。这种情况通常需要创建中间表来处理多对多关系。例如再创建一张表t\_stu\_tea表,给出两个外键,一个相对t\_stu表的外键,另一个相对t\_teacher表的外键。

七、DML增删改操作

DML是数据操作语句,用户对表的数据进行操作,所有的DML操作都有一个受影响的行,表示SQL执行,操作了多少行数据。

7.1、插入操作

7.1.1、语法

1
sql复制代码INSERT INTO 表名 (列1,列2,列3...) VALUES(值1,值2,值3...);

7.1.2、例子

1
2
3
4
5
6
7
8
9
sql复制代码-- 1.插入完整数据记录
INSERT INTO t_student(name,email,age) VALUES('xiaoming','xiao@',18);
-- 2.插入数据记录一部分
INSERT INTO t_student(name,age) VALUES('xiaodong',19);
-- 3.插入多条数据记录(MySQL特有)
INSERT INTO t_student(name,email,age) VALUES('xiaohong','hong@',17),
('xiaohong2','hong2@',17),('xiaohong3','hong@3',17)
-- 4.插入查询结果
INSERT INTO t_student(name,email,age) SELECT name,email,age FROM t_student

7.1.3、注意

**一次插入操作只插入一行,插入多条数据为 MySQL 特有语法(不推荐使用,Mybatis有循环来批量加入)**

7.2、修改操作

7.2.1、语法

1
2
3
sql复制代码UPDATE 表名
SET 列1 = 值1, 列2 = 值2, column3 = value3...
WHERE [条件]

7.2.2、练习

1
2
3
4
sql复制代码-- 将张三改为西门吹水
UPDATE t_student SET name='西门吹水' WHERE name='张三';
-- 将 id 为3 的 name 改为叶孤城,email 改为ye@,age 改为100
UPDATE t_student SET name='叶孤城' WHERE id=3;

7.2.3、注意

  1. 如果省略了条件,那么整张表的数据都会被修改,所以一般都会带上条件
  2. 修改语句没有from关键字。

7.3、删除操作

7.3.1、语法

1
sql复制代码DELETE FROM 表名 WHERE [条件]

7.3.2、练习

1
2
3
4
sql复制代码-- 删除 id 为 2 的学生信息
DELETE FROM t_student WHERE id=2;
-- 删除叶孤城的所有信息
DELETE FROM t_student WHERE name='叶孤城'

7.3.3、注意

  1. FROM 不能写成 FORM
  2. 如果省略了 WHERE 子句,则全表的数据都会被删除

八、DQL 查询操作

8.1、语法说明

1
2
3
4
sql复制代码SELECT 列1,列2,列3... FROM 表名 [WHERE];
-- SELECT 选择要查询的列
-- FROM 提供数据源 (表、视图或其他的数据源)
-- 可以写*表示查询所有列,但是在实际开发中基本上不会使用,性能低,实际开发中是将所有字段列出来

8.2、普通查询

8.2.1、设置别名

8.2.1.1、语法

1
2
sql复制代码SELECT 列名 AS 别名 FROM 表名 [WHERE];
SELECT 列名 别名 FROM 表名 [WHERE]

8.2.1.2、作用

  1. 改变列的标题头。
  2. 作为计算结果的含义。
  3. 作为列的别名。
  4. 如果别名中使用特殊字符,或是强制大小写或有空格时都需要加单引号。

8.2.1.3、例子

1
2
3
sql复制代码-- 查询所有货品的id,名称,各进50个,并且每个运费1元的成本(使用别名)
SELECT id,productName,(costPrice + 1) * 50 1 AS allPrice FROM product
SELECT id,productName,(costPrice + 1) * 50 allPrice FROM product

8.2.2、按照格式输出

为方便用户浏览查询结果数据,有时需要设置查询结果的显示格式,可以使用 `CONCAT` 函数来 连接字符串。

8.2.2.1、语法

1
sql复制代码CONCAT(字符串1,字符串2,...)

8.2.2.2、实战

1
2
sql复制代码-- 查询商品的名字和零售价。格式: xxx 商品的零售价为:ooo
SELECT CONCAT(productName,'商品的零售价为:',salePrice) FROM product

8.2.3、消除重复的数据

**distinct前面不能接其他的字段,他只能出现在所有字段的最前方。他表示的意思是后面所有的字段联合起来一起去重。**
1
sql复制代码SELECT DISTINCT 列名, ... FROM 表名;

8.2.4、算数运算符

对 number 型数据可以使用算数操作符创建表达式
他有如下优先级:
  1. 乘法和除法的优先级高于加法和减法。
  2. 同级运算的顺序是从左到右。
  3. 表达式中使用”括号”可强行改变优先级的运算顺序
1
2
3
4
5
6
sql复制代码-- 查询所有货品的id,名称和批发价(批发价=卖价*折扣)
SELECT id,productName,salePrice * cutoff FROM product
-- 查询所有货品的id,名称,和各进50个的成本价(成本=costPirce)
SELECT id,productName,costPrice * 50 FROM product
-- 查询所有货品的id,名称,各进50个,并且每个运费1元的成本
SELECT id,productName,(costPrice + 1) * 50 FROM product

8.2.5、比较运算符

比较运算符有如下几个:
  1. =
  2. >
  3. <
  4. <=
  5. != (<> 等价 !=)
1
2
3
4
5
6
sql复制代码-- 查询商品名为 罗技G9X 的货品信息
SELECT * FROM product WHERE productName='罗技G9X';
-- 查询零售价小于等于 200 的所有货品信息
SELECT * FROM product WHERE salePrice <= 200
-- 查询批发价大于 350 的货品信息
SELECT *,salePrice * cutoff allPrice FROM product WHERE salePrice * cutoff > 350

8.2.6、逻辑运算符

运算符 含义
AND 如果组合的条件都是TRUE,返回TRUE
OR 如果组合的条件之一是TRUE,返回TRUE
NOT 如果下面的条件是FALSE,返回 TRUE,如果是 TRUE ,返回 FALSE

8.2.7、范围匹配

范围匹配:BETWEEN AND 运算符,一般使用在数字类型的范围上。但对于字符数据和日期类型同样可

用。需要两个数据。

8.2.7.1、语法

1
sql复制代码WHERE 列名 BETWEEN minValue AND maxValue; -- 闭区间

8.2.7.2、例子

1
2
3
4
sql复制代码-- 查询零售价在300-400 之间的货品信息
SELECT * FROM product WHERE salePrice BETWEEN 300 AND 400
-- 查询零售价不在300-400之间的货品信息
SELECT * FROM product WHERE NOT salePrice BETWEEN 300 AND 400

8.2.8、集合查询

集合查询: 使用 IN 运算符,判断列的值是否在指定的集合中。

8.2.8.1、语法

1
sql复制代码WHERE 列名 IN (值1,值2....);

8.2.8.2、例子

1
2
3
4
sql复制代码-- 查询分类编号为2,4的所有货品的id,货品名称,
SELECT id,productName FROM product WHERE dir_id IN (2,4)
-- 查询分类编号不为2,4的所有货品的id,货品名称,
SELECT id,dir_id,productName FROM product WHERE dir_id NOT IN (2,4)

8.2.9、判空

IS NULL: 判断列的值是否为空值,非空字符串,空字符串使用`==`判断。

8.2.9.1、语法

1
sql复制代码WHERE 列名 IS NULL;

8.2.9.2、例子

1
2
3
sql复制代码-- 查询商品名为NULL的所有商品信息。
SELECT * FROM product WHERE productName IS NULL;
SELECT * FROM product WHERE supplier =''

8.2.9.3、注意

**使用`=`来判断只能判断空字符串,不能判断null 的,而使用`IS NULL`只能判断null值,不能判断空**

字符串。

8.2.10、过滤查询

使用 WHERE 子句限定返回的记录

8.2.10.1、语法

1
2
3
sql复制代码SELECT <selectList>
FROM 表名
WHERE 条件;

8.2.10.2、注意

  1. WHERE子句在 FROM 子句后。
  2. 查询语句的字句的执行顺序 FROM 子句: 从哪张表中去查询数据 => WHERE 子句 : 筛选需要哪些行的数据 => SELECT 子句: 筛选要显示的列。

8.2.11、模糊查询

模糊查询数据使用 LIKE 运算符执行通配查询,他有两个通配符:
  1. %:表示可能有零个或者任意多个字符。
  2. _:表示任意的一个字符。

8.2.11.1、语法

1
sql复制代码WHERE 列名 Like '%M_'

8.2.11.2、例子

1
2
sql复制代码-- 查询货品名称匹配'%罗技M9_' 的所有货品信息
SELECT * FROM product WHERE productName LIKE '%罗技M9_'

8.3、结果排序

使用 ORDER BY 子句将查询结果进行排序,他有两种排序的模式:
  1. ASC : 升序(默认)。
  2. DESC:降序。
ORDER BY 子句出现在,SELECT 语句的最后。

8.3.1、例子

1
2
3
4
sql复制代码--单列排序: 选择id,货品名称,分类编号,零售价并且按零售价降序排序
SELECT id,productName,dir_id,salePrice FROM product ORDER BY salePrice DESC
--多列排序: 选择id,货品名称,分类编号,零售价先按分类编号降序排序,再按零售价升序排序
SELECT * FROM product ORDER BY dir_id DESC,salePrice ASC

8.3.2、注意

  1. 谁在前面谁先排序。
  2. 如果列的别名使用 ' ' 则按此别名进行的排序无效。
1
2
sql复制代码-- 反例
SELECT id,salePrice 'sp' FROM product ORDER BY 'sp'

8.4、分页查询

limit是mysql特有的,他用于取结果集中的部分数据,Oracle中有一个相同的机制,叫rownum。


**limit是SQL语句最后执行的环节。**

8.4.1、语法

1
2
3
4
5
6
7
8
9
10
11
sql复制代码SELECT <selectList>
FROM 表名
[WHERE] LIMIT ?,?
-- 第一个? : 开始行的索引数 beginIndex,默认为0
-- 第二个? : 每页显示的最大记录数 pageSize
-- 每页显示 3条数据
-- 第一页: SELECT * FROM product LIMIT 0,3
-- 第三页: SELECT * FROM product LIMIT 6,3
-- 第八页: SELECT * FROM product LIMIT 21,3
-- 当前页 : currentPage
-- 每页显示的最大记录数: pageSize

8.4.2、通用的标准分页sql

**beginIndex = (currentPage - 1) \* pageSize**

8.4.3、案例

案例

找出工资排名在4到6名的员工
1
mysql复制代码select name,sal,from emp order by desc limit 3,6;

8.5、分组函数

  • COUNT(*) : 统计表中有多少条记录
  • SUM(列) : 汇总列的总和
  • MAX(列) : 获取某一列的最大值
  • MIN(列) : 获取某一列的最小值
  • AVG(列) : 获取列的平均值
1
2
3
4
sql复制代码-- 查询货品表中有多少数据
SELECT COUNT(*) FROM product
-- 计算所有货品的总的进货价
SELECT SUM(costPrice) FROM product

注意:

  1. 分组忽略null,无需额外过滤是否为null这个条件。**
  2. SQL语句中有一个语法规则,分组函数不可以直接使用在where字句当中。
  3. count(*)和count(具体的字段的区别)
    • count(*)一定是总记录数,和字段无关。
    • count(具体的某个字段)是这个字段不为空的记录数。

8.6、分组查询

8.6.1、group by

​ group by:按照某个字段或者是某些字段进行分组。

聚合函数分组会和group by一起联合使用,并且任何一个分组函数都是在group by语句执行结束之后才会执行。当一条sql语句没有group by的话,整张表的数据会自成一组。
**SQL语句中有一个语法规则,分组函数不可以直接使用在where字句当中。原因是因为:group by是在where执行之后才会执行。如下面这条错误的sql语句:**
1
sql复制代码select * from emp where sal > avg(sal);
当执行到avg(sal)的时候,还没有执行group by,所以没办法执行分组函数。还没有分组就不可以执行分组函数。

需求:求每一个工作岗位的最高薪资

1
mysql复制代码select max(sal),job from emp group by job;
**结论:当一条sql语句中有group by的时候,select 后面只允许出现分组函数或者是参加分组的字段。**

需求:找出每个部门不同工作岗位的最高薪资)

1
2
3
4
5
6
mysql复制代码select
deptno ,job ,max(sql)
from
emp
group by
deptno,job

8.6.2、having

`having`:having是对分组之后的数据进行再次过滤。

需求:找出每个部门的平均薪资,要求显示薪资大于2000的数据

1
sql复制代码select val(sal),deptno from emp group by deptno having val(sal) > 2000;

8.6.3、group by和having的总结

**能用where过滤的就先用where过滤,无法用where过滤的在用having,但是having一定要搭配group by使用,先分组在过滤。**

8.6、DQL字句的执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql复制代码select 
...
from
...
where
...
group by
...
having
...
order by
...
limit
...
  1. from: 从哪张表中去查数据。
  2. where: 筛选需要的行数据。
  3. group by :分组
  4. having:对分组的数据进行再次过滤
  5. SELECT : 筛选需要显示的列的数据。
  6. ORDER BY : 排序操作。

九、多表查询

9.1、连接查询

在实际开发中,大部分的情况下都不是从单表中查询数据,一般是多张表进行联合查询取出最终的结果,一般一个业务都会对应多张表。

​ 连接查询的分类有两种:

  1. SQL92(语法较老,过时)。
  2. SQL99(语法比较新)。

9.2、笛卡尔积现象

当两张表进行连接查询的时候,没有任何条件进行限制,最终查询的结果条数是两张表记录条数的乘积,这个现象称为笛卡尔积现象。


我们在开发的时候一般会给表起别名,他有两个好处:
  1. 执行效率高。
  2. 可读性好
1
2
3
4
mysql复制代码select 
e.ename,d.dname
from
emp e,dept d
这样出现的条数就是两张表条数的乘积。


既然出现了笛卡尔积现象,我们就要避免笛卡尔积现象,避免笛卡尔积现象的措施就是增加条件进行过滤,但是**避免了笛卡尔积你现象,会减少记录匹配的次数吗?答案是不会,次数还是两张表条数的乘积,只不过显示的是有效的记录数。**

9.3、内连接

假设A和B两张表进行连接,使用内连接的话,凡是A表和B表能够匹配上的记录都会被查询出来,**AB两张表是平等的,没有主副之分**,这就是内连接。

9.3.1、等值连接

内连接最大的特点是:**条件是等量关系。**
1
2
3
4
5
6
7
8
9
10
mysql复制代码select
...
from
...
inner join -- inner是可以省略的,带着inner可读性更好
...
on
连接条件
where
...
1
2
3
4
5
6
7
8
mysql复制代码select 
e.name,d.name
from
emp e
inner join -- inner是可以省略的,带着inner可读性更好
dept d
on
e.deptno = d.deptno
SQL99语法结构更清晰一些,表的连接条件和后来的where过滤条件分离了。

9.3.2、非等值连接

连接条件中的关系是非等量关系。
1
2
3
4
mysql复制代码select e.name,e.sal,e.grade
from emp e
join salgrade s
on e.sal between s.local and s.hisal

9.3.3、自连接

最大的特点是一张表看成两张表,自己连接自己。**(不常用)**

9.4、外连接

假设A表和B表进行连接,使用外连接的话,AB两张表有一张表是主表,一张表是副表,主要查询主表中的数据,捎带着查询副表。


当副表中的数据没有和主表中的数据匹配上的时候副表自动模拟出NULL与之匹配。**主表的数据会无条件的全部查询出来。**

9.4.1、外连接的分类

​ 外连接分为两类:

  1. 左外连接(左连接 LEFT):表示左边的这张表是主表。
  2. 右外连接(右连接 RIGHT):表示右边的这张表是主表。

​ 左连接有连接的写法,右连接也有对应的左连接的写法。用左连接LEFT的时候,说明上面(左边)的表是主表。

9.4.1.1、左连接

1
mysql复制代码SELECT * FROM emp e LEFT OUTER JOINdept d ON e.deptno=d.deptno;

​ 注意:OUTER可以省略

左连接是先查询出左表(即以左表为主),然后查询右表,右表中满足条件的显示出来,不满足条件的显示NULL。

9.4.1.2、右连接

右连接就是先把右表中所有记录都查询出来,然后左表满足条件的显示,不满足显示NULL。

需求:

dept表中的40部门并不存在员工,但在右连接中,如果dept表为右表,那么还是会查出40部门,但相应的员工信息为NULL。
1
mysql复制代码SELECT * FROM emp e RIGHT OUTER JOIN dept d ON e.deptno=d.deptno

9.4.2、注意

内连接说明两张表是平等的,没有主副之分。


外连接说明有一张表是主表,另一张表是副表。


**在开发中外连接居多,因为内连接查询的数据会丢失。**

9.5、三张表连接

1
2
3
4
5
6
7
8
9
10
11
mysql复制代码select ...
from
...
join
...
on
...
join
...
on
...(条件)

9.6、union

union关键字可以用于将查询结果集相加。他是连接两个查询结果的,可以用于两张不相干的表中的数据拼接在一起显示。


**注意:union必须用于两张列数相同的表进行查询,否则无法显示。**

案例

查询工作岗位是MANAGER和SALESMAN的员工
1
2
3
mysql复制代码select ename,job from  emp where job = 'MANAGER'
union
select ename,job from emp where job = 'SALESMAN'

十、子查询

10.1、子查询概述

select语句中嵌套select语句,被嵌套的select语句就是子查询,他可以出现的位置有select、from、where后。

10.2、where子句中使用

案例

​ 找出高于平均薪资的员工信息

1
2
3
mysql复制代码select * 
from emp
where sal > (select avg(sal) from emp);

10.3、from字句后使用

案例

找出每个部门平均薪水的薪资等级。
  1. 先找出每个部门的平均薪水(按照部门编号分组,求sal的平均值)
1
mysql复制代码select deptno,avg(sal) avgsal from emp group by deptno
  1. 将以上的查询结果作为临时表t,让t表和salgrade(薪水等级表) s连接,条件是:t.avgsal between s.losal and s.hisal
1
2
3
4
5
6
7
8
mysql复制代码select 
t.*,s.grade
from
(select deptno,avg(sal) avgsal from emp group by deptno) t
join
salgrade s
on
t.avgsal between s.losal and s.hisal

10.4、在select后使用

需求

找出每个员工所在的部门名称,要求显示员工名和部门名。
1
2
3
4
5
mysql复制代码select 
e.ename,
(select d.dname from dept d where e.deptno = d.deptno) dname
from
emp e;

十一、事务

11.1、什么是事务

一个事务是一个i完整的业务逻辑单元,不可再分。事务可以保证多个操作原子性,要么全成功,要么全失败。对于数据库来说事务保证批量的DML要么全成功,要么全失败。

​ 和事务相关的语句只有DML语句,因为他们这三个语句都是和数据库表中的数据相关的。事务的存在是为了保证数据的完整性、安全性。

11.2、开启事务的原理

假设我们完成一个操作,需要先执行一条insert,然后再执行一条update,最后执行一条delete,在mysql中执行流程可以这么理解:

在这里插入图片描述

11.3、事务的特征

事务具有四个特征ACID
  1. 原子性(Atomicity)
事务是最小的工作单元,不可再分。整个事务中的所有操作,必须作为一个单元全部完成(取消)。
  1. 一致性(Consistency)
事务必须保证多条DML语句同时成功或者同时失败。
  1. 隔离性(Isolation)
一个事务不会影响其他事务的运行。
  1. 持久性(Durability)
最终该事务对数据库所作的更改将持久地保存在硬盘文件之中,事务才算成功。


 **MySQL事务默认情况下是自动提交的**,可以通过命令来改成手工提交。
1
mysql复制代码start transaction;

11.4、隔离性详解

11.4.1、并发访问可能导致的问题

1
shell复制代码####  11.4.1.1、脏读取
一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交,这就出现了脏读取。

11.4.1.2、不可重复读

在同一个事务中,同一个读操作对同一个数据的前后两次读取产生了不同的结果,这就是不可重复读。

11.4.1.3、幻读

幻像读是指在同一个事务中以前没有的行,由于其他事务的提交而出现的新行。幻读强调的是前后读的行数不一样。

11.4.2、隔离级别

InnoDB 实现了四个隔离级别,用以控制事务所做的修改,并将修改通告至其它并发的事务。隔离级别从低往高依次是:
  1. 读未提交(READ UMCOMMITTED)
  2. 读已提交(READ COMMITTED)
  3. 可重复读(REPEATABLE READ) MySQL默认
  4. 串行化(SERIALIZABLE)

在这里插入图片描述

11.4.2.1、读未提交

对方的事务还没有提交,我们当前事务可以读取到对方未提交的数据。这种隔离级别是最低的,**读为未提交存在脏读现象,表示堵到了脏数据。**

11.4.2.2、读已提交

对方事务提交之后的数据我们才可以读到,这种隔离级别解决了脏读现象,但是却出现了不可重复读现象。


**这个级别是oracle的默认隔离级别。**

11.4.2.3、可重复读

我们无法看到已提交的事务了,这种隔离级别虽然解决了不可重复读的问题,但是却带来了幻读的问题。比方说一个线程删除了数据库中的所有数据,但是我们依然读取的是原来的数据,读到的是数据库的备份。

MySQL的默认级别。

11.4.2.4、串行化

将一个事务与其他事务完全地隔离。两个事务不可以并发,线程之间需要排队,也叫作序列化。虽然很安全,但是性能很低且客户的体验不好。

十二、索引

12.1、什么是索引

索引相当于一本书的目录,通过目录可以快速找到对应的资源。**索引被用来快速找出在一个列上用一特定值的行,索引可以有效地缩小扫描的范围。添加索引是给某个字段或者是某些字段添加的。**


在数据库方面,查询一张表的时候有两种检索方式:
  1. 全表扫描
  2. 根据索引检索(效率高)
索引虽然可以提高检索的效率,但是不能随意添加索引,因为索引也是数据库中的对象,也需要数据库不断地维护,维护需要成本的。比如表中的的数据如果经常被修改的话就不适合添加索引,因为数据一旦被修改,索引需要重新排序。

12.2、什么时候需要创建索引

  1. 数据量庞大。
  2. 该字段很少的DML操作(因为字段进行修改操作,索引也需要维护)。
  3. 该字段经常出现在where子句中(经常根据哪个字段查询)

​ 注意:主键和具有unique约束的字段会自动添加索引,根据主键查询的效率高,尽量根据主键索引,我们可以查询sql语句的执行计划。他的底层是B+Tree。

1
mysql复制代码 explain select * from emp where SAL = 1500;

image-20210106171850865
type字段的值时ALL表示是全表扫描(没有添加索引)。rows表示搜索了14条数据。

12.3、添加索引

1
2
3
4
5
mysql复制代码-- 给emp表的sal字段添加一个索引,名称为emp_sal_index
create index emp_sal_index on emp(sal);

-- 语法格式
create index 索引名称 on 表名(字段名)

在这里插入图片描述

12.4、查看索引

1
2
3
4
5
mysql复制代码-- 查看索引的语法
show index from emp;

-- 语法格式
show index from 表名;

在这里插入图片描述

12.5、删除索引

1
2
mysql复制代码-- 删除索引的语法
drop index 索引名称 on 表名;

12.6、索引的原理

索引底层采用的数据结构是B+Tree,通过B+Tress缩小扫描范围,底层索引进行排序、分区,索引会携带在表中的`物理地址`,最终通过索引检索到数据之后,获取到关联的物理地址,通过物理地址定位到表中的数据,效率是最高的(不走表,走硬盘)。
1
mysql复制代码select ename from emp where ename = 'SMITH';

​ 通过索引sql语句会转换

1
mysql复制代码select ename from emp where 物理地址 = '索引检索到的物理地址'

12.7、索引的分类

  1. 单一索引:给打那个字段添加索引。
  2. 复合索引:给多个字段联合起来添加索引。
  3. 主键索引:主键上会自动添加索引。
  4. 唯一索引:有unique约束的字段上会自动添加索引。

12.8、索引的失效

在模糊查询的时候,如果第一个通配符使用的是`%`,这个索引会失效,因为他不知道一开始匹配的字符是什么。

十三、视图

13.1、什么是视图

视图是一种根据查询(也就是SELECT表达式)定义的数据库对象,用于获取想要看到和使用的局部数据。所以他也称为虚拟表。


视图是站在不同的角度看到数据,同一张表的数据,通过不同的角度去看待数据。


我们可以对视图进行增删改查,会影响到原表的数据,通过视图来影响原表数据的,并不是直接操作原表。**只有DQL语句才可以以视图对象的方式创建出来。**

在这里插入图片描述

13.2、创建视图

1
2
3
4
5
mysql复制代码-- 语法格式
create view 视图名 as select语句

-- 示范
create view myview as select empo,ename from emp;

13.3、修改视图

1
2
mysql复制代码-- 语法格式
update 视图名 set 列名 = '值' where 条件;

13.4、删除视图

1
2
3
4
5
mysql复制代码-- 语法格式
delete from 视图名 where 条件;

-- 示范
delete from myview where empo = '12134';

13.5、视图的作用

视图可以隐藏表的实现细节,保密级别比较高的系统,数据库只对外提供相关的视图,面向视图对象进行CRUD。

十四、数据库设计三范式

设计范式是设计表的依据,按照这三个范式设计的表不会出现数据冗余。**但是在实际开发中,根据客户的需求,可能会拿数据冗余来换取执行速度,拿空间换时间。**

14.1、第一范式

任何一张表都应该有主键,且每一个字段原子性不可再分。

14.2、第二范式

建立在第一范式的基础上,所有非主键字段完全依赖于主键,不能产生部分依赖。

​ 典型的例子就是解决多对多的问题上,遇到多对多的时候,背口诀:多对多?三张表,关系表两外键

14.3、第三范式

建立在第二范式的基础上,所有非主键字段直接依赖主键,不能产生传递依赖。


典型的例子就是一对多,遇到一对多问题的时候背口诀:**一对多?两张表,多的表加外键。**

14.4、一对一关系的设计方案

14.4.1、主键共享

​ t_user_login 用户登录表

id(pk) username password
1 zs 123
2 ls 456

t_user_detail 用户详细信息表

id(pk+fk) realname tel
1 张三 111
2 李四 456

14.4.2、外键唯一

t_user_login 用户登录表

id(pk) username password
1 zs 123
2 ls 456

t_user_detail 用户详细信息表

id(pk) realname tel userid(fk+unique)
1 张三 111 2
2 李四 456 2

视图

1
2
3
4
5
mysql复制代码-- 语法格式
delete from 视图名 where 条件;

-- 示范
delete from myview where empo = '12134';

13.5、视图的作用

视图可以隐藏表的实现细节,保密级别比较高的系统,数据库只对外提供相关的视图,面向视图对象进行CRUD。

十四、数据库设计三范式

设计范式是设计表的依据,按照这三个范式设计的表不会出现数据冗余。**但是在实际开发中,根据客户的需求,可能会拿数据冗余来换取执行速度,拿空间换时间。**

14.1、第一范式

任何一张表都应该有主键,且每一个字段原子性不可再分。

14.2、第二范式

建立在第一范式的基础上,所有非主键字段完全依赖于主键,不能产生部分依赖。


典型的例子就是解决多对多的问题上,遇到多对多的时候,背口诀:**多对多?三张表,关系表两外键**

14.3、第三范式

建立在第二范式的基础上,所有非主键字段直接依赖主键,不能产生传递依赖。


典型的例子就是一对多,遇到一对多问题的时候背口诀:**一对多?两张表,多的表加外键。**

14.4、一对一关系的设计方案

14.4.1、主键共享

​ t_user_login 用户登录表

id(pk) username password
1 zs 123
2 ls 456

t_user_detail 用户详细信息表

id(pk+fk) realname tel
1 张三 111
2 李四 456

14.4.2、外键唯一

t_user_login 用户登录表

id(pk) username password
1 zs 123
2 ls 456

t_user_detail 用户详细信息表

id(pk) realname tel userid(fk+unique)
1 张三 111 2
2 李四 456 2

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

解决数据库和缓存数据不一致情况:延迟双删

发表于 2021-07-17

在高并发的场景下,数据库处理数据增删改查很是薄弱。有一些数据查询的频率远大于修改频率,就需要使用缓存技术,让先去请求redis,redis存在返回缓存数据,redis不存在就查询数据库,返回数据的同时将数据缓存到redis中。

问题

读取缓存一般没有什么问题,一旦涉及到数据更新:数据库或者缓存更新,就容易出现缓存和数据库数据不一致情况。首先,数据“一致性”包含两种情况:

  1. 缓存有数据,那么缓存的值和数据库中的值相同。
  2. 缓存没有数据,那么,数据库中的值必须是最新值。

在高并发的情况下,不管是先写数据库,再删缓存;还是先删缓存,再写数据库,都有可能出现数据不一致的情况,比如:

  1. 如果删除了缓存redis,还没来得及写库mysql,另一个线程就读取,发现缓存为空,则去数据库读取数据写入缓存,此时缓存中的数据为脏数据。
  2. 如果写了库,在删除缓存前,写库的线程宕机了,也会出现数据不一致的情况。

解决办法

延迟双删策略

1、先删除缓存
2、再写数据库
3、休眠500ms(根据统计线程读取数据和写缓存的时间)
(休眠的作用是当前线程等其他线程读完了数据后写入缓存后,删除缓存)
4、再删除缓存

设置缓存过期时间

总结

先清除缓存,然后再写入数据库。有可能存在删除缓存以后,另一个线程读取数据,发现没有数据,就去数据读取数据,然后写入缓存中,此时缓存中的数据为脏数据;
解决办法:

  1. 先删除缓存
  2. 再写入数据库
  3. 休眠500ms
  4. 删除缓存
    其中第三步骤的500ms,是根据业务读取数据平均耗时,这样做的目的是确保读请求可以结束,写请求可以删除读请求造成的脏数据的问题。

本文由博客一文多发平台 OpenWrite 发布!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot整合SpringSecurity系列(1

发表于 2021-07-16

一、Thymeleaf-Security

  1. Spring Security 可以在一些视图技术中进行控制显示效果,如JSP 或 Thymeleaf
  2. 在非前后端分离且使用 Spring Boot 的项目中大多使用 Thymeleaf 作为视图展示技术
  3. Thymeleaf 对 Spring Security 的支持都放在hymeleaf-extras-springsecurityX中,目前最新版本为 5。所以需要在项目中添加此 jar 包的依赖和 thymeleaf 的依赖
1
2
3
4
5
6
7
8
9
xml复制代码<!-- thymeleaf依赖 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  1. 在 html 页面中引入thymeleaf命名空间和security命名空间,引用命名空间之后才能使用对应属性信息
1
2
3
4
5
html复制代码<!DOCTYPE html>
<html lang="zh"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
</html>
  1. 配置文件中配置thymeleaf相关属性,默认属性参考类
    • org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码spring:
# Thymeleaf
thymeleaf:
# 关闭缓存
cache: false
# 开启引擎
enabled: true
# 视图模型
mode: HTML
# 指定后缀
suffix: .html
# 模板路径
prefix: classpath:/templates/

二、Thymeleaf获取属性

2.1 获取属性

  1. 可以在html页面中通过 sec:authentication=”” 获取 UsernamePasswordAuthenticationToken 中以及父类中的属性
    • org.springframework.security.authentication.UsernamePasswordAuthenticationToken

image.png
2. 源码属性如下

  • name :登录账号名称
  • principal:登录主体,在自定义登录逻辑中是 UserDetails
  • credentials:凭证信息
  • authorities:权限和角色
  • details:WebAuthenticationDetails实例,可以获取 remoteAddress (客户端ip)和 sessionId (当前 sessionId)

image.png
3. 在 /templates/ 新建 security.html 用于显示上述所述属性信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
html复制代码<!DOCTYPE html>
<html lang="zh"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>权限信息</title>
</head>
<body>
<ol>
<!-- 属性获取,authentication:n. 证明;鉴定;证实 -->
<li>登录账号:<span sec:authentication="name"></span></li>
<!-- principal相当于UserDetails信息 -->
<li>登录账号:<span sec:authentication="principal.username"></span></li>
<li>登录密码:<span sec:authentication="principal.password"></span></li>
<li>账号过期:<span sec:authentication="principal.accountNonExpired"></span></li>
<li>账号锁定:<span sec:authentication="principal.accountNonLocked"></span></li>
<li>凭证过期:<span sec:authentication="principal.credentialsNonExpired"></span></li>
<li>账号启用:<span sec:authentication="principal.enabled"></span></li>
<li>凭证:<span sec:authentication="credentials"></span></li>
<li>权限和角色:<span sec:authentication="authorities"></span></li>
<!-- WebAuthenticationDetails实例 -->
<li>客户端地址:<span sec:authentication="details.remoteAddress"></span></li>
<li>sessionId:<span sec:authentication="details.sessionId"></span></li>
</ol>
</body>
</html>
  1. 在控制器中添加转发路径,必须要通过转化,直接访问无法获取属性
1
2
3
4
java复制代码@RequestMapping("/security")
public String security() {
return "security";
}
  1. 正常访问项目,可以显示如下权限信息
1
2
3
4
5
6
7
8
9
10
11
ruby复制代码登录账号:admin
登录账号:admin
登录密码:$2a$10$.9UpYAxTDg/cd8U7wtal5et7TcC7QaInySM1p8tBEp.OO20UvjR/S
账号过期:true
账号锁定:true
凭证过期:true
账号启用:true
凭证:
权限和角色:[{"authority":"ROLE_USER","id":3,"userId":2}]
客户端地址:0:0:0:0:0:0:0:1
sessionId:CAD8A5A28BC2EBDDF0990111BC86C3CA

2.2 权限判断

  1. 可以在html页面中通过 sec:authorize=”” 进行鉴权,用来控制是否对应用户具备对应菜单
  2. 在security.html中增加权限相关信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
html复制代码<!DOCTYPE html>
<html lang="zh"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>权限信息</title>
</head>
<body>
<ol>
<!-- 属性获取,authentication:n. 证明;鉴定;证实 -->
<li>登录账号:<span sec:authentication="name"></span></li>
<!-- principal相当于UserDetails信息 -->
<li>登录账号:<span sec:authentication="principal.username"></span></li>
<li>登录密码:<span sec:authentication="principal.password"></span></li>
<li>账号过期:<span sec:authentication="principal.accountNonExpired"></span></li>
<li>账号锁定:<span sec:authentication="principal.accountNonLocked"></span></li>
<li>凭证过期:<span sec:authentication="principal.credentialsNonExpired"></span></li>
<li>账号启用:<span sec:authentication="principal.enabled"></span></li>
<li>凭证:<span sec:authentication="credentials"></span></li>
<li>权限和角色:<span sec:authentication="authorities"></span></li>
<!-- WebAuthenticationDetails实例 -->
<li>客户端地址:<span sec:authentication="details.remoteAddress"></span></li>
<li>sessionId:<span sec:authentication="details.sessionId"></span></li>
</ol>

<div>
<!-- 权限判断,authorize:vt. 批准,认可;授权给;委托代替 -->
通过权限判断:
<button sec:authorize="hasAuthority('ROLE_ADMIN')">新增</button>
<button sec:authorize="hasAuthority('ROLE_ADMIN')">删除</button>
<button sec:authorize="hasAuthority('ROLE_ADMIN')">修改</button>
<button sec:authorize="hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('ADMIN')">新增</button>
<button sec:authorize="hasRole('ADMIN')">删除</button>
<button sec:authorize="hasRole('ADMIN')">修改</button>
<button sec:authorize="hasAnyRole('ADMIN', 'USER')">查看</button>
</div>
</body>
</html>
  1. 和 access() 中表达式使用一致,分别使用admin和root用户登录,可以看到菜单部分对于没有权限的菜单并不会显示

image.png
4. 对于非前后分离的项目来说,菜单权限控制比较简单;对于前后分离的菜单不能直接使用到thymeleaf中对于security的支持,需要自定处理权限管理

三、thymeleaf解决csrf

  1. 在 /templates/ 中新增 login.html,添加隐藏域name值为 _csrf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html复制代码<!DOCTYPE html>
<html lang="zh"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>系统登录</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username" value="admin" /><br/>
密码:<input type="password" name="password" value="tianxin" /><br/>
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
记住我:<input type="checkbox" name="remember-me" value="true" /><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
  1. 添加在controller中添加转化请求
1
2
3
4
java复制代码@RequestMapping("/csrfLogin")
public String csrfLogin() {
return "login";
}
  1. 在登录控制中需要放行 /csrfLogin 路径的访问
1
2
3
java复制代码http.authorizeRequests()
// csrf登录放行
.antMatchers("/csrfLogin").permitAll();
  1. 安全配置类中注释掉csrf相关代码
1
2
java复制代码// 关闭csrf保护
// http.csrf().disable();
  1. 使用csrfLogin访问登录页面,正常输入账号和密码之后系统正常登录
    • http://localhost:8888/csrfLogin

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot整合SpringSecurity系列(1

发表于 2021-07-16

一、基于注解访问控制

  1. Spring Security 中提供了一些访问控制的注解,这些注解默认不可用,需要通过 @EnableGlobalMethodSecurity 进行开启后使用,如果设置的条件允许则程序正常执行,反之不允许会报 500(AccessDeniedException异常)
    • org.springframework.security.access.AccessDeniedException
1
2
3
4
5
java复制代码@Configuration
@EnableGlobalMethodSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
  1. @EnableGlobalMethodSecurity开启之后,外有三大注解可以写到 Service 接口或方法上,也可以写到 controller或 controller 的方法上,通常情况下都是写在控制器方法上,控制接口URL是否允许被访问
  2. 新建annotation.html,用于注解控制
1
2
3
4
5
6
7
8
9
10
html复制代码<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>SpringBoot Security</title>
</head>
<body>
<h3>欢迎登录SpringBoot Security 注解控制首页</h3>
</body>
</html>
  1. 在WebSecurityConfigurerAdapter配置类上 @EnableGlobalMethodSecurity 开启注解即可开启全局方法注解功能

二、三大注解控制方法

2.1 @Secured

  1. @Secured专门用于判断是否具有指定角色,可写在方法或类上,注意参数以 ROLE_ 开头,由于只能判断角色,所以实际中使用并不多
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Secured {

/**
* Returns the list of security configuration attributes (e.g.&nbsp;ROLE_USER,
* ROLE_ADMIN).
* @return String[] The secure method attributes
*/
String[] value();
}
  1. 在 @EnableGlobalMethodSecurity 注解功能类上通过指定参数 securedEnabled = true 开启 @Secured 功能
    • @EnableGlobalMethodSecurity(securedEnabled = true)
1
2
3
4
5
java复制代码@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
  1. 在控制器方法上添加@Secured注解,要求具备 ROLE_ADMIN 权限才能访问
1
2
3
4
5
6
7
8
java复制代码/**
* 注解权限访问
*/
@RequestMapping("/toAnnotation")
@Secured("ROLE_ADMIN")
public String annotation() {
return "redirect:/annotation.html";
}
  1. 在SecurityConfig中配置对应的权限
    • successForwardUrl指定到对应的处理器
1
2
3
4
5
6
7
java复制代码http.formLogin()
// 指定登录页面,/不能舍弃
.loginPage("/login.html")
// 表单提交路径,和登录表单配置一样
.loginProcessingUrl("/login")
// 注解访问控制
.successForwardUrl("/toAnnotation");
  1. 启动项目,然后测试有权限和无权限
    • 有权限

在这里插入图片描述

  • 无权限

在这里插入图片描述

  • 并且控制台抛出对应异常

image.png

2.2 @PreAuthorize

  1. @PreAuthorize是方法或类级别注解,表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式
  2. 使用时需要在 @EnableGlobalMethodSecurity 注解功能类上通过指定参数 prePostEnabled= true 开启 @PreAuthorize 功能
1
2
3
4
5
java复制代码@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
  1. 在控制器方法上添加@PreAuthorize,参数可以是任何 access() 支持的表达式
1
2
3
4
5
java复制代码@RequestMapping("/preAuthorize")
@PreAuthorize("hasRole('ADMIN')")
public String preAuthorize() {
return "redirect:/annotation.html";
}
  1. 访问接口,分别使用admin和root访问,结果和@Secured一致
    • http://localhost:8888/preAuthorize

2.3 @PostAuthorize

  1. @PostAuthorize是方法或类级别注解,表示方法或类执行结束后判断权限,此注解很少被使用到
  2. 使用时需要在 @EnableGlobalMethodSecurity 注解功能类上通过指定参数 prePostEnabled= true 开启 @PostAuthorize功能
1
2
3
4
5
java复制代码@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
  1. 在控制器方法上添加@PreAuthorize,参数可以是任何access()支持的表达式
1
2
3
4
5
java复制代码@RequestMapping("/postAuthorize")
@PostAuthorize("hasRole('ADMIN')")
public String postAuthorize() {
return "redirect:/annotation.html";
}
  1. 访问接口,分别使用admin和root访问,结果和@Secured一致
    • http://localhost:8888/postAuthorize
  2. 由于是方法和类执行结束后才判断的权限,而实际中通常是在执行前做判断,不然权限拦截就没有实际意义

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot集成Elasticsearch 进阶,实

发表于 2021-07-16

​

查了很多关于es 拼音分词器的文章,有价值的的不是很多,还是自己写一篇吧

1、定义

分词分为读时分词和写时分词。

读时分词发生在用户查询时,ES 会即时地对用户输入的关键词进行分词,分词结果只存在内存中,当查询结束时,分词结果也会随即消失。而写时分词发生在文档写入时,ES 会对文档进行分词后,将结果存入倒排索引,该部分最终会以文件的形式存储于磁盘上,不会因查询结束或者 ES 重启而丢失。

写时分词器需要在 mapping 中指定,而且一经指定就不能再修改,若要修改必须新建索引。

分词一般在ES中有分词器处理。英文为Analyzer,它决定了分词的规则,Es默认自带了很多分词器,如:

Standard、english、Keyword、Whitespace等等。默认的分词器为Standard,通过它们各自的功能可组合

成你想要的分词规则。分词器具体详情可查看官网:分词器

另外,在常用的中文分词器、拼音分词器、繁简体转换插件。国内用的就多的分别是:

elasticsearch-analysis-ik

elasticsearch-analysis-pinyin

elasticsearch-analysis-stconvert

2、插件安装

(插件需要下载自己es对应版本的插件,我得es版本是6.6.1,为了稳定没有采用7.x版本)

将下载的项目并用maven打包,在项目target文件夹下会生成elasticsearch-analysis-pinyin-6.6.1.zip

将压缩包 放到es plugins中,解压重命名analysis-pinyin ,重启es。

​

3、配置

自定义setting

在resources目录下创建 elasticsearch_setting.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
json复制代码{
"index": {
"analysis": {
"analyzer": {
"pinyin_analyzer": {
"tokenizer": "my_pinyin"
}
},
"tokenizer": {
"my_pinyin": {
"type": "pinyin",
//true:支持首字母
"keep_first_letter": true,
//false:首字母搜索只有两个首字母相同才能命中,全拼能命中
//true:任何情况全拼,首字母都能命中
"keep_separate_first_letter": false,
//true:支持全拼 eg: 刘德华 -> [liu,de,hua]
"keep_full_pinyin": true,
"keep_original": true,
//设置最大长度
"limit_first_letter_length": 16,
"lowercase": true,
//重复的项将被删除,eg: 德的 -> de
"remove_duplicated_term": true
}
}
}
}
}

在resources目录下创建 elasticsearch_mapping.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
json复制代码{
"block": {
"properties": {
"preClosePx": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"blockTypeName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
} ,
"blockId": {
"type": "keyword",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"blockName": {
"type": "text",
"analyzer": "pinyin_analyzer",
"search_analyzer": "pinyin_analyzer",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}

实体设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
less复制代码@ToString
@Getter
@Setter
@Mapping(mappingPath = "elasticsearch_mapping.json")//设置mapping
@Setting(settingPath = "elasticsearch_setting.json")//设置setting
@Document(indexName = "info", type = "block", shards = 5, replicas = 1)
public class BlockInfoItem {
/**
* id
*/
@Id
private String id;

@Field(type = FieldType.Keyword)
private String preClosePx;

@Field(type = FieldType.Text)
private String blockTypeName;

@Field(type = FieldType.Text,analyzer = "pinyin_analyzer",searchAnalyzer = "pinyin_analyzer")
private String blockName;

@Field(type = FieldType.Keyword)
private String blockId;
}

关注我的微信公众号

)​

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

怎么提高自己的系统架构水平 背景常规业务系统设计关键——领域

发表于 2021-07-16

系统设计与架构理论这个问题,回答起来非常宽泛,基本所有的技术理论都可以涵盖。作为一个撸代码快 10 年的后端技术人员,简单发表一下我的看法。

系统设计与架构,与系统的业务类型关联还是很大的,比如传统的业务系统主要关注的是领域建模设计,高并发、高可用、数据一致性等系统,在设计的时候会与业务系统有较大的差别,所以这里针对不同类型的系统,来简单介绍一下设计的时候面临的一些难点与解决方案。

背景常规业务系统设计关键——领域模型

业务系统设计的关键是在于如何定义系统的模型以及模型之间的关系,其中主要是领域模型的定义,当我们在模型确定之后,模型之间的关系也会随之明确。

模型设计可以参考领域模型的经典书籍《Domain-Driven Design》一书,通过这个基本可以对领域定义、防腐层、贫血模型等概念有一个较为清晰的认识了。

单个应用内的领域模型系统也需要注意领域分层,作为开发大家是不是见过、重构过很多Controller-Service-DAO 样式的代码分层设计?往往在在做重构的时候会令人吐血。

设计较好的领域设计这里给一个分层建议:

接口层 Interface

主要负责与外部系统进行交互&通信,比如一些 dubbo服务、Restful API、RMI等,这一层主要包括 Facade、DTO还有一些Assembler。

应用层 Application

这一层包含的主要组件就是 Service 服务,但是要特别注意,这一层的Service不是简单的DAO层的包装,在领域驱动设计的架构里面,Service层只是一层很“薄”的一层,它内部并不实现任何逻辑,只是负责协调和转发、委派业务动作给更下层的领域层。

领域层 Domain

Domain 层是领域模型系统的核心,负责维护面向对象的领域模型,几乎全部的业务逻辑都会在这一层实现。内部主要包含Entity(实体)、ValueObject(值对象)、Domain Event(领域事件)和 Repository(仓储)等多种重要的领域组件。

基础设施层 Infrastructure

它主要为 Interfaces、Application 和 Domain 三层提供支撑。所有与具体平台、框架相关的实现会在 Infrastructure 中提供,避免三层特别是 Domain 层掺杂进这些实现,从而“污染”领域模型。Infrastructure 中最常见的一类设施是对象持久化的具体实现。

高并发系统设计

在面试中是不是经常被问到一个问题:如果你系统的流量增加 N 倍你要怎么重新设计你的系统?这个高并发的问题可以从各个层面去解,比如:

代码层面

  • 锁优化(采用无锁数据结构),主要是 concurrent 包下面的关于 AQS 锁的一些内容
  • 数据库缓存设计(降低数据库并发争抢压力),这里又会有缓存、DB 数据不一致的问题,在实际使用中,高并发系统和数据一致性系统采用的策略会截然相反。
  • 数据更新时采用合并更新,可以在应用层去做更新合并,同一个 Container 在同一时间只会有一个 DB 更新请求。
  • 其他的比如基于 BloomFilter 的空间换时间、通过异步化降低处理时间、通过多线程并发执行等等。

数据库层面

  • 根据不同的存储诉求来进行不同的存储选型,从早期的 RDBMS,再到 NoSql(KV存储、文档数据库、全文索引引擎等等),再到最新的NewSql(TiDB、Google spanner/F1 DB)等等。
  • 表数据结构的设计,字段类型选择与区别。
  • 索引设计,需要关注聚簇索引原理与覆盖索引消除排序等,至于最左匹配原则都是烂大街的常识了,高级一点索引消除排序的一些机制等等,B+树与B树的区别。
  • 最后的常规手段:分库分表、读写分离、数据分片、热点数据拆分等等,高并发往往会做数据分桶,这里面往深了去说又有很多,比如分桶如何初始化、路由规则、最后阶段怎么把数据合并等等,比较经典的方式就是把桶分成一个主桶+N个分桶。

架构设计层面

  • 分布式系统为服务化
  • 无状态化支持水平弹性扩缩容
  • 业务逻辑层面 failfast 快速失败
  • 调用链路热点数据前置
  • 多级缓存设计
  • 提前容量规划等等

高可用系统设计

对于可用性要求非常高的系统,一般我们都说几个9的可用率,比如 99.999% 等。

面对高可用系统设计也可以从各个方面来进行分析

代码层面:需要关注分布式事务问题,CAP理论是面试的常规套路

软件层面:应用支持无状态化,部署的多个模块完全对等,请求在任意模块处理结果完全一致 => 模块不存储上下文信息,只根据请求携带的参数进行处理。目的是为了快速伸缩,服务冗余。常见的比如session问题等。

负载均衡问题

软件部署多份之后,如何保证系统负载?如何选择调用机器?也就是负载均衡问题

狭义上的负载均衡按照类型可以分为这几种:

  1. 硬件负载:比如F5等
  2. 软件负载:比如 LVS、Ngnix、HaProxy、DNS等。
  3. 当然,还有代码算法上的负载均衡,比如Random、RoundRobin、ConsistentHash、加权轮训等等算法

广义上的负载均衡可以理解为负载均衡的能力,比如一个负载均衡系统需要如下4个能力:

  1. 故障机器自动发现
  2. 故障服务自动摘除(服务熔断)
  3. 请求自动重试
  4. 服务恢复自动发现

幂等设计问题

上面提负载均衡的时候,广义负载均衡需要完成自动重试机制,那么在业务上,我们就必须保证幂等设计。

这里可以从2个层面来进行考虑:

请求层面

由于请求会重试所以必须做幂等,需要保证请求重复执行和执行一次的结果完全相同。请求层面的幂等设计需要在数据修改的层做幂等,也就是数据访问层读请求天然幂等,写请求需要做幂等。读请求一般是天然幂等的,无论查询多少次返回的结果都是一致。这其中的本质实际上是分布式事务问题,这里下面再详细介绍。

业务层面

不幂等会造成诸如奖励多发、重复下单等非常严重的问题。业务层面的幂等本质上是分布式锁的问题,后面会介绍。如何保证不重复下单?这里比如token机制等等。如何保证商品不超卖?比如乐观锁等。MQ消费方如何保证幂等等都是面试的常见题。

分布式锁

业务层面的幂等设计本质上是分布式锁问题,什么是分布式锁?分布式环境下锁的全局唯一资源,使请求串行化,实际表现互斥锁,解决业务层幂等问题。

常见的解决方式是基于 Redis 缓存的 setnx 方法,但作为技术人员应该清楚这其中还存在单点问题、基于超时时间无法续租问题、异步主从同步问题等等,更深一点,CAP理论,一个AP系统本质上无法实现一个CP需求,即使是 RedLock 也不行。

那我们如何去设计一个分布式锁呢?强一致性、服务本身要高可用是最基本的需求,其他的比如支持自动续期,自动释放机制,高度抽象接入简单,可视化、可管理等。

基于存储层的可靠的解决方案比如:

zookeeper

CP/ZAB/N+1可用:基于临时节点实现和Watch机制。

ETCD

CP or AP/Raft/N+1可用:基于 restful API;KV存储,强一致性,高可用,数据可靠:持久化;Client TTL 模式,需要心跳CAS 唯一凭证 uuid。

服务的熔断

微服务化之后,系统分布式部署,系统之间通过 RPC 通讯,整个系统发生故障的概率随着系统规模的增长而增长,一个小的故障经过链路传导放大,有可能造成更大的故障。希望在调用服务的时,在一些非关键路径服务发生服务质量下降的情况下,选择尽可能地屏蔽所造成的影响

服务降级

服务整体负载超出预设的上限,或者即将到来的流量预计将会超过阀值,为了保证重要或者基本的服务能够正常运行,拒绝部分请求或者将一些不重要的不紧急的服务或任务进行服务的延迟使用或暂停使用。

主要的手段如下:

服务层降级,主要手段

  1. 拒绝部分请求(限流),比如缓存请求队列,拒绝部分等待时间长的请求;根据Head,来拒绝非核心请求;还有其他通用算法上的限流比如令牌桶、漏桶算法等等。
  2. 关闭部分服务:比如双11大促0点会关闭逆向退款服务等等。
  3. 分级降级:比如自治式服务降级,从网关到业务到DB根据拦截、业务规则逐渐降低下游请求量,体现上是从上到下的处理能力逐渐下降。

数据层降级

比如流量大的时候,更新请求只缓存到MQ,读请求读缓存,等流量小的时候,进行补齐操作(一般数据访问层如果做了降级,就没必要在数据层再做了)

柔性可用策略

比如一些指定最大流量的限流工具,又或是根据CPU负载的限流工具等,需要保证自动打开,不依赖于人工。

发布方式引发的可用性问题

发布方式也是影响高可用的一个点,哈哈,以前还经历过一些线上直接停机发布的案例(银行内部系统),不过作为高大上的互联网,主要会采用这几种发布方式:灰度发布、蓝绿发布、金丝雀发布等等。

数据一致性系统设计

一般一些金融、账务系统对这一块要求会非常严格,下面主要介绍下这里面涉及到的事务一致性、一致性算法等内容。

事务一致性问题

在 DB 层面,一般通过 刚性事务 来实现数据一致性,主要通过 预写日志(WAL) 的方式来实现,WAL(write ahead logging)预写日志的方式。就是所有对数据文件的修改,必须要先写日志,这样,即使在写数据的时候崩溃了,也能通过日志文件恢复,传统的数据库事务就是基于这一个机制(REDO 已提交事务的数据也求改 UNDO 未提交事务的回滚)。

除了这个方式之外,还有一个就是通过 影子数据块 来进行数据备份,提前记录被修改的数据块的修改前的状态,备份起来,如果需要回滚,直接用这个备份的数据块进行覆盖就好了。

其他的就是基于二阶段提交的 XA模型 了。

但是目前互联网系统,已经广泛采用分布式部署模式了,传统的刚性事务无法实现,所以 柔性事务成了目前主流的分布式事务解决防范,主要的模式有下面几种:

TCC 模式/或者叫2阶段模式

在 try 阶段预扣除资源(但是不锁定资源,提升可用性),在Confirm 或者 Cancel 阶段进行数据提交或者回滚。一般需要引入协调者,或者叫事务管理器。

SAGA模式

业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,支持向前或者向后补偿。

MQ的事务消息

就是先发 halfMsg,在处理完之后,再发送 commit 或者 rollback Msg,然后 MQ 会定期询问 producer ,halfMsg 能不能 commit 或者 rollback,最终实现事务的最终一致性。实际上是把补偿的动作委托给了 RocketMQ。

分段事物(异步确保)

基于可靠消息+本地事务消息表 + 消息队列重试机制。目前这也是一些大厂的主流方案,内部一般称为分段事物。

柔性事务基本都是基于最终一致性去实现,所以肯定会有 补偿 动作在里面,在达到最终一致性之前,对用户一般展示 软状态。

需要注意的一点是,并不是所有的系统都适合引入数据一致性框架,比如用户可以随时修改自己发起的请求的情况,例如,商家设置后台系统,商户会随时修改数据,这里如果涉及到一致性的话,引入一致性框架会导致补偿动作达到最终一致性之前,资源锁会阻塞用户后续的请求。导致体验较差。这种情况下就需要通过其他手段来保障数据一致性了,比如数据对账等操作。

一致性算法

从早期的 Paxos 算法,再到后面衍生的 zab 协议(参考:A simple totally ordered broadcast protocol),提供了当下可靠的分布式锁的解决方案。再到后来的 Raft 算法(In Search of an Understandable Consensus Algorithm),也都是分布式系统设计里面需要了解到的一些知识要点。

最后

这里简单介绍了不同系统设计的时候会面临的一些难点,基本里面每一个点,都是前人在解决各种疑难问题的道路上不断探索,最终才得出的这些业界解决方案,呈现在大家眼前,作为一个技术人员,学会这些技术点只是时间问题,但这种发现问题、直面问题、再到解决问题的能力和精神才是我们最值得学习的地方,也是做为一个系统设计人员或者说是架构师的必要能力。

————————————————————————————————————————————

作者|勇剑

编辑|橙子君

出品|阿里巴巴新零售淘系技术

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

cookie、session和jwt原理解析

发表于 2021-07-16

简介

  • 由于http协议是无状态的协议,为了保存用户登录状态和区分用户,一般使用cookie、session、jwt进行身份认证
  • cookie是在http-header中的(不宜过大,减少传输数据量)
  • 可以通过浏览器添加cookie,服务端也可以设置cookie, 每次请求都会携带cookie (每次请求都携带,浪费流量) 合理设置
  • cookie 默认不能跨域 (两个完全不同的域名 父子域名(可以设置子域能拿到父域中的数据), cookie存在前端里
  • session在服务器里的,默认浏览器是拿不到的,session可以存放数据原则上没有上线,而且安全,基于cookie的 session默认都是存在内存中的(如果服务器宕掉了,session就丢失了)
  • jwt这种方案,服务根据用户提供的信息生成一个令牌。每次带带上令牌和你的信息,用你的信息再次生成令牌做对比 (里面不能存放隐私)

cookie

特点

  • 只支持存储字符串类型的value
  • 一般单个Cookie保存的数据不能超过4K
  • 客户端计算机暂时或永久保存的信息
  • 浏览器每次请求服务器,会默认携带当前域名下的cookie,放在http的请求头中,由于每次请求都携带,应合理设置,减少流量浪费
  • 由于是存储在浏览器中,可能被人篡改,可以加入签名验证提高安全性
  • 在浏览器中是以字符串形式保存的使用;进行分隔,js通过document.cookie进行获取,同时可以添加一些options
  • xsrf攻击就是利用了浏览器请求自动携带cookie这个特征进行攻击的

options

  • name 表示cookie的名字,一个域名下的cookie名不能相同,相同会被覆盖
  • value 表示cookie的值
  • domain cookie绑定的域名,如果没有设置,就会自动绑定到执行语句的当前域,可以设置这个属性为父域名实现不同子域的读写
  • path 指定路径,如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read。默认为/通常不设置,如果设置了局限性很大
  • secure 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
  • expires/max-age 存活时间
  • httpOnly 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全

在node原生中设置cookie

1
js复制代码 res.setHeader('Set-Cookie', ['name=zf', 'age=12; domain=.zf.cn; httpOnly=true']);

在koa中使用cookie

实现cookie中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
js复制代码// 删除=,将+转化为-,将/转化为_
const toBase64URL = (str) => {
return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//, "_");
};
app.use(async (ctx, next) => {
// 保存要发送的cookie
const cookies = [];
ctx.myCke = {
// 设置cookie的方法,key:cookie名 value:cookie名 options:其他配置例如httpOnly/domain/max-age等
set(key, value, options = {}) {
// 其他配置的数组
let optsArr = [];
// 如果设置了domain就添加上这个配置
if (options.domain) {
optsArr.push(`domain=${options.domain}`);
}
if (options.httpOnly) {
optsArr.push(`httpOnly=${options.httpOnly}`);
}
if (options.maxAge) {
optsArr.push(`max-age=${options.maxAge}`);
}
// 配置了加密签名
if (options.signed) {
// base64 在传输的时候 会 把 + / = 做特殊处理
const salt = [key, value].join("=");
let sign = toBase64URL(crypto.createHmac("sha1", secret).update(salt).digest("base64"));
cookies.push(`${key}.sign=${sign}`);
}
// 将cookie的配置放入cookies
cookies.push(`${key}=${value}; ${optsArr.join("; ")}`);
console.log(cookies);
// 设置响应头,返回cookie
ctx.res.setHeader("Set-Cookie", cookies);
},
get(key, options) {
// 获取浏览器传过来的cookie,并解析为对象格式
let cookieObj = querystring.parse(ctx.req.headers["cookie"], "; "); // a=1; b=2 {a:1,b:2}
// 如果需要验证签名
if (options.signed) {
// 将cookie中携带的签名和真实签名进行对比,如果相同,校验成功,返回对应的cookie
if (cookieObj[`${key}.sign`] === toBase64URL(crypto.createHmac("sha1", secret).update(`${key}=${cookieObj[key]}`).digest("base64"))) {
return cookieObj[key];
} else {
return "error";
}
}
return cookieObj[key] || "";
}
};
return next();
});
使用自带的cookies属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码const secret = "secret";
router.get("/write", async function (ctx) {
ctx.cookies.set("name", "zf", {
httpOnly: true
});
ctx.cookies.set("age", "12", {
signed: true
});
ctx.body = "write ok";
});
router.get("/read", async function (ctx) {
ctx.body = ctx.cookies.get("age", {
signed: true
}) || "empty"; // name=zf; age=12
});

session

特点

  • 存储类型丰富,存储数据量比cookie大很多
  • 保存在服务器中,比较安全
  • 因为是存储在服务器中,但是当访问量过多,会占用过多的服务器资源,影响服务器性能
  • 基于cookie,通过cookie携带sessionId
  • session默认是储存在内容中,如果服务器重启,session就丢失了

原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码let cardName = "zf"; // 店铺名字
let session = {}; // session就是一个服务器记账的本子,为了稍后能通过这个本找到具体信息
// 获取sessionId
let hasVisit = ctx.cookies.get(cardName,{signed:true});
// 必须保证你的卡是我的店的
if(hasVisit && session[hasVisit]){
session[hasVisit].mny -= 100;
ctx.body = '恭喜你消费了 ' + session[hasVisit].mny
}else{
const id = uuid.v4(); //冲500
session[id] = {mny:500};
ctx.cookies.set(cardName,id,{signed:true});
ctx.body = '恭喜你已经是本店会员了 有500元'
}

使用koa-session

1
2
3
4
5
6
7
8
9
10
11
js复制代码let cardName = "zf";
router.get("/wash", async function(ctx) {
let hasVisit = ctx.session[cardName];
if (hasVisit) {
ctx.session[cardName].mny -= 100;
ctx.body = "恭喜你消费了 " + ctx.session[cardName].mny;
} else {
ctx.session[cardName] = { mny: 500 };
ctx.body = "恭喜你已经是本店会员了 有500元";
}
});

只要关闭浏览器 ,session 真的就消失了?

  • 不对。对 session 来说,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 log off 的时候发个指令去删除 session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭
  • 之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 session id,而关闭浏览器后这个 session id 就消失了,再次连接服务器时也就无法找到原来的 session。
  • 如果服务器设置的 cookie 被保存在硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 session id 发送给服务器,则再次打开浏览器仍然能够打开原来的 session。
  • 恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 session 设置了一个失效时间,当距离客户端上一次使用 session 的时间超过这个失效时间时,服务器就认为客户端已经停止了活动,才会把 session 删除以节省存储空间。

jwt

jwt的组成

JWT 的三个部分依次如下。

  • Header(头部)

    • Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

      1
      2
      3
      4
      js复制代码{
      "alg": "HS256",
      "typ": "JWT"
      }
    • 上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。

    • 最后,将上面的 JSON 对象使用 Base64URL转成字符串。

  • Payload(负载)

    • Payload 部分也是一个JSON对象,用来存放实际需要传递的数据。JWT规定了7个官方字段,供选用。
    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号
  • Signature(签名)

    • Signature 部分是对前两部分的签名,防止数据篡改。
  • 也就是:Header.Payload.Signature

  • Base64URL
    JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。

特点

  • 服务端无状态化、可扩展性好
  • 浏览器默认不会携带token,需要每次请求放到请求头中(Authorization),不是每次请求都需要携带,可减小传输数据
  • 可以避开浏览器的同源策略,更容易跨域携带身份凭证
  • 基于token的认证,是服务端无状态,用计算token的时间换取服务端的存储空间
  • 支持移动端设备
  • 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  • 不加密的情况下,不能将秘密数据写入JWT。
  • JWT不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  • JWT的最大缺点是,由于服务器不保存session状态,因此无法在使用过程中废止某个token,或者更改token的权限。也就是说,一旦JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  • JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  • 为了减少盗用,JWT不应该使用HTTP协议明码传输,要使用HTTPS协议传输。

jwt.png

实现jwt原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
js复制代码const secret = "zf"; // 秘钥
const jwt = {
// 加密,通过crypto模块进行加密,返回base64并对base64中的 = + / 进行处理
sign(content, secret) {
return this.toBase64URL(crypto.createHmac("sha256", secret).update(content).digest("base64"));
},
// 处理base64中的字符串的 = + /
toBase64URL: (str) => {
return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//, "_");
},
// 解密固定写法
base64urlUnescape(str) {
str += new Array(5 - str.length % 4).join("=");
return str.replace(/-/g, "+").replace(/_/g, "/");
},
// 将内容转化为base64
toBase64(content) {
return this.toBase64URL(Buffer.from(JSON.stringify(content)).toString("base64"));
},
// 根据内容和秘钥,返回token令牌
encode(info, secret) {
// head是固定的对象转化为JSON进行加密
const head = this.toBase64({
typ: "JWT",
alg: "HS256"
});
// content是对传入内容进行base64转化
const content = this.toBase64(info);
// sign是对head和content进行加密
const sign = this.sign([head, ".", content].join(""), secret);
return head + "." + content + "." + sign;
},
// 解密操作
decode(token, secret) {
// 获取token字符串的三个部分
let [head, content, sign] = token.split(".");
// 将token的head和content重新加密,和sign进行对比,如果一样,证明没有被篡改
let newSign = this.sign([head, content].join("."), secret);
if (newSign === sign) {
// 将内容解密返回
return JSON.parse(Buffer.from(this.base64urlUnescape(content), "base64").toString());
} else {
throw new Error("用户更改了信息");
}
}
};

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

袋鼠云:基于Flink构建实时计算平台的总体架构和关键技术点

发表于 2021-07-16

平台建设的背景

传统离线数据开发时效性较差,无法满足快速迭代的互联网需求。伴随着以 Flink 为代表的实时技术的飞速发展,实时计算被越来越多的企业使用,但是在使用中,各种问题也随之而来。比如开发者使用门槛高、产出的业务数据质量没有保障、企业缺少统一平台管理难以维护等。在诸多不利因素的影响下,我们决定利用现有的 Flink 技术构建一套完整的实时计算平台。

平台总体架构

从总体架构来看,实时计算平台大体可以分为三层:

  • 计算平台
  • 调度平台
  • 资源平台。

每层承担着相应的功能,同时层与层之间又有交互,符合高内聚、低耦合的设计原子,架构图如下:

计算平台

直接面向开发人员使用,可以根据业务需求接入各种外部数据源,提供后续任务使用。数据源配置完成后,就可以在上面做基于 Flink 框架可视化的数据同步、SQL 化的数据计算的工作,并且可以对运行中的任务进行多维度的监控和告警。

调度平台

该层接收到平台传过来的任务内容和配置后,接下来就是比较核心的工作,也是下文中重点展开的内容。这里先做一个大体的介绍,根据任务类型的不同将使用不同的插件进行解析。

  • 数据同步任务:接收到上层传过来的 json 后,进入到 FlinkX 框架中,根据数据源端和写出目标端的不同生成对应的 DataStream,最后转换成 JobGraph。
  • 数据计算任务:接收到上层传过来的 SQL 后,进入到 FlinkStreamSQL 框架中,解析 SQL、注册成表、生成 transformation,最后转换成 JobGraph。

调度平台将得到的 JobGraph 提交到对应的资源平台,完成任务的提交。

资源平台

目前可以对接多套不同的资源集群,并且也可以对接不同的资源类型,如:yarn 和 k8s.

数据同步和数据计算

在调度平台中,接收到用户的任务后就开始了后面的一系列的转换操作,最终让任务运行起来。我们从底层的技术细节来看如何基于 Flink 构建实时计算平台,以及如何使用 FlinkX、FlinkStreamSQL 做一站式开发。

FlinkX

作为数据处理的第一步,也是最基础的一步,我们来看看 FlinkX 是如何在 Flink 的基础上做二次开发。用户只需要关注同步任务的 json 脚本和一些配置,无需关心调用 Flink 的细节,且 FlinkX 支持下图中所展示的功能。

我们先看下 Flink 任务提交中涉及到的流程,其中的交互流程图如下:

那么 FlinkX 又是如何在 Flink 的基础对上述组件进行封装和调用,使得 Flink 作为数据同步工具使用更加简单?

主要从 Client、JobManager、TaskManager 三个部分进行扩展,涉及到的内容如下图:

Client 端

FlinkX 对原生的 Client 做了部分定制化开发,在 FlinkX-launcher 模块下,主要有以下几个步骤:

  1. 解析参数,如:并行度、savepoint 路径、程序的入口 jar 包(平常写的 Flink demo)、Flink-conf.yml 中的配置等;
  2. 通过程序的入口 jar 包、外部传入参数、savepoint 参数生成 PackagedProgram;
  3. 通过反射调用 PackagedProgram 中指定的程序的入口 jar 包的 main 方法,在 main 方法中,通过用户配置的 reader 和 writer 的不同,加载对应的插件;
  4. 生成 JobGraph,将其中需要的资源 (Flink 需要的 jar 包、reader 和 writer 的 jar 包、Flink 配置文件等) 加入到 YarnClusterDescriptor 的 shipFiles 中,最后 YarnClusterDescriptor 就可以和 yarn 交互启动 JobManager;
  5. 任务提交成功后,Client 端就可得到 yarn 返回的 applicationId,后续既可以通过 application 跟踪任务的状态。

JobManager 端

Client 端提交完后,随后 yarn 启动 jobmanager,jobmanager 会启动一些自己的内部服务,并且会构建 ExecutionGraph。

在这个过程中,FlinkX 主要做了以下两件事:

  1. 用不同插件重写 InputFormat 接口中的 createInputSplits 的方法创建分片,在上游数据量较大或者需要多并行度读取的时候,该方法就起到给每个并行度设置不同的分片的作用。比如:在两个并行度读取 MySQL 时,通过配置的分片字段 (比如自增主键 ID)。
* 第一个并行度读取 SQL 为:select \* from table where id mod 2=0;
* 第二个并行度读取 SQL 为:select \* from table where id mod 2=1;
  1. 分片创建完后,通过 getInputSplitAssigner 按顺序返回分配给各个并发实例。

TaskManager 端

在 TaskManager 端接收到 JobManager 调度过来的 task 之后,就开始了自己的生命周期的调用,主要包含以下几个重要的阶段:

  1. **initialize-operator-states():**循环遍历该 task 所有的 operator,并调用实现了 CheckpointedFunction 接口的 initializeState 方法,在 FlinkX 中为 DtInputFormatSourceFunction 和 DtOutputFormatSinkFunction,该方法在任务第一次启动的时候会被调用,作用是恢复状态,当任务失败时可以从最近一次的 checkpoint 恢复读取位置,从而达到可以续跑的目的,如下图所示:
  2. **open-operators():**该方法调用 OperatorChain 中所有 StreamOperator 的 open 方法,最后调用的是 BaseRichInputFormat 中的 open 方法。该方法主要做以下几件事:
* 初始化累加器,记录读入、写出的条数、字节数;
* 初始化自定义的 Metric;
* 开启限速器;
* 初始化状态;
* 打开读取数据源的连接 (根据数据源的不同,每个插件各自实现)。
  1. **run():**调用 InputFormat 中的 nextRecord 方法、OutputFormat 中的 writeRecord 方法进行数据的处理。
  2. **close-operators():**做一些关闭操作,例如调用 InputFormat、OutputFormat 的 close 方法等,并做一些清理工作。

以上就是TaskManager 中 StreamTask 整体的生命流程,除了上面介绍的 FlinkX 如何调用 Flink 接口,FlinkX 还有如下一些特性。

  • **自定义累加器:**累加器是从用户函数和操作中,分布式地统计或者聚合信息。每个并行实例创建并更新自己的 Accumulator 对象, 然后合并收集不同并行实例,在作业结束时由系统合并,并可将结果推动到普罗米修斯中,如图:
  • **支持离线和实时同步:**我们知道 FlinkX 是一个支持离线和实时同步的框架,这里以 MySQL 数据源为例,看看是如何实现的。
+ 离线任务:在 DtInputFormatSourceFunction 的 run 方法中会调用 InputFormat 的 open 方法,读取数据记录到 resultSet 中,之后再调用 reachedEnd 方法,来判断 resultSet 的数据是否读取完。如果读取完,就走后续的 close 流程。
+ 实时任务:open 方法和离线一致,在 reachedEnd 时判断是否是轮询任务,如果是,则会进入到间隔轮询的分支中,将上一次轮询读取到的最大的一个增量字段值,作为本次轮询的开始位置,并进行下一次轮询,轮询流程图如下:
  • **脏数据管理和错误控制:**把写入数据源时出错的数据记录下来,并把错误原因分类,然后写入配置的脏数据表。错误原因目前有:类型转换错误、空指针、主键冲突和其它错误四类。错误控制是基于 Flink 的累加器,在运行过程中记录出错的记录数,然后在单独的线程里定时判断错误的记录数是否已经超出配置的最大值,如果超出,则抛出异常使任务失败。这样可以对数据精确度要求不同的任务,做不同的错误控制,控制流程图如下:
  • **限速器:**一些上游数据产生过快的任务,会对下游数据库造成较大的压力,故而需要在源端做一些速率控制,FlinkX 使用的是令牌桶限流的方式控制速率。如下图,当源端产生数据的速率达到某个阈值时,就不会再读取新的数据,在 BaseRichInputFormat的open 阶段也初始化了限速器。

以上就是 FlinkX 数据同步的基本原理,但是数据业务场景中数据同步只是第一步,由于 FlinkX 目前的版本中只有 ETL 中的 EL,并不具备对数据的转换和计算的能力,故而需要将产生的数据流入到下游的 FlinkStreamSQL。

FlinkStreamSQL

基于 Flink,对其实时 SQL 进行扩展,主要扩展了流与维表的 join,并支持原生 Flink SQL 所有的语法。目前 FlinkStreamSQL source 端只能对接 Kafka,所以默认上游数据来源都是 Kafka。

接下来我们看看 FlinkStreamSQL 如何在 Flink 基础上做到,用户只需要关注业务 SQL 代码,如何调用 Flink api 来屏蔽底层。整体流程和上面介绍的 FlinkX 基本类似,不同点在 Client 端,这里主要包括 SQL 解析、注册表、执行 SQL 三个部分。

解析 SQL

这里主要是解析用户写的 create function、create table、create view、insert into 四种 SQL 语句,封装到结构化的 SQLTree 数据结构中。SQLTree 中包含了自定义函数集合、外部数据源表集合、视图语句集合、写数据语句集合。

表注册

得到了上面解析的 SQLTree 之后,就可以将 SQL中create table 语句对应的外部数据源集合作为表注册到 tableEnv 中,并且将用户自定的 UDF 注册进 tableEnv 中。

执行 SQL

将数据源注册成表之后,就可以执行后面 insert into 的 SQL 语句了,执行 SQL 这里会分两种情况:

  • SQL 中没有关联维表,就直接执行 SQL;
  • SQL 中关联了维表,由于在 Flink 早期版本中不支持维表 join 语法,我们在这块做了扩展,不过在 FlinkStreamSQL v1.11 之后和社区保持了一致,支持了和维表 join 的语法。根据维表的类型不同,使用不同的关联方式:
+ 全量维表:将上游数据作为输入,使用 RichFlatMapFunction 作为查询算子,初始化时将数据全表捞到内存中,然后和输入数据组拼得到打宽后的数据,之后重新注册一张大表,供后续 SQL 使用。
+ 异步维表:将上游数据作为输入,使用 RichAsyncFunction 作为查询算子,并将查询得到的数据使用 LRU 缓存,然后和输入数据组拼得到打宽后的数据,之后重新注册一张大表,供后续SQL使用。

上面介绍的就是 FlinkX 和 FlinkStramSQL 在 Client 端的不同之处,由于 source 端只有 Kafka 且使用了社区原生的 Kafka-connector,所以在 jobmanager 端也没有数据分片的逻辑,taskmanager 逻辑和 FlinkX 基本类似,这里不再介绍。

任务运维

当使用 FlinkX 和 FlinkStreamSQL 开发完业务之后,接下来进入到了任务运维阶段。在运维阶段,我们主要在任务运行信息、数据进出指标 metrics、数据延迟、反压、数据倾斜等维度做了监控。

任务运行信息

我们知道 FlinkStreamSQL 是基于 FlinkSQL 封装的,所以在提交任务运行时最终还是走的 FlinkSQL 的解析、验证、逻辑计划、逻辑计划优化、物理计划,最后将任务运行起来,也就得到了我们经常看见的 DAG 图:

但是由于 FlinkSQL 对任务做了很多优化,以至于我们只能看到如上图的大体 DAG 图,子 DAG 图里面的一些细节我们是没法直观的看到发生了什么事情。所以我们在原来生成 DAG 图的方式上进行了一定的改造,这样就能直观的看到子 DAG 图中每个 Operator 和每个并行度里面发生了什么事情,有了详细的 DAG 图后,其他的一些监控维度就能直观的展示,比如:数据输入输出、延时、反压、数据倾斜,在出现问题时就能具体定位到,如下图的反压:

了解了上面的结构后,我们来看看它是如何实现的。我们知道在 Client 提交任务时,会生成 JobGraph,JobGraph 中的 taskVertices 集合就封装了上图完整的信息,我们将 taskVertices 生成 json 后,再结合 LatencyMarker 和相关的 metrics,即可在前端生成上图,并做相应的告警。

除了上面的 DAG 以外,还有自定义 metrics、数据延时获取等,这里不具体介绍,有兴趣的同学可以参考 FlinkStreamSQL 项目。

使用案例:

通过上面的介绍后,我们看下在平台上使用的实际案例。下面展示了一个完整的案例:使用 FlinkX 将 MySQL 中新增用户数据实时同步到 Kafka,然后使用 FlinkstreamSQL 消费 Kafka 实时计算每分钟新增用户数,产出结果落库到下游 MySQL,供业务使用。

实时同步 MySQL 新增数据

实时计算每分钟新增用户数

运行信息

整体 DAG,可以直观的显示上面提到的多项指标

解析后的详细 DAG 图,可以看到子 DAG 内部的多项指标

以上就是 Flink 在袋鼠云实时计算平台的总体架构和一些关键的技术点,如有不足之处欢迎大家指出。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

bytemd使用(vue版本) 初步使用

发表于 2021-07-16

初步使用

首先是根据bytemd中md的教程来进行操作。我这里使用的是vue版本的。

首先你要创建或者有一个vue项目(我是新创建的vue项目)

结构了解

主要是分为编辑和查看两个页面

  • 编辑是Editor
  • 查看是View

安装bytemd

1
npm复制代码npm install @bytemd/vue

新建一个test页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
vue复制代码<template>
<Editor :value="value" :plugins="plugins" @change="handleChange" />
</template>

<script>
//这里就是引入所有的扩展的插件
import 'bytemd/dist/index.min.css'
import { Editor} from '@bytemd/vue'
import gfm from '@bytemd/plugin-gfm'
import highlight from "@bytemd/plugin-highlight-ssr";

const plugins = [
//将所有的扩展功能放入插件数组中,然后就可以生效了
gfm(),
highlight(),
]

export default {
name: "test",
components: { Editor },
data() {
return { value: '', plugins }
},
methods: {
handleChange(v) {
this.value = v
},
},
}
</script>
<style scoped>

</style>

修改APP页面

image.png

启动项目

最后就是启动这个项目了

image.png
这只是简单的运行起来了,需要自己慢慢来摸索优化

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Authorization Server的使用

发表于 2021-07-16

一、背景

在 Spring Security 5中,现在已经不提供了 授权服务器 的配置,但是 授权服务器 在我们平时的开发过程中用的还是比较多的。不过 Spring 官方提供了一个 由Spring官方主导,社区驱动的授权服务 spring-authorization-server,目前已经到了 0.1.2 的版本,不过该项目还是一个实验性的项目,不可在生产环境中使用,此处来使用项目搭建一个简单的授权服务器。

二、前置知识

1、了解 oauth2 协议、流程。可以参考阮一峰的这篇文章
2、JWT、JWS、JWK的概念

JWT:指的是 JSON Web Token,由 header.payload.signture 组成。不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的。
JWS:指的是签过名的JWT,即拥有签名的JWT。
JWK:既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。

三、需求

1、 完成授权码(authorization-code)流程。

最安全的流程,需要用户的参与。

2、 完成客户端(client credentials)流程。

没有用户的参与,一般可以用于内部系统之间的访问,或者系统间不需要用户的参与。

3、简化模式在新的 spring-authorization-server 项目中已经被弃用了。

4、刷新令牌。

5、撤销令牌。

6、查看颁发的某个token信息。

7、查看JWK信息。

8、个性化JWT token,即给JWT token中增加额外信息。

完成案例:
张三通过QQ登录的方式来登录CSDN网站。
登录后,CSDN就可以获取到QQ颁发的token,CSDN网站拿着token就可以获取张三在QQ资源服务器上的 个人信息 了。

角色分析

张三: 用户即资源拥有者
CSDN:客户端
QQ:授权服务器
个人信息: 即用户的资源,保存在资源服务器中

四、核心代码编写

1、引入授权服务器依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework.security.experimental</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.1.2</version>
</dependency>

2、创建授权服务器用户

张三通过QQ登录的方式来登录CSDN网站。

此处完成用户张三的创建,这个张三是授权服务器的用户,此处即QQ服务器的用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@EnableWebSecurity
public class DefaultSecurityConfig {

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin();
return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// 此处创建用户,张三。
@Bean
UserDetailsService users() {
UserDetails user = User.builder()
.username("zhangsan")
.password(passwordEncoder().encode("zhangsan123"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

3、创建授权服务器和客户端

张三通过QQ登录的方式来登录CSDN网站。

此处完成QQ授权服务器和客户端CSDN的创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码package com.huan.study.authorization.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;

/**
* 认证服务器配置
*
* @author huan.fu 2021/7/12 - 下午2:08
*/
@Configuration
public class AuthorizationConfig {

@Autowired
private PasswordEncoder passwordEncoder;
/**
 * 个性化 JWT token
 */
class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

    @Override
    public void customize(JwtEncodingContext context) {
        // 添加一个自定义头
        context.getHeaders().header("client-id", context.getRegisteredClient().getClientId());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

/**
* 定义 Spring Security 的拦截器链
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {

// 设置jwt token个性化
http.setSharedObject(OAuth2TokenCustomizer.class, new CustomOAuth2TokenCustomizer());

// 授权服务器配置
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();

return http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer)
.and()
.formLogin()
.and()
.build();
}

/**
* 创建客户端信息,可以保存在内存和数据库,此处保存在数据库中
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端id 需要唯一
.clientId("csdn")
// 客户端密码
.clientSecret(passwordEncoder.encode("csdn123"))
// 可以基于 basic 的方式和授权服务器进行认证
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
// 授权码
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 刷新token
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 客户端模式
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 密码模式
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
// 简化模式,已过时,不推荐
.authorizationGrantType(AuthorizationGrantType.IMPLICIT)
// 重定向url
.redirectUri("https://www.baidu.com")
// 客户端申请的作用域,也可以理解这个客户端申请访问用户的哪些信息,比如:获取用户信息,获取用户照片等
.scope("user.userInfo")
.scope("user.photos")
.clientSettings(clientSettings -> {
// 是否需要用户确认一下客户端需要获取用户的哪些权限
// 比如:客户端需要获取用户的 用户信息、用户照片 但是此处用户可以控制只给客户端授权获取 用户信息。
clientSettings.requireUserConsent(true);
})
.tokenSettings(tokenSettings -> {
// accessToken 的有效期
tokenSettings.accessTokenTimeToLive(Duration.ofHours(1));
// refreshToken 的有效期
tokenSettings.refreshTokenTimeToLive(Duration.ofDays(3));
// 是否可重用刷新令牌
tokenSettings.reuseRefreshTokens(true);
})
.build();

JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
if (null == jdbcRegisteredClientRepository.findByClientId("csdn")) {
jdbcRegisteredClientRepository.save(registeredClient);
}

return jdbcRegisteredClientRepository;
}

/**
* 保存授权信息,授权服务器给我们颁发来token,那我们肯定需要保存吧,由这个服务来保存
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);

class CustomOAuth2AuthorizationRowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
public CustomOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
this.setLobHandler(new DefaultLobHandler());
}
}

CustomOAuth2AuthorizationRowMapper oAuth2AuthorizationRowMapper =
new CustomOAuth2AuthorizationRowMapper(registeredClientRepository);

authorizationService.setAuthorizationRowMapper(oAuth2AuthorizationRowMapper);
return authorizationService;
}

/**
* 如果是授权码的流程,可能客户端申请了多个权限,比如:获取用户信息,修改用户信息,此Service处理的是用户给这个客户端哪些权限,比如只给获取用户信息的权限
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}

/**
* 对JWT进行签名的 加解密密钥
*/
@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

/**
* jwt 解码
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}

/**
* 配置一些断点的路径,比如:获取token、授权端点 等
*/
@Bean
public ProviderSettings providerSettings() {
return new ProviderSettings()
// 配置获取token的端点路径
.tokenEndpoint("/oauth2/token")
// 发布者的url地址,一般是本系统访问的根路径
// 此处的 qq.com 需要修改我们系统的 host 文件
.issuer("http://qq.com:8080");
}
}

注意⚠️:
1、需要将 qq.com 在系统的 host 文件中与 127.0.0.1 映射起来。
2、因为客户端信息、授权信息(token信息等)保存到数据库,因此需要将表建好。
客户端和授权信息表
3、详细信息看上方代码的注释

五、测试

从上方的代码中可知:

资源所有者:张三 用户名和密码为:zhangsan/zhangsan123
客户端信息:CSDN clientId和clientSecret:csdn/csdn123
授权服务器地址: qq.com
clientSecret 的值不可泄漏给客户端,必须保存在服务器端。

1、授权码流程

1、获取授权码

qq.com:8080/oauth2/auth… user.userInfo

client_id=csdn:表示客户端是谁
response_type=code:表示返回授权码
scope=user.userInfo user.userInfo:获取多个权限以空格分开
redirect_uri=www.baidu.com:跳转请求,用户同意或拒绝后

2、根据授权码获取token

1
2
3
shell复制代码 curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=authorization_code&code=tDrZ-LcQDG0julJBcGY5mjtXpE04mpmXjWr9vr0-rQFP7UuNFIP6kFArcYwYo4U-iZXFiDcK4p0wihS_iUv4CBnlYRt79QDoBBXMmQBBBm9jCblEJFHZS-WalCoob6aQ&redirect_uri=https%3A%2F%2Fwww.baidu.com'

Authorization: 携带具体的 clientId 和 clientSecret 的base64的值
grant_type=authorization_code 表示采用的方式是授权码
code=xxx:上一步获取到的授权码

3、流程演示

在这里插入图片描述

2、根据刷新令牌获取token

1
2
3
shell复制代码curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=refresh_token&refresh_token=Wpu3ruj8FhI-T1pFmnRKfadOrhsHiH1JLkVg2CCFFYd7bYPN-jICwNtPgZIXi3jcWqR6FOOBYWo56W44B5vm374nvM8FcMzTZaywu-pz3EcHvFdFmLJrqAixtTQZvMzx'

在这里插入图片描述

3、客户端模式

此模式下,没有用户的参与,只有客户端和授权服务器之间的参与。

1
2
3
shell复制代码curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=client_credentials'

客户端模式

4、撤销令牌

1
2
shell复制代码curl -i -X POST \
'http://qq.com:8080/oauth2/revoke?token=令牌'

5、查看token 的信息

1
2
3
shell复制代码curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/introspect?token=XXX'

token详情

6、查看JWK信息

1
2
shell复制代码curl -i -X GET \
'http://qq.com:8080/oauth2/jwks'

JWK信息

六、完整代码

gitee.com/huan1993/sp…

七、参考地址

1、github.com/spring-proj…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…604605606…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%