### 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。 ![image-20231213143224213](./images/image-20231213143224213.png) **配置文件方式** security框架提供了配置文件设置默认用户和密码的方式。(可以在不编写任何代码的前提下,定义登陆用户名和密码) ````yaml spring: security: user: name: qiusj password: qiusj123 ```` #### 2. 自定义认证流程 为了提供不同用户的权限,一般都会基于`RBAC`(**Role-Based Access Control**)模型设计数据库。 ![Image](./images/Image.png) > 每个用户,拥有哪些角色,角色拥有哪些操作?可以访问哪些数据? **设计表** 权限表`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 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中配置的加密算法生成,用于校验整个数据的完整和可靠性 生成的数据格式如下: ![303a67802f69df895428fbcce482c9c5.png](en-resource://database/676:1) >这里数据通过 . 隔开成三部分 **1.4.2 JWT交互流程** 流程图: ![3b5ca86318db22f6c0a9fabb0688f300.png](en-resource://database/677:1) 步骤翻译: 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