认证、登录
一、思路分析
登录
1、自定义登录接口
调用ProviderManager的方法进行认证 如果认证通过生成jwt
把用户信息存入redis中
2、自定义UserDetailsService
在这个实现类中去查询数据库
校验:
1、定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
疑问:最后为什么要存入SecurityContextHolder呢?
答:因为如果我们其他的过滤器、包括一些资源可能需要去获取当前登录用户信息的,所以我们要把它存到
SecurityContextHolder当前,让其他地方要用也可以用到。
FilterSecuritylnterceptor过滤器就会用到:请求经过这里的时候,如果还是一个未认证的状态,它就拦截下来了,它是怎么判断的呢?它会获取SecurityContextHolder中用户的一个认证信息,如果能获取到,并且是认证过的状态,它就会放行。
二、准备工作
2.1、重新创建一个项目
省略,完成后如图:
添加依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
创建启动类跟controller接口
然后启动测试一下,输入我们的hello接口,一样回跳到login页面
2.2、准备项目环境
直接下载
链接:https://pan.baidu.com/s/1FVSI0v3AOZAi8p_xNrvkHg
提取码:i81o
完成之后,总的目录是这样的
2.3、准备数据库
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 '用户类型(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 '更新时间',
`del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'
然后创建一个测试类,测试一下,ok,没有问题
三、实现
3.1 数据库校验用户
创建一个类实现UserDetailsService接口,重写其中的方法。更加用户名从数据库中查询用户信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private 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("用户名或密码错误");
}
//TODO 根据用户查询权限信息 添加到LoginUser中
//封装成UserDetails对象返回
return new LoginUser(user);
}
}
因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户
信息封装在其中。
把下面的返回false全部都改成true,然后name跟password分别返回对应的name,password
@Data
@NoArgsConstructor
@AllArgsConstructor
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;
}
}
页面登录一下,没反应,后台报错了。
因为数据库是明文存储的,所以报错了。
如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密
码前加{noop}。如下:这样,再登录就不会有问题了。
如果填写错误的账号密码呢?就会返回用户名或者密码错误
这里有一个问题,我们没有校验密码,这是为什么呢?
答:其实我们也没有校验用户名,只是给了一个必要的校验信息而已。用了安全框架 是不需要你自己去校验的 你只需要将认证需要的必要信息 也就是用户信息 权限信息 封装到UserDetails里面就可以 校验是框架自己在干活。
3.2 数据库密码加密存储
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该
PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承
WebSecurityConfigurerAdapter。
但我们发现这是一个过时的类,在springboot 2.7 以上就过时了
改用EnableWebSecurity注解
然后,测试一下,有两个方法
encode();
传入一个明文,返回一个密文
matches();
第一个参数是明文,第二个参数是密码,如果匹配上就返回true,否则就是false
测试一下:
一切都没有问题,之后,重启服务,登录测试一下
数据库,存储的已经是密文了,但同样是可以登录成功了
关于加密可以去搜索一下对应的加密算法,这里简单的叙述一下:
当我们输入密码1234后,Spring Security会根据存储的密码密文解析出加密算法和盐值,然后它会根据这个加密算法和盐值将你输入的密码加密,最后才会将生成的密码密文与存储的密码密文进行比较。
每次加密的随机盐会被保存在一个hash中的,每次加密时候,密文中都包含了随机盐的基本信息,所以他能根据这个基本信息找到保存在hash上的那个随机盐。所以是一一对应的关系。
3.3 登录接口
1、 修改一下SecurityConfig配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig{
//配置密码加密器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 安全配置
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过session获取securitycontext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//对于登录接口,允许匿名访问
.antMatchers("/user/login").anonymous()
//除上面外的所有请求全部需要坚定认证
.anyRequest().authenticated();
return http.build();
}
/**
* 认证管理器,登录的时候参数会传给 authenticationManager
*/
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
2、创建LoginController
@RestController
public class LoginController {
@Autowired
private LoginServcie loginServcie;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
System.out.println("11111");
return loginServcie.login(user);
}
}
3、LoginServcie接口
public interface LoginServcie {
ResponseResult login(User user);
}
4、LoginServiceImpl实现类
登录实现类的三步走
//AuthenticationManager authenticate 进行用户认证
//如果认证通过了,使用userid生成一个jwt,jwt存入ResponseRsult
//把完整的用户信息存入redis,userid作为keys
@Service
public class LoginServiceImpl implements LoginServcie {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new
UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
Authentication authenticate =
authenticationManager.authenticate(authenticationToken);
//使用userid生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
//把token响应给前端
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
return new ResponseResult(200,"登陆成功",map);
}
}
对第二句代码的一个小解析:
它会调用我们之前自定义的UserDetailsServiceimpl里面的方法进行一个校验,会根据我们传过去的用户名去查询数据库,然后再返回
做完了之后,重启一下项目,使用poetman测试一下效果
OK,没有 任何问题。
测试ok了,还有一步没有做,用户信息没有存入redis
3.4 用户信息存入redis
因为我已经搭建、整合好redis哨兵集群了,这里就省略了redis过程了
@Autowired
private RedisTemplate redisTemplate;
四、完结
至此,认证—登录,部分完成