18 KiB
Spring Security
Spring Security 基于Spring框架提供的一套Web应用安全性的完整解决方案。
1.简介
SpingSecurity框架是Spring开源社区的顶级项目。
Spring开源社区顶级项目都提供了Spring Boot启动器(spring-boot-starter-security
),做快速启动、环境装配。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
项目中增加security启动器依赖。不需要任何配置和特殊编码,自动提供一个登陆处理逻辑。
默认登陆方式 默认情况下,出/login请求地址可以正常访问外,其他所有请求地址,在未登陆时,自动跳转到/login登陆页面。 默认情况下,security框架启动时,会在控制台日至终输出一个UUID字符串,此字符串是security框架默认生成的登陆密码,用户名默认为user。
配置文件方式 security框架提供了配置文件设置默认用户和密码的方式。(可以在不编写任何代码的前提下,定义登陆用户名和密码)
spring:
security:
user:
name: qiusj
password: qiusj123
2. 自定义认证流程
为了提供不同用户的权限,一般都会基于RBAC
(Role-Based Access Control)模型设计数据库。
每个用户,拥有哪些角色,角色拥有哪些操作?可以访问哪些数据?
设计表
权限表tb_permission
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
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
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
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
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;
测试数据:
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类型对象,且对象唯一。
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);
}
}
@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
。
接口中定义了若干方法,重要方法是,获取用户对象和获取用户权限列表:
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();
}
@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框架默认设置的前提下。提供自定义认证流程后
-
请求当前项目的任何地址,出/login外。框架检查是否已登陆,未登录时,自动进入登陆页面。重定向到/login。已登陆,正常访问。 /login get请求,进入登陆页面 /login post请求,登陆
-
填写登陆表单(用户名和密码),点击登陆,访问/login,post请求
-
Security 框架负责收集请求参数,用户名和密码。
-
Security 框架从Spring容器中,根据类型获取bean对象,UserDetailsService类型的对象。ApplicationContext.getBean(UserDetailsService.class)
-
Security框架调用方法,loadUserByUsername。根据用户名查询用户对象。
-
如果loadUserByUsername 抛出异常,止咳重定向到/login?error,登陆失败。用户名不存在。
-
如果loadUserByUsername 未抛出异常,则校验返回值中保存的密码和请求参数密码是否匹配。
获取Spring容器中的PasswordEncoder类型对象,调用方法matches比较密码。
如果密码比较结果为true,登陆成功。
如果密码比较结果为false,登陆失败,重定向到/login?error。
4. 认证安全强化
修改密码解析器的实现对象。
在数据库中保存加密后的密码数据,并修改密码解析器对象。
Security框架,提供若干密码解析器实现类型,其中BCryptPasswordEnder
叫强散列加密,可以保证相同明文,多次加密后,密文有相同的散列数据,而不是相同的结果。匹配时,是基于相同的散列数据做的匹配。
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
}
}
强化密码解析器
@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,HttpSecurity>
其中:
- 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对象的方法。@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中配置的加密算法生成,用于校验整个数据的完整和可靠性
生成的数据格式如下:
这里数据通过 . 隔开成三部分
步骤翻译:
- 应用程序或客户端向授权服务器请求授权
- 获取到授权后,授权服务器会向应用程序返回访问令牌
- 应用程序使用访问令牌来访问受保护的资源
因为JWT签发的token中已经包含了用户的身份信息。并且每次请求都会携带,这样服务端就无需保存用户信息,
1.4.3 JWT存在的问题
- 续签问题,
- 注销问题:由于服务端不再保存用户信息。所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的为过期的token就会认证失败,进而实现注销
- 密码重置:密码重置后,原本的token依然可以进行访问系统,这时候也需要强制修改secret
- 基于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