Spring Security
Spring Security
Spring Security是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity , Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
如果不是前后端分离的项目,它是基于session存储进行一个认证和授权的。
前后端分离项目登录校验流程
- 前端携带用户名和密码,向服务器请求访问登录接口(login)
- 通过与数据库中的用户名和密码进行比对,如果正确,就会根据用户名或者用户的id,生成jwt,随后把jwt响应给前端。
- 登录(认证)成功后,如果向服务器访问其他请求,那么需要在请求头header中携带token,服务器获取请求头中的token进行解析,获取其中的用户Id,根据用户id获取用户的相关信息。
- 通过这些信息中是否含有可满足访问相关资源的信息,从而实现授权。
- 最后访问目标资源,响应给前端。
SpringSecurity过滤器链
SpringSecurity工作流程本质上是通过过滤器链实现的。以下是核心的过滤器链。
- UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后的登陆请求。认证工作主要有它负责。
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
- FilterSecuritylnterceptor:负责权限校验的过滤器。
除了核心的过滤器,容器中SpringSecurity过滤器链DefaultSecurityFilterChain中含有的过滤器还有:
从数据库中校验
由于默认的情况下,认证时是从内存中对用户名和密码进行校验,默认的用户名是user,默认的密码会在控制台打印出来,根据默认的用户名和密码进行校验的过程,是通过UserDetailsService这个类实现的,如果想要实现从数据库中拿到用户名和密码进行校验,就必须重写自定义一个类来实现UserDetailsService,进而重写它的方法。
登录
- 自定义登录接口
调用ProviderManager的方法进行认证如果认证通过生成jwt
把用户信息存入redis中 - 自定义UserDetailsService
在这个实现类中去查询数据库
校验:
- 定义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='用户表'
引入依赖
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</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相关的异常。
最后校验得以成功。
但是实际开发中,我们数据库的密码字段绝对不是这么存储的。而是以密文的方式去存储。这时就需要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字段中:
这样即便数据库中的密文泄露了,也不会影响到认证,因为认证需要的是明文。