前言
經過一週的學習對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