2.3.3 实现
2.3.3.1 数据库校验用户
从之前的分析我们可以知道,我们可以自定义一个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 '账号状态(O正常 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 '用户类型(0管理员,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'更新时间',
`de1_f1ag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
引入mybatis-plus和mysql驱动的依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>mysq1</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
配置数据库信息
spring:
datasource:
ur1: jdbc:mysq1://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysq1.cj.jdbc.Driver
定义mapper接口
public interface UserMapper extends BaseMapper<User> {
}
修改User实体类
类名上加 @TableName(value ="sys_user"),id字段上加 @TableId
如果数据库和实体类的字段不一致,这里还需要 @TableField 来指定别名
具体参考官方文档:https://baomidou.com/
配置mapper扫描
@springBootApplication
@MapperScan("xxx.xxx.mapper")//xxx替换成自己的路径
public class SecurityApplication {
public static void main(string[] args){
ConfigurableApplicationContext run = SpringApplication.run(SecurityApplication.class);
System.out.println(run);
}
}
添加junit依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
测试mybatis-plus能不能正常使用
@springBootTest
public class MapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testUserMapper(){
List<User> users = userMapper.selectList(nul1);
System.out.println(users);
}
核心代码实现
实现自定义UserDetailsService,这里只需要重写默认的UserDetailsService的方法即可。
package service.impl;
import ...
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
private UserMapper
//查询用户信息
LambdaQueryWrapper<User> queryWeapper=new LambdaQueryWrapper();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//如果没有查询到用户,就抛出异常
if(Object.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
//TODO 查询对应的权限信息,本节仅对user做身份认证,不对权限做要求,暂时不写
//把数据封装成UserDetails返回
//这里要用到UserDetails的实现类,会在下面给出相应的写法
return new LoginUser(user);
}
}
package domain;
import ...
/**
* UserDetails的实现类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser 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;
}
}
做完了以上工作,我们就可以运行application来进行测试,当输入完账号密码之后点击登录可能会报下图所示的错误。
这是因为当前使用的是一个默认的PasswordEncoder,用来进行密码校验。这个默认的工具要求我们实际在数据库表中查到的密码字段的前面加上一个大括号,这个大括号里面写一些标识。
如果密码是原文存储的,则写成 {noop} ,表示密码是明文存储的,如下图所示:
后面会对这种写法进行更改。
2.3.3.2密码加密存储
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BcryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//创建BCryptPasswordEncoder注入容器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf,前后端分离项目必须写
.csrf().disable()
//前后端分离不能使用session,这里不通过session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//对于登录接口允许匿名访问
.antMatchers("/user/login").anonymous()
//除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
这里我们仍然采用老版本的写法,最新的写法参考官方文档:
Spring Security without the WebSecurityConfigurerAdapter
注意:在最新、独立的 Spring Security 5.7 版本,之前的 WebSecurityConfigurerAdapter 已经被废弃了,这个类将在5.7版本被@Deprecated
所标记了,未来这个类将被移除。新版本加个@EnableWebSecurity注解就行了,不用继承那个类了
另外,在最新的 Spring Boot 版本中的 Spring Security 并不一定也是最新版本,这个在实际开发中,需要留意一下。
我们可以编写一个测试类来查看 BCryptPasswordEncoder 的加密效果:
运行完成之后,在控制台中打印出了如下两行结果。
可以发现,虽然我们加密的内容是一致的,但是输出的结果是不相同的。这是因为encoder在内部加密的时候,会生成一个随机的盐值,它会拿盐值和原文进行比较(调用的是matches方法),返回结果为true则校验成功。
思考:这里的策略是不是从密文解析得到明文,而不是通常的明文加上盐值得到密文?
解答:显然不是,这里的策略是从加密密码中获取真实盐值,获取加密密码中前缀信息,拼接前缀和盐值部分的转成base64的值,存放在rs字符串里面,在上一步基础上,拼接原始密码值,这样就保证了对现在要校验的明文的加密和得到已有密文的加密用的是同样的加密策略,算法和盐值都相同,这样如果新产生的密文和原来的密文相同,则这两个密文对应的明文字符串就是相等的。
需要注意的是有些人会有一个误区,把开头的$2a$10$当作盐值。实际上开头的$2a$10$不是盐,是加密格式,随后的22位才是盐,然后是密文。
测试完成之后我们该如何在项目中实际使用呢?这个时候只需要使用spring为我们提供的自动注入即可。
@Autowired
private PasswordEncoder passwordEncoder;
评论