SpringSecurity认证(三)
工作原理
Spring Security
所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截, 校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter
或AOP
等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
当初始化Spring Security
时,会创建一个名为SpringSecurityFilterChain
的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy
,它实现了javax.servlet.Filter,因此外部的请求会经过此 类,下图是Spring Security过虑器链结构图:FilterChainProxy
是一个代理,真正起作用的是FilterChainProxy
中SecurityFilterChain所包含的各个Filter,同时 这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认 证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)
和决策管理器(AccessDecisionManager)
进行处理
spring Security功能的实现主要是由一系列过滤器链相互配合完成。
- SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
- UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密 码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
- FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问;
- ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
认证方式
内存用户信息认证
基于内存UserDetailsService
,不使用密码编码器,使用字符串进行比较
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager detailsManager = new InMemoryUserDetailsManager();
detailsManager.createUser(User.withUsername("zhangsan").password("123").authorities("save","update").build());
detailsManager.createUser(User.withUsername("lisi").password("456").authorities("save").build());
return detailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
PasswordEncoder
DaoAuthenticationProvider
认证处理器通过UserDetailsService
获取到UserDetails后,它是如何与请求 Authentication中的密码做对比呢?
在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过 PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:
public interface PasswordEncoder {
// 加密方法
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如 下声明即可,如下:
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下
- 用户输入密码(明文 )
- DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)
- DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通 过,否则校验失败。
NoOpPasswordEncoder
的校验规则拿 输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致 则校验通过,否则 校验失败
实际项目中推荐使用
BCryptPasswordEncoder
, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等。
连接数据库用户信息认证
在
Spring Boot整合SpringSecurity(二)
的基础上进行的操作
创建数据库
创建user_db数据库
CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
创建t_user表
CREATE TABLE `t_user` (
`id` bigint NOT NULL COMMENT '用户id',
`username` varchar(64) NOT NULL,
`password` varchar(64) NOT NULL,
`fullname` varchar(255) NOT NULL COMMENT '用户姓名',
`mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC
pom
添加依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
application.yml
在application.yml
配置数据源
spring:
application:
name: security-springboot
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/user_db?serverTimezone=UTC
username: root
password: root
entity
在entity包下定义UserDto实体类
@Data
@TableName("t_user")
public class UserDto {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String fullname;
private String mobile;
}
mapper
在mapper包下定义UserMapper接口
public interface UserMapper extends BaseMapper<UserDto> {
}
service
在service包下定义UserService接口
public interface UserService extends IService<UserDto> {
}
在service/impl包下定义UserServiceImpl接口
实现UserDetailsService
类,重写loadUserByUsername方法,更具username查询数据库,构建User用户信息认证,并返回
@Service
@Log4j2
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDto> implements UserService, UserDetailsService {
@Autowired(required = false)
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("登录的账号:{}",username);
//根据账号去数据库查询
QueryWrapper<UserDto> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",username);
UserDto userDto = userMapper.selectOne(queryWrapper);
if (userDto == null){
throw new UsernameNotFoundException("用户名或密码不存在");
}
//权限使用静态数据,后面去查数据库
UserDetails userDetails = User.withUsername(userDto.getFullname())
.password(userDto.getPassword())
.authorities("save","update")
.build();
return userDetails;
}
}
使用BCryptPasswordEncoder
1.在安全配置类中定义BCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
2.UserDetails中的密码存储BCrypt格式
3.数据库中的密码应该存储BCrypt格式
测试数据
@SpringBootTest
class WebSecurityConfigTest {
@Autowired
private UserMapper userMapper;
@Test
public void testBCryptPasswordEncoder() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserDto userDto = new UserDto();
//userDto.setUsername("张三");
userDto.setUsername("lisi");
// 使用BCryptPasswordEncoder对密码进行加密存储到数据库
//userDto.setPassword(passwordEncoder.encode("123"));
userDto.setPassword(passwordEncoder.encode("456"));
//userDto.setFullname("张三");
userDto.setFullname("李四");
userDto.setMobile("15096000000");
userMapper.insert(userDto);
}
}
测试
启动测试,当输入用户名会去数据库中读取,不存在抛出异常,其他测试正常