### Spring Security
---
>Spring Security 基于Spring框架提供的一套Web应用安全性的完整解决方案。
#### 1.简介
SpingSecurity框架是Spring开源社区的顶级项目。
Spring开源社区顶级项目都提供了Spring Boot启动器(`spring-boot-starter-security`),做快速启动、环境装配。
```xml
org.springframework.boot
spring-boot-starter-parent
2.0.4.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
```
项目中增加security启动器依赖。不需要任何配置和特殊编码,自动提供一个登陆处理逻辑。
**默认登陆方式**
默认情况下,出/login请求地址可以正常访问外,其他所有请求地址,在未登陆时,自动跳转到/login登陆页面。
默认情况下,security框架启动时,会在控制台日至终输出一个UUID字符串,此字符串是security框架默认生成的登陆密码,用户名默认为user。

**配置文件方式**
security框架提供了配置文件设置默认用户和密码的方式。(可以在不编写任何代码的前提下,定义登陆用户名和密码)
````yaml
spring:
security:
user:
name: qiusj
password: qiusj123
````
#### 2. 自定义认证流程
为了提供不同用户的权限,一般都会基于`RBAC`(**Role-Based Access Control**)模型设计数据库。

> 每个用户,拥有哪些角色,角色拥有哪些操作?可以访问哪些数据?
**设计表**
权限表`tb_permission`
```shell
CREATE TABLE IF NOT EXISTS public.tb_permission
(
id smallint NOT NULL DEFAULT nextval('tb_permission_id_seq'::regclass),
name character varying(32) COLLATE pg_catalog."default",
url character varying(255) COLLATE pg_catalog."default" DEFAULT NULL::character varying,
parent_id integer,
type character varying(24) COLLATE pg_catalog."default",
permit character varying(128) COLLATE pg_catalog."default",
remark text COLLATE pg_catalog."default",
CONSTRAINT tb_permission_pkey PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.tb_permission OWNER to postgres;
COMMENT ON COLUMN public.tb_permission.name IS '权限名称';
COMMENT ON COLUMN public.tb_permission.url IS '请求地址';
COMMENT ON COLUMN public.tb_permission.parent_id IS '父权限主键';
COMMENT ON COLUMN public.tb_permission.type IS '权限类型,M-菜单,A-子菜单,U-普通请求';
COMMENT ON COLUMN public.tb_permission.permit IS '权限字符串描述';
COMMENT ON COLUMN public.tb_permission.remark IS '描述';
```
角色表:`tb_role`
```shell
CREATE TABLE IF NOT EXISTS public.tb_role
(
id smallint NOT NULL DEFAULT nextval('tb_role_id_seq'::regclass),
name character varying(32) COLLATE pg_catalog."default",
remark text COLLATE pg_catalog."default",
CONSTRAINT tb_role_pkey PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.tb_role OWNER to postgres;
COMMENT ON COLUMN public.tb_role.name IS '角色名称';
COMMENT ON COLUMN public.tb_role.remark IS '角色描述';
```
用户表:`tb_user`
```shell
CREATE TABLE IF NOT EXISTS public.tb_user
(
id smallint NOT NULL DEFAULT nextval('tb_user_id_seq'::regclass),
name character varying(32) COLLATE pg_catalog."default",
username character varying(32) COLLATE pg_catalog."default",
password character varying(128) COLLATE pg_catalog."default",
remark text COLLATE pg_catalog."default",
CONSTRAINT tb_user_pkey PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.tb_user OWNER to postgres;
COMMENT ON COLUMN public.tb_user.name IS '姓名';
COMMENT ON COLUMN public.tb_user.username IS '用户名';
COMMENT ON COLUMN public.tb_user.password IS '密码';
COMMENT ON COLUMN public.tb_user.remark IS '描述';
```
角色权限关系表:`tb_role_permission`
```shell
CREATE TABLE IF NOT EXISTS public.tb_role_permission
(
role_id integer,
permission_id integer
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.tb_role_permission OWNER to postgres;
```
用户角色关系表:`tb_user_role`
```shell
CREATE TABLE IF NOT EXISTS public.tb_user_role
(
user_id integer,
role_id integer
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.tb_user_role OWNER to postgres;
```
测试数据:
```sql
INSERT INTO public.tb_permission(
name, url, parent_id, type, permit, remark)
VALUES
( '登陆', '/login', 0, 'U', 'login:login', '登陆权限'),
( '进入登陆页', '/login', 0, 'U', 'login:toLogin', '进入登录页面权限'),
( '退出', '/logout', 0, 'U', 'logout', '退出权限'),
( '注册', '/register', 0, 'U', 'reg:register', '注册权限'),
( '进入注册界面', '/toRegister', 0, 'U', 'reg:toRegister', '进入注册页面权限'),
( '用户管理', '', 0, 'M', 'user:manager', '用户管理权限'),
( '用户查询', '/user/list', 6, 'U', 'user:list', '用户查询权限'),
( '用户新增', '/user/add', 6, 'U', 'user:add', '用户新增权限'),
( '进入用户新增界面', '/user/ToAdd', 6, 'U', 'user:toAdd', '进入用户新增界面权限'),
( '用户修改', '/user/modify', 6, 'U', 'user;modify', '用户修改权限'),
( '进入用户修改', '/user/ToModify', 6, 'U', 'user:ToModify', '进入用户修改权限'),
( '用户删除', '/user/remove', 6, 'U', 'user:remove', '用户删除权限'),
( '进入主页面', '/main', 0, 'U', 'reg:toRegister', '进入主页面权限');
INSERT INTO public.tb_role( name, remark)
VALUES
('超级管理员','全部权限'),
('普通用户','基础权限');
INSERT INTO public.tb_user(
name, username, password, remark)
VALUES ('超级管理员','admin','123','超级管理员'),
('普通用户','guest','guest','普通用户');
INSERT INTO public.tb_role_permission(
role_id, permission_id)
VALUES (1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),(1,8),(1,9),(1,10),(1,11),(1,12),(1,13),
(2,1),(2,2),(2,3),(2,4),(2,5),(2,13),;
insert into tb_user_role(user_id,role_id)
values(1,1),(2,2);
```
**密码解析器 **
Security框架定义的接口 PasswordEncoder,**Security框架强制要求**,必须在Spring容器中存在PasswordEncoder类型对象,且对象唯一。
```java
public class MyPasswordEncoder implements PasswordEncoder {
/**
* 密码加密方法
* @param charSequence
* @return
*/
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
/**
* 校验密码明文和密文是否相同的方法
* @param charSequence 明文密码,登陆时客户端传递过来的
* @param s
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
// 先使用encode方法加密明文,在与加密后的密文对比
return encode(charSequence).equals(s);
}
}
```
```java
@Configuration
public class MySecurityConfiguration {
/**
* 创建一个PasswordEncoder对象
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new MyPasswordEncoder();
}
}
```
**定义登陆服务类型**
Security框架定义了一个**登陆服务接口**,类型是:UserDetailsService。
> 此接口必须自定义实现类,自定义实现类型的对象必须被spring容器管理且唯一。
此接口中定义了唯一的登陆服务方法,方法是:
`UserDetails loadUserByUsername(String username) throws UsernameNotFoundException`;
此方法用于查询登陆用户,仅需要根据用户名查询用户对象并返回。
如果用户查询没有结果,跑出异常UsernameNotFoundException。
方法返回类型是Security框架定义的接口类型,是`UserDetails`。
接口中定义了若干方法,重要方法是,获取用户对象和获取用户权限列表:
```java
public interface UserDetails extends Serializable {
//返回登陆用户的权限列表
Collection extends GrantedAuthority> getAuthorities();
//返回登陆用户密码,是服务器保存的密文密码,编写友好的实现,一般都会隐藏密码结果
String getPassword();
// 返回用户名,身份 principal
String getUsername();
// 返回用户是否过期,true未过期
boolean isAccountNonExpired();
// 返回用户是否锁定,true未锁定
boolean isAccountNonLocked();
// 返回账户凭证是否过期,true未过期
boolean isCredentialsNonExpired();
// 返回登陆用户是否可用
boolean isEnabled();
}
```
```java
@Component
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 根据用户名查询用户对象
* @param s
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMapper.selectByUsername(s);
if(user == null){
throw new UsernameNotFoundException("用户名或密码错误!");
}
// 返回 UserDetials对象
org.springframework.security.core.userdetails.User result = new org.springframework.security.core.userdetails.User(
s,
user.getPassword(),// 登陆用户的密码,是服务端的密文
AuthorityUtils.NO_AUTHORITIES//暂时用框架提供的无权限集合
);
return result;
}
}
```
#### 3.简述Security认证流程
下述所有流程,是基于Security框架默认设置的前提下。提供自定义认证流程后
1. 请求当前项目的任何地址,出/login外。框架检查是否已登陆,未登录时,自动进入登陆页面。重定向到/login。已登陆,正常访问。
/login get请求,进入登陆页面
/login post请求,登陆
2. 填写登陆表单(用户名和密码),点击登陆,访问/login,post请求
3. Security 框架负责收集请求参数,用户名和密码。
4. Security 框架从Spring容器中,根据类型获取bean对象,UserDetailsService类型的对象。ApplicationContext.getBean(UserDetailsService.class)
5. Security框架调用方法,loadUserByUsername。根据用户名查询用户对象。
6. 如果loadUserByUsername 抛出异常,止咳重定向到/login?error,登陆失败。用户名不存在。
7. 如果loadUserByUsername 未抛出异常,则校验返回值中保存的密码和请求参数密码是否匹配。
获取Spring容器中的PasswordEncoder类型对象,调用方法matches比较密码。
如果密码比较结果为true,登陆成功。
如果密码比较结果为false,登陆失败,重定向到/login?error。
#### 4. 认证安全强化
> 修改密码解析器的实现对象。
在数据库中保存加密后的密码数据,并修改密码解析器对象。
Security框架,提供若干密码解析器实现类型,其中`BCryptPasswordEnder` 叫强散列加密,可以保证相同明文,多次加密后,密文有相同的散列数据,而不是相同的结果。匹配时,是基于相同的散列数据做的匹配。
```java
public class TestBCryptPE {
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String pwd = "123";
String m1 = bCryptPasswordEncoder.encode(pwd);
String m2 = bCryptPasswordEncoder.encode(pwd);
String m3 = bCryptPasswordEncoder.encode(pwd);
System.out.println(m1);
System.out.println(m2);
System.out.println(m3);
System.out.println(bCryptPasswordEncoder.matches(pwd,m1));//true
System.out.println(bCryptPasswordEncoder.matches(pwd,m2));//true
System.out.println(bCryptPasswordEncoder.matches(pwd,m3));//true
}
}
```
强化密码解析器
```java
@Configuration
public class MySecurityConfiguration {
/**
* 创建一个PasswordEncoder对象,更换为BCryptPasswordEncoder实例
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
```
#### 5. 自定义配置
使用自定义配置信息,覆盖Security内置的默认信息。
低版本(springboot2 + javaEE + security 5)
使用Configuration实现配置。继承`Security`框架提供的适配器类型`SecurityConfigurerAdapter`。该类型需要提供泛型,`SecurityConfigurerAdapter`其中:
- DefaultSecurityFilterChain:用于提供Servlet技术中的过滤链。
- HttpSecurity:用于提供Seurity框架中的配置处理。
高版本(Spring Boot3 + jakartaEE + security 6)
Spring Boot3 + jakartaEE + security 6版本开发时,修改了自定义配置策略。从springboot2 + javaEE + security 5版本的`WebSecurityConfigure`自动装配策略,改为`Configuration + Bean` 对象配置策略。
要求:
- 定义Configuration配置类型,定义Bean对象管理方法。方法创建`SecurityFilterChain`类型对象。Security框架,自动在spring容器中,创建一个`HttpSecurity`类型的对象,可以通过方法参数传递个创建SecurityFilterChain对象的方法。
```java
@Configuration
public class MySecurityConfiguration {
/**
* 创建一个PasswordEncoder对象
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity security) throws Exception {
return security.build();
}
}
```
SecurityFilterChain对象,不需要手工new,可以通过HttpSecurity对象中的方法build构建。
Security框架官方推荐,在提供Security配置对象的类型上,增加注解。EnableWebSecurity,让Security框架一定生效。
HttpSecurity对象中,存在若干方法,可以提供配置内容,包括登陆认证配置、授权配置、CSRF配置等
#### JWT
> JWT(JSON Web Token)是JSON格式被加密的字符串
**1. 无状态登陆**
**1.1 什么是有状态**
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份处理请求,例如:Tomcat中的Session(用户登录后,我们把用户的信息保存在服务端Session中,并且给用户一个cookie值,记录session,然后下次请求用户带上cookie值,浏览器自动完成,这样服务端能识别到对应的Session 从而找到用户信息)这不足之处在于:
* 服务端保存大量的数据
* 服务端保存的用户状态,不支持集群化部署
**1.2 什么是无状态**
微服务集群中的每个服务,对外提供的都是使用RESTful风格的接口,而RESTful风格的一个最重要的规范就是服务的无状态,即
* 服务端不保存任何客户端请求信息
* 客户端的每次请求必须具备自我描述信息,通过这些信息识别客户端身份
这种无状态的好处在于:
* 客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
* 服务端的集群和状态对客户端透明
* 服务端可以任意的迁移和伸缩(可以方便进行集群化部署)
* 减少服务端存储压力
**1.3 如何实现无状态**
无状态的登陆流程:
* 首先客户端发送用户名密码到服务端进行认证
* 认证通过后,服务端将用户信息加密并且编码成一个token,返回客户端
* 以后客户端每次发送请求,都需要携带认证的token
* 服务端对客户端发送来的token进行解密,判断是否有效,并且获取用户登陆信息
**1.4 JWT(JSON Web Token)**
JWT是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的web应用授权。
JWT作为一种规范,并没有和某种语言绑定在一起,常用的Java实现是Github上的开源项目jjwt
**1.4.1 JWT数据格式**
JWT包含三部分数据:
1. Header :通常包含两部分信息(声明类型、加密算法)
2. Payload:载荷,即有效数据
3. Signature:签名
荷载部分在官方文档中(RFC7519)给了7个示例信息
* **iss(issuer):签发人**
* **exp(expiration time):token过期时间**
* **sub(subject):主题**
* **aud(audience):受众**
* **nbf(Not Before):生效时间**
* **lat(issued At):签发时间**
* **jti(JWT ID):编号**
**签名**是整个数据的认证信息,是根据前两个步骤的数据,再加上服务的密钥secret(密钥保存在服务端,不能泄露给客户端),通过header中配置的加密算法生成,用于校验整个数据的完整和可靠性
生成的数据格式如下:

>这里数据通过 . 隔开成三部分
**1.4.2 JWT交互流程**
流程图:

步骤翻译:
1. 应用程序或客户端向授权服务器请求授权
2. 获取到授权后,授权服务器会向应用程序返回访问令牌
3. 应用程序使用访问令牌来访问受保护的资源
因为JWT签发的token中已经包含了用户的身份信息。并且每次请求都会携带,这样服务端就无需保存用户信息,
**1.4.3 JWT存在的问题**
1. 续签问题,
2. 注销问题:由于服务端不再保存用户信息。所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的为过期的token就会认证失败,进而实现注销
3. 密码重置:密码重置后,原本的token依然可以进行访问系统,这时候也需要强制修改secret
4. 基于2和3,一般建议不同用户取不同的secret
---
#### 2. Spring Security
**2.1 Spring Security 中的核心概念**
* AuthenticationManager:用户认证的管理类,所有的认证请求都会通过提交一个token给 AuthenticationManager 和
事件循环
闭包:应用场景,防抖,节流
深拷贝实现方式()
number boolean string null undefind object Function Array symbol Set Map Collection list
XHR
xmlHttpRequest
let const 工具IDE babel
Promise
fetch