Spring Security

Spring Security

Spring Security是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity , Shiro的上手更加的简单。

一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作

而认证和授权也是SpringSecurity作为安全框架的核心功能。

如果不是前后端分离的项目,它是基于session存储进行一个认证和授权的。

前后端分离项目登录校验流程

Spring Security_第1张图片

  1. 前端携带用户名和密码,向服务器请求访问登录接口(login)
  2. 通过与数据库中的用户名和密码进行比对,如果正确,就会根据用户名或者用户的id,生成jwt,随后把jwt响应给前端。
  3. 登录(认证)成功后,如果向服务器访问其他请求,那么需要在请求头header中携带token,服务器获取请求头中的token进行解析,获取其中的用户Id,根据用户id获取用户的相关信息。
  4. 通过这些信息中是否含有可满足访问相关资源的信息,从而实现授权
  5. 最后访问目标资源,响应给前端。

SpringSecurity过滤器链

SpringSecurity工作流程本质上是通过过滤器链实现的。以下是核心的过滤器链。
Spring Security_第2张图片

  1. UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后的登陆请求。认证工作主要有它负责。
  2. ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
  3. FilterSecuritylnterceptor:负责权限校验的过滤器。

除了核心的过滤器,容器中SpringSecurity过滤器链DefaultSecurityFilterChain中含有的过滤器还有:
Spring Security_第3张图片

从数据库中校验

由于默认的情况下,认证时是从内存中对用户名和密码进行校验,默认的用户名是user,默认的密码会在控制台打印出来,根据默认的用户名和密码进行校验的过程,是通过UserDetailsService这个类实现的,如果想要实现从数据库中拿到用户名和密码进行校验,就必须重写自定义一个类来实现UserDetailsService,进而重写它的方法。
Spring Security_第4张图片
登录

  1. 自定义登录接口
    调用ProviderManager的方法进行认证如果认证通过生成jwt
    把用户信息存入redis中
  2. 自定义UserDetailsService
    在这个实现类中去查询数据库

校验:

  1. 定义Jwt认证过滤器
    获取token
    解析token
    获取其中的userid
    从redis中获取用户信息存入到SecurityContextHolder

准备工作

我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的自定义UserDetailsService。自定义UserDetailsService可以从数据库中查询用户名和密码。
因此数据库建表:

CREATE TABLE `sys_user`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常1停用)',
`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
`sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',
`user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(O管理员,1普通用户) ',
`create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除) ',
PRIMARY KEY (`id`)
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'

引入依赖


        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.5.2version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>

配置数据源信息

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

整合mybatis-plus

定义mapper接口

public interface UserMapper extends BaseMapper<User> {

}

扫描mapper

@MapperScan("com.kxy.mapper")

@TableName @TableId映射到当前表

@TableName("sys_user")//防止表名映射失败
public class User implements Serializable {

 @TableId
    private Long id;

测试mybaits-plus整合我们这个mapper是否成功运行

@SpringBootTest
class SpringbootSpringSecurityApplicationTests {
    @Resource
    UserMapper userMapper;
    @Test
    void contextLoads() {
        List<User> list = userMapper.selectList(null);
        System.out.println(list);
    }
}

自定义实现类

前面说了,如果想通过数据库中的用户名和密码进行认证和校验,就必须重写UserDetailsService类中的loadUserByUsername方法:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //登录认证
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        if (Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        //校验

        //封装并返回一个UserDetails对象
        return new UserDetailsImpl(user);
    }
}

由于loadUserByUsername最终要返回一个UserDetails对象,因此我们的大致思路是写一个UserDetailsImpl 实现UserDetails接口,把实体类User 作为属性,然后重写getPassword和getUsername方法,返回User的用户名和密码,然后使用lombok进行构造器的生成,这样我们就可以通过new UserDetailsImpl(user)的方式去封装一个UserDetails对象。

@Service
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDetailsImpl implements UserDetails {
    private User user;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

为了便于测试
在这里插入图片描述
我们将这个数据库中的password前面加上{noop}表示改密码字段是明文存储的。否则会抛出passwordEncoder相关的异常。
Spring Security_第5张图片
最后校验得以成功。

但是实际开发中,我们数据库的密码字段绝对不是这么存储的。而是以密文的方式去存储。这时就需要PasswordEncoder去进行明文的加密:PasswordEncoder有很多种,我们这里使用BCryptPasswordEncoder,在SecurityConfig配置类里把它注入到容器中。

实际项目中我们不会把密码明文存储在数据库中。

默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。

我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    //创建BCryptPasswordEncoder并注入到容器中
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

可以通过测试类,在控制台中输出通过passwordEncoder对象对明文123进行加密后的密文:

    @Test
    void testForPasswordEncoder(){
        //$2a$10$BMExXgJDwBrMeNelUUzhE.r19OSLuMpJQ6nYn/UijDmov5LuiJ1vi
        String encode = passwordEncoder.encode("123");
        System.out.println(encode);
        System.out.println(passwordEncoder.matches("123", encode));
    }

最后我们将密文保存到数据库中的表的password字段中:
在这里插入图片描述
这样即便数据库中的密文泄露了,也不会影响到认证,因为认证需要的是明文。

你可能感兴趣的:(SpringBoot,spring,数据库,前端)