1.SpringSecurity简单介绍
Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。
关键的类:
-
WebSecurityConfigurerAdapter
:自定义Security策略 -
AuthenticationManagerBuilder
:自定义认证策略 -
@EnableWebSecurity
:开启WebSecurity模式
2.基本原理:
SpringSecurity本质是一个过滤器链,使用到了责任链模式,只有当前过滤器通过才能进入下一个过滤器。
当开启了Spring Security自动化配置后,Spring Security会自动创建一个名为springSecurityFilterChain的过滤器并注入到Spring容器中,springSecurityFilterChain包含15个过滤器:
而我们主要用到的就是UsernamePasswordAuthenticationFilter,这个过滤器就是用来处理来自/login的 post请求。
3.认证流程:
在UsernamePasswordAuthenticationFilter中进行的认证流程如下图:
在UsernamePasswordAuthenticationFilter中会处理前端提交的登录的post请求
然后从请求中获取,用户输入的账号和密码,然后生成一个认证对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
这时候这个认证对象只包含用户输入的账号和密码,并没有认证,然后会通过认证管理器
AuthenticationManager调用authenticate()方法进行认证,
源码:
在认证管理器的authenticate()方法中,它会遍历所有的认证器,找到能够处理对应认证对象的处理器:
上述图片源码的部分解析:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//接收认证对象 toTest
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
//生成迭代器用于遍历所有的认证器
Iterator var9 = this.getProviders().iterator();
while(var9.hasNext()) {
//接收当前的认证器
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
//判断当前认证器是否支持处理该类型认证对象
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try {
//如果支持就会进入这个if,调用该认证器的认证方法,我们这里是处理登录,使用的是DaoAuthenticationProvider这个认证器
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
这里我们就走到了上面认证流程图中的第四步,调用DaoAuthenticationProvider认证器的authenticate()方法进行认证,但是DaoAuthenticationProvider中并没有authenticate()这个方法,
它调用的是父类AbstractUserDetailsAuthenticationProvider的authenticate()方法,
在父类的这个方法中他会首先从缓存中查看有没有这个用户名对应的信息,如果缓存中根据用户名查出来的user对象为空,则会调用retrieveUser()方法,
在这个方法中会调用UserDetailsService的loadUserByUsername()方法去根据用户名查询用户,
他这个用户默认是UserDetailsService接口的实现类:InMemoryUserDetailsManager实例,
InMemoryUserDetailsManager负责提供用户数据,默认用户数据是基于内存的用户,用户名为user,密码则是随机生成的UUID字符串,但是我们的系统是需要从数据库中查询用户,然后根据数据库查询的结果来确定发起登录请求的这个用户是否是我们系统中的用户,从而决定是否认证。
所以这里我们需要自己编写一个类实现UserDetailsService接口重写loadUserByUsername()方法,
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserMapper.selectUserByUserName(username);
if(Objects.isNull(sysUser)){
throw new UsernameNotFoundException("用户名不存在");
}
//如果为空说明用户名错误抛出异常
//查看用户是否被禁用等
else if (UserStatus.DELETED.getCode().equals(sysUser.getDelFlag()))
{
throw new ServiceException("对不起,您的账号已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(sysUser.getStatus()))
{
throw new ServiceException("用户已封禁,请联系管理员");
}
return createLoginUser(sysUser);
}
public LoginUser createLoginUser(SysUser sysUser){
LoginUser loginUser = new LoginUser();
loginUser.setUserId(sysUser.getUserId());
loginUser.setUser(sysUser);
loginUser.setDeptId(sysUser.getDeptId());
return loginUser;
}
}
这个方法的返回值UserDetails是一个接口,只要是实现了这个接口的对象都可以,我这里的longinUser是根据自己系统的需求设计的一个,这个可以根据自己的需求去添加字段,只要实现UserDetails接口就行。
这样我们就实现了从数据库中查询我们系统的用户,如果查询的结果是空,则说明发请求的用户不是我们系统中的用户,认证失败。
如果查询的结果不为空,说明发请求的用户是我们系统的用户,我们需要对用户输入的密码进行验证,看是否与数据库中的密码一致,如果不一致就会抛出异常结束认证
验证密码的方法是DaoAuthenticationProvider的additionalAuthenticationChecks()方法,
他会调用PasswordEncoder的matches()方法,因为我们存入数据库中的密码是经过加密算法加密的,这个matches()方法就是将用户输入的密码进行相同加密后再去匹配,他这里默认的进行加密的类是BCryptPasswordEncoder
如果你的系统不是使用的它这个默认的加密方法,你就需要写一个类去实现PasswordEncoder接口,
我这里是封装了一个工具类用来处理密码相关的操作。
如果密码验证通过,就会将认证通过的权限信息封装到我们的认证对象中,然后返回
这就是完成了认证,但是中间的认证流程是我们自定义从数据库查询用户认证的,并不是它默认配置类中配置的,所以我们要写一个配置类继承WebSecurityConfigurerAdapter,就是我们文章开头提到的开启自定义认证策略的类,
继承这个类之后,我们需要重写configure()方法,再我们写的类上面加上 @Configuration注解
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @author suke
* @version 1.0
* @title SpringSecurityConfiguration
* @description springSecurity的自定义配置类
* @create 2024/7/25 10:14
*/
@Configuration
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
//对密码进行加密 密码校验
//使用自己编写的密码校验类
@Bean
public MyPasswordEncoder passwordEncoder(){
return new MyPasswordEncoder();
}
//修改用户名,密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//存放的密码: 密文
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
这就是使用SpringSecurity认证的登录的简单实现。