发布时间:2022-12-13 09:00
Spring Security是一个灵活和强大的身份验证和访问控制框架,以确保基于Spring的Java Web应用程序的安全,其中就是两方面的功能:认证和授权,认证意思是你是谁,授权意思是你能干什么。核心是通过一组过滤器,里面的每个过滤器去执行一种认证方式。
我们知道,登录的整体流程是:用户点击登录从前端发起请求,后端接受前端发送来的用户名和密码,然后去数据库里面查询是否存在此用户,如果存在,就放行,让用户进入系统,如果不存在用户或者密码错误,则提示用户信息错误。那么在springsecurity中,也是大致遵循这个思路,只不过做了很多校验。下面就让我们一一解读一下。
获取到请求里传递来的用户名/密码之后,接下来就构造一个 UsernamePasswordAuthenticationToken 对象,传入 username 和 password,username 对应了 UsernamePasswordAuthenticationToken 中的 principal 属性,而 password 则对应了它的 credentials 属性。见下图
我们获得UsernamePasswordAuthenticationToken 对象后,会去执行authenticationManager的authenticae方法。当我们点authenticationManager会看到,它其实是一个接口,里面就只有一个authenticae空方法
那我们找到这个接口的实现类ProviderManager,里面有authenticae的实现方法,
首先获取到传进来的 authentication 的 Class,判断当前 provider 是否support该 authentication(标记1处)。
如果支持,则调用 provider 的 authenticate 方法(标记2处)开始做校验,校验完成后,会返回一个新的 Authentication。
这里的 provider 可能有多个,如果 provider 的 authenticate 方法没能正常返回一个 Authentication,则调用 provider 的 parent 的 authenticate 方法继续校验(标记3处)。
最主要的流程,就是这样,在 for 循环中,第一个 provider 是一个 AnonymousAuthenticationProvider,这个 provider 压根就不支持 UsernamePasswordAuthenticationToken这种认证,也就是会直接在 provider.supports 方法中返回 false,结束 for 循环,然后会进入到下一个 if 中,直接调用 parent 的 authenticate 方法进行校验。而 parent 就是 ProviderManager,所以会再次回到这个 authenticate 方法中。再次回到 authenticate 方法中,provider 也变成了 DaoAuthenticationProvider,这个 provider 是支持 UsernamePasswordAuthenticationToken 的,所以会顺利进入到该类的 authenticate 方法去执行,而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authenticate 方法,所以 我们最终来到 AbstractUserDetailsAuthenticationProvider#authenticate 方法中:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 从authentication里面拿到当前登录用户的用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//去数据库里面查有没有此用户名的用户,如果有,就返回此用户
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
//检查此用户中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等
preAuthenticationChecks.check(user);
//检查用户密码对比是否正确
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
上述代码就是去校验登录用户的各个状态。
下面我们依次展开来说,
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//this.getUserDetailsService()读取了框架中的UserDetailsService(这是个接口),进而去UserDetailsService里面执行loadUserByUsername方法,然而我们通常会写xxDetailsService来实现UserDetailsService接口,并注入到框架中,所以会去执行我们自己写的xxDetailsService里面的loadUserByUsername方法
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
this.getUserDetailsService()读取了框架中的UserDetailsService(这是个接口),然后去UserDetailsService里面执行loadUserByUsername方法,然而我们通常会写xxDetailsService来实现UserDetailsService接口,并注入到框架中,所以会去执行我们自己写的xxDetailsService里面的loadUserByUsername方法。下面是我的接口实现类
public class UserDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private UserService userService;
// @Autowired
// private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
return new LoginUser(user);
}
// public UserDetails createLoginUser(SysUuser user)
// {
// return new LoginUser(user, permissionService.getMenuPermission(user));
// }
}
主要是userService.selectUserByUserName(username)方法,会去调用相应的service层和mapper层查询数据库,这里不再讲述。如果存在用户就重新new一个LoginUser,这个LoginUser是UserDetails的实现类,故可以直接返回。
2.preAuthenticationChecks.check()函数,这个函数会去检查我们账户的状态信息,例如账户是否被禁用、账户是否被锁定、账户是否过期,具体代码如下:
public void check(UserDetails user) {
//检查账户是否锁定
if (!user.isAccountNonLocked()) {
throw new LockedException(messages.getMessage(
"AccountStatusUserDetailsChecker.locked", "User account is locked"));
}
//检查账户是否启用
if (!user.isEnabled()) {
throw new DisabledException(messages.getMessage(
"AccountStatusUserDetailsChecker.disabled", "User is disabled"));
}
//检查账户是否过期
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(
messages.getMessage("AccountStatusUserDetailsChecker.expired",
"User account has expired"));
}
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(messages.getMessage(
"AccountStatusUserDetailsChecker.credentialsExpired",
"User credentials have expired"));
}
}
默认情况下,在我的UserDetails接口实现类LoginUser里面,重写了这些属性值,默认返回true就好。
3.additionalAuthenticationChecks()函数,这个函数会去匹配密码,当然是拿登录用户传进来的明文密码去匹配数据库里面的加密后的密码,具体代码如下
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
//获得登录用户的明文密码
String presentedPassword = authentication.getCredentials().toString();
//将明文密码与之前从数据库里面获取的userDetails对象的密文密码做匹配
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
首先获得登录用户的明文密码 ,然后将明文密码与之前从数据库里面获取的userDetails对象的密文密码做匹配。匹配函数实现不做讲述。
最终通过createSuccessAuthentication(principalToReturn, authentication, user)方法,返回一个authentication,在这个方法里会去重新创建一个UsernamePasswordAuthenticationToken,将已认证状态标志注明。
将明文密码与之前从数据库里面获取的userDetails对象的密文密码做匹配。匹配函数实现不做讲述。
最终通过createSuccessAuthentication(principalToReturn, authentication, user)方法,返回一个authentication,在这个方法里会去重新创建一个UsernamePasswordAuthenticationToken,将已认证状态标志注明。
在这里插入图片描述
c语言字符串处理的常用库函数总结,c语言字符串操作,及常用函数
USART(RS232422485)、I2C、SPI、CAN、USB总线
【React】一文搞懂路由组件传参(三种方案params、search、state)
python pillow库画图_python几种柱状图画法以及简单图片处理pillow库的学习
【比特熊故事汇】X Microsoft Build 2022——微软专家+MVP,技术亮点全解析
【04】制作一个鸿蒙应用-【先写一下最基本的前端代码】-优雅草科技伊凡