meface/docs/article/java/spring_security.md

18 KiB
Raw Blame History

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。

image-20231213143224213

配置文件方式 security框架提供了配置文件设置默认用户和密码的方式。可以在不编写任何代码的前提下定义登陆用户名和密码

spring: 
  security: 
    user: 
      name: qiusj
      password: qiusj123   

2. 自定义认证流程

为了提供不同用户的权限,一般都会基于RBAC(Role-Based Access Control)模型设计数据库。

Image

每个用户,拥有哪些角色,角色拥有哪些操作?可以访问哪些数据?

设计表

权限表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', 'usermodify', '用户修改权限'),
	( '进入用户修改', '/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框架定义的接口 PasswordEncoderSecurity框架强制要求必须在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框架默认设置的前提下。提供自定义认证流程后

  1. 请求当前项目的任何地址,出/login外。框架检查是否已登陆未登录时自动进入登陆页面。重定向到/login。已登陆正常访问。 /login get请求进入登陆页面 /login post请求登陆

  2. 填写登陆表单(用户名和密码),点击登陆,访问/loginpost请求

  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 叫强散列加密,可以保证相同明文,多次加密后,密文有相同的散列数据,而不是相同的结果。匹配时,是基于相同的散列数据做的匹配。

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

JWTJSON Web Token是JSON格式被加密的字符串

1. 无状态登陆 1.1 什么是有状态 有状态服务即服务端需要记录每次会话的客户端信息从而识别客户端身份根据用户身份处理请求例如Tomcat中的Session用户登录后我们把用户的信息保存在服务端Session中并且给用户一个cookie值记录session然后下次请求用户带上cookie值浏览器自动完成这样服务端能识别到对应的Session 从而找到用户信息)这不足之处在于:

  • 服务端保存大量的数据
  • 服务端保存的用户状态,不支持集群化部署

1.2 什么是无状态 微服务集群中的每个服务对外提供的都是使用RESTful风格的接口而RESTful风格的一个最重要的规范就是服务的无状态

  • 服务端不保存任何客户端请求信息
  • 客户端的每次请求必须具备自我描述信息,通过这些信息识别客户端身份

这种无状态的好处在于:

  • 客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩(可以方便进行集群化部署)
  • 减少服务端存储压力

1.3 如何实现无状态 无状态的登陆流程:

  • 首先客户端发送用户名密码到服务端进行认证
  • 认证通过后服务端将用户信息加密并且编码成一个token返回客户端
  • 以后客户端每次发送请求都需要携带认证的token
  • 服务端对客户端发送来的token进行解密判断是否有效并且获取用户登陆信息

1.4 JWTJSON Web Token JWT是一种JSON风格的轻量级的授权和身份认证规范可实现无状态、分布式的web应用授权。 JWT作为一种规范并没有和某种语言绑定在一起常用的Java实现是Github上的开源项目jjwt

1.4.1 JWT数据格式 JWT包含三部分数据 1. Header :通常包含两部分信息(声明类型、加密算法) 2. Payload载荷即有效数据 3. Signature签名 荷载部分在官方文档中RFC7519给了7个示例信息

  • ississuer签发人
  • expexpiration timetoken过期时间
  • subsubject主题
  • audaudience受众
  • nbfNot Before生效时间
  • latissued At签发时间
  • jtiJWT ID编号

签名是整个数据的认证信息是根据前两个步骤的数据再加上服务的密钥secret密钥保存在服务端不能泄露给客户端通过header中配置的加密算法生成用于校验整个数据的完整和可靠性 生成的数据格式如下: 303a67802f69df895428fbcce482c9c5.png

这里数据通过 . 隔开成三部分

1.4.2 JWT交互流程 流程图: 3b5ca86318db22f6c0a9fabb0688f300.png

步骤翻译:

  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