从上一篇博客中可以看见,当我们什么都没有配置的时候,账号和密码都是由Spring Security定义生成的。而再实际项目中账号和密码都是从数据库中查询出来的。所以我们需要通过自定义逻辑控制认证登录。
需要自定义逻辑时,我们只需编写一个类实现 UserDetailsService 接口即可。接口定义如下:
这个接口中,只有一个方法。我们将这个方法拆分成四个部分。
①UserDetails
:返回值,也是一个接口,具体定义如下:
我们可以从它的实现类和方法观察出,这个类其实就是一个用户类。
①loadUserByUsername
:方法名,见名知意,就是将我们根据username加载用户,一般都是从数据库中查询用户。
①String username
:参数,就是登录页面传递过来的用户名,查询条件。
①UsernameNotFoundException
:异常,如果在加载用户时,找不到指定用户名的具体信息,则抛出该异常。
User类的全限定名是:org.springframework.security.core.userdetails.User
。我们可以用这个类来接收loadUserByUsername()方法的返回值。我们来看一下这个类的结构:
有两个构造方法,实际上,上面的三参构造,其实就是调用下面的七参构造,默认给其他几个属性默认赋上了true
。
此类中,最重要的三个构造参数如下,也是我们必须传入的参数:
此处的用户名就是客户端传递过来的username, 密码则是从数据库中查询出来的密码。Spring Security会根据User中的password
属性和客户端传递过来的password
进行比较。如果相同,则表示认证通过,如果不相同则认证失败。
authorities里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403(禁止访问)。
通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建 authorities 集合对象的。参数是一个字符串,多个权限使用逗号分隔。
实现自定义登录逻辑,Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要求必须给容器注入 PaswordEncoder 的bean对象。
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。 是对 bcrypt 强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认 10,可选范围是4-31。
当 进 行 自 定 义 登 录 逻 辑 时 需 要 用 到 之 前 讲 解 的 UserDetailsService 和PasswordEncoder 。但是 Spring Security 要求:当进行自定义登录逻辑时容器内必须有PasswordEncoder 实例。所以不能直接 new 对象。
将PasswordEncoder实例放入容器中。
package com.linqibin.springsecuritystudy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在 Spring Security 中实现 UserDetailsService 就表示为用户详情服务。在这个类的loadUserByUsername()方法中编写用户认证逻辑。
package com.linqibin.springsecuritystudy.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.linqibin.springsecuritystudy.entity.UserEntity;
import com.linqibin.springsecuritystudy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
// 根据前端传进来的username查询用户
UserEntity user = this.userService.getOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
System.out.println(user);
return new User(username, user.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin, normal"));
}
}
在以上代码中,我们提前在数据库中准备了一个用户,用户的密码是经过BCryptPasswordEncoder
加密过的。
使用MybatisPlus
根据用户名查询用户,如果未查到,则抛出异常。
如果查到了该用户,我们则返回一个实现了UserDetailsService接口的实体类,这里一定要注意,返回的类型必须是实现了这个接口的实体。将客户端传递过来的username和从数据库中查询出来的密码,以及权限(此例中我们的权限是自己手写的)作为参数。
用户加载成功后,Spring Security会将客户端传递过来的密码使用我们配置类中所注入的一个PasswordEncoder来加密,之后与用户中的密码进行比对,比对成功则认证通过。
认证过程就是一个简单的比对密码的过程,Spring Security先根据用户名去数据库中查询用户,将用户的密码和权限相关加载进内存;再将客户端传进来的密码用我们的注入到Spring容器中的PasswordEncoder进行加密,随后比较。
所以我们需要做的就只是:①编写用户认证逻辑 ②注入一个PasswordEncoder实例到Spring容器中。仅此而已。