前言
经过一周的学习对SpringSecurity框架有了基本的认识,并写出了一个基于JWT认证的小模块,通过对源代码的debug大致了解了此框架的认证流程,并做如下记录。
正文
在开始了解SpringSecurity认证流程之前,先看看在认证过程中涉及到的几个重要类。
org.springframework.security.core.Authentication
该接口是认证实体,包含将要认证的用户名,密码和权限等,实际开发中使用其子类UsernamePasswordAuthenticationToken
org.springframework.security.authentication.AuthenticationManager
该接口只定义了一个方法authenticate()
,所有的认证请求都需通过该方法,接口的主要实现类为ProviderManager
org.springframework.security.authentication.ProviderManager
这个类主要完成用户的认证,密码对比
org.springframework.security.authentication.AuthenticationProvider
该类提供了对不同认证方式的支持,常用子类是DaoAuthenticationProvider
org.springframework.security.core.userdetails.UserDetailsService
该结构定义了方法loadUserByUsername(String username)
,完成用户的用户信息的查找
直接子类的调用关系:
源代码
- ProviderManager
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 获取当前认证类型
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
// 循环AuthenticationProvider子类,找出可以处理此认证请求的Provider
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 这里进行实际的认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
try {
result = parentResult = parent.authenticate(authentication);
}
// 若AuthenticationProvider子类没有能力进行认证,则认证失败,抛出异常
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 认证成功,删除密码
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
- AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 判断是否是UsernamePasswordAuthenticationToken认证类型
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 获取将要认证的用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 其实没有被缓存的,忽略即可
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 使用DaoAuthenticationProvider调用UserDetailsService子类获取用户权限
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) {
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);
}
- DaoAuthenticationProvider
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 获取用户权限信息
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);
}
}
- SpaceUserDetailsImpl
/**
* @ProjectName: l-space
* @Package: website.lhc.lspace.config.security
* @ClassName: SpaceUserDetails
* @Author: lhc
* @Description: 自定义用户认证
* @Date: 2020/4/5 下午 12:09
*/
@Component
public class SpaceUserDetailsImpl implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(SpaceUserDetailsImpl.class);
@Autowired
private SpUserMapper userMapper;
@Autowired
private SpMenuMapper menuMapper;
/**
* 用户校验,并获取权限
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!StringUtils.hasText(username)) {
throw new UsernameNotFoundException("[username]为空");
}
SpUser spUser = userMapper.getUserByAccount(username);
if (spUser == null) {
throw new UsernameNotFoundException("用户不存在");
}
if (spUser.getStatus() == UserStatus.LOCKED.getStatus()) {
throw new LockedException("账户已冻结");
}
if (spUser.getStatus() == UserStatus.DISABLE.getStatus()) {
throw new DisabledException("账户已禁用");
}
List<GrantedAuthority> authorities = new ArrayList<>();
Set<String> permissionSet = menuMapper.listPermissionByUserId(spUser.getUserId());
for (String s : permissionSet) {
if (StringUtils.hasText(s)) {
authorities.add(new SimpleGrantedAuthority(s));
}
}
log.info("user:{}; roles:{}", spUser.toString(), authorities.toString());
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String bCryptPassword = passwordEncoder.encode(spUser.getUserPasswd());
// 这里需要自定义,实现UserDetails接口接口
return new SpaceUserDetail(spUser.getUserAccount(), bCryptPassword, spUser.getStatus(), authorities);
}
}
项目地址:https://github.com/longhaicheng/l-space