一、Springsecurity簡介
SpringSecurity是一個功能強大且高度可定制的身份驗證和通路控制架構。提供了完善的認證機制和方法級的授權功能。
二、登入認證流程
springsecurity的預設登入流程圖:
我們平常的登入的整體流程是:使用者點選登入從前端發起請求,後端接受前端發送過來的使用者名和密碼,然後去資料庫查詢是否存在此使用者,如果存在,就放行,讓使用者進入系統,如果不存在使用者或者密碼錯誤,則提示使用者資訊錯誤。在springsecurity中,也大緻遵循這個思路。
下面我們來一一解讀一下:
user:前端傳過來的參數封裝到了user對象裡面。
UsernamePasswordAuthenticationToken:(位于org.springframework.security.authentication包下)通過類名可以看出,它是使用使用者名密碼方式進行認證。它需要傳兩個參數使用者名和密碼。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
// 序列化id
private static final long serialVersionUID = 530L;
// 一般指的是使用者名
private final Object principal;
// 一般指的是密碼
private Object credentials;
// 構造器,
// principal 一般指的是使用者名
// credentials 一般指的是密碼
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
// 初始化父類構造 權限集合為空
super((Collection)null);
this.principal = principal; //principal傳過來的使用者名
this.credentials = credentials; //credentials穿過來的密碼
// 設定是否已認證 預設為false
this.setAuthenticated(false); //false表示未認證
}
}
AuthenticationManager:認證管理接口類,它有好幾個實作類。在實際需求中,我們可能會允許使用者使用使用者名+密碼登入,同時允許使用者使用郵箱+密碼,手機号碼+密碼登入,甚至,可能允許使用者使用指紋登入(還有這樣的操作?沒想到吧),是以說AuthenticationManager一般不直接認證,AuthenticationManager接口的常用實作類ProviderManager 内部會維護一個List清單,存放多種認證方式,實際上這是委托者模式的應用(Delegate)。
流程:
我們獲得UsernamePasswordAuthenticationToken 對象後,會去執行authenticationManager的authenticae方法。當我們點authenticationManager會看到,它其實是一個接口,裡面就隻有一個authenticae空方法。
那我們找到這個接口的實作類ProviderManager,裡面有authenticae的實作方法。
@Override
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;
int currentPosition = 0;
int size = this.providers.size();
//通過for循環,檢視是否支援該登入方式
for (AuthenticationProvider provider : getProviders()) {
//擷取class,判斷目前provider是否支援authentication
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//如果支援,則調用provider的authentication方法開始校驗
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
//如果不支援的話調用父級提供Provider,重新執行該authenticate方法,看是否支援該登入方式
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
result = provider.authenticate(authentication);:進入的是AbstractUserDetailsAuthenticationProvider#authenticate 方法中:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
//從Authentication提取出登入使用者名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//從資料庫中擷取到一個實作了UserDetails的user對象
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);//preAuthenticationChecks.check()方法區檢驗user中的各個賬戶狀态是否正常,例如賬戶是否被禁用、賬戶是否被鎖定、賬戶是否過期等等。
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);//additionalAuthenticationChecks方法則是做密碼比對的
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// 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);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);//postAuthenticationChecks.check方法中檢查密碼是否過期
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//通過createSuccessAuthentication方法建構一個新的UsernamePasswordAutheToken
return createSuccessAuthentication(principalToReturn, authentication, user);
}
依次展開來說:
1、首先是retrieveUser方法,這個方法的實作在DaoAuthenticationProvider類裡面,因為這個類繼承了AbstractUserDetailsAuthenticationProvider類,故retrieveUser函數會去資料庫裡面查找有沒有使用者名為username的使用者,DaoAuthenticationProvider裡面的retrieveUser實作代碼如下:
@Override
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);
}
}
自己寫的xxDetailsService裡面的loadUserByUsername方法。下面是我的接口實作類
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//查詢使用者資訊
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName,s);
User user = userMapper.selectOne(wrapper);
//在這裡進行判斷一下,資料庫裡面是否有此使用者資訊。如果沒有查詢到使用者資訊就抛出異常
if(Objects.isNull(user)){
throw new RuntimeException("使用者名或者密碼錯誤");
}
//TODO 查詢對應的權限資訊
// List<String> permission = Arrays.asList("test","admin");
List<String> list = menuMapper.selectPermsByUserId(user.getId());
//把資料封裝成userDetails進行傳回
LoginUser loginUser = new LoginUser(user,list);
return loginUser;
}
}
2、preAuthenticationChecks.check()函數,這個函數會去檢查我們賬戶的狀态資訊,例如賬戶是否被禁用、賬戶是否被鎖定、賬戶是否過期,具體代碼如下:
public void check(UserDetails user) {
//檢查賬戶是否鎖定
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is locked");
throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
}
//檢查賬戶是否啟用
if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
}
//檢查賬戶是否過期
if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account has expired");
throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
預設情況下,在我的UserDetails接口實作類LoginUser裡面,重寫了這些屬性值,預設傳回true就好。
public class LoginUser implements UserDetails {
private User user;
private List<String> permissions;
public LoginUser(User user, List<String> permission) {
this.user = user;
this.permissions = permission;
}
@JSONField(serialize = false)//不會序列化到流中
private List<SimpleGrantedAuthority> authorities;
//傳回權限資訊的方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//把permissions中的string類型的權限資訊封裝成SimpleGrantedAuthority對象
// List<SimpleGrantedAuthority> list =null;
// for(String permission : permissions){
// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
// list.add(simpleGrantedAuthority);
// }
if(authorities == null){
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
return authorities;
}
//傳回密碼
@Override
public String getPassword() {
return user.getPassword();
}
//傳回使用者名
@Override
public String getUsername() {
return user.getUserName();
}
//判斷是否沒過期
@Override
public boolean isAccountNonExpired() {
return true;
}
//是否被鎖定
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//是否啟用
@Override
public boolean isEnabled() {
return 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,将已認證狀态标志注明。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}