spring security小結
Spring Security 主要實現了Authentication(認證,解決who are you? ) 和 Access Control(訪問控制,也就是what are you allowed to do?,也稱為Authorization)。Spring Security在架構上將認證與授權分離,並提供了擴充套件點。
核心物件:
SecurityContextHolder 是 SecurityContext的存放容器,預設使用ThreadLocal 儲存,意味SecurityContext在相同執行緒中的方法都可用。
SecurityContext主要是儲存應用的principal資訊,在Spring Security中用Authentication 來表示。
獲取principal:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
在Spring Security中,可以看一下Authentication定義:
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); /** * 通常是密碼 */ Object getCredentials(); /** * Stores additional details about the authentication request. These might be an IP * address, certificate serial number etc. */ Object getDetails(); /** * 用來標識是否已認證,如果使用使用者名稱和密碼登入,通常是使用者名稱 */ Object getPrincipal(); /** * 是否已認證 */ boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
在實際應用中,通常使用UsernamePasswordAuthenticationToken
:
public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {
}
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
}
一個常見的認證過程通常是這樣的,建立一個UsernamePasswordAuthenticationToken,然後交給authenticationManager認證,
認證通過則通過SecurityContextHolder存放Authentication資訊。
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword());
Authentication authentication = this.authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails 是Spring Security裡的一個關鍵介面,用來表示一個principal。
public interface UserDetails extends Serializable {
/**
* 使用者的授權資訊,可以理解為角色
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 使用者密碼
*
* @return the password
*/
String getPassword();
/**
* 使用者名稱
* */
String getUsername();
boolean isAccountNonExpired(); // 使用者是否過期
boolean isAccountNonLocked(); // 是否鎖定
boolean isCredentialsNonExpired(); // 使用者密碼是否過期
boolean isEnabled(); // 賬號是否可用
}
UserDetails提供了認證所需的必要資訊,在實際使用裡,可以自己實現UserDetails,並增加額外的資訊,比如email、mobile等資訊。
在Authentication中的principal通常是使用者名稱,我們可以通過UserDetailsService來通過principal獲取UserDetails:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
小結
SecurityContextHolder
, 用來訪問SecurityContext
.SecurityContext
, 用來儲存Authentication
.Authentication
, 代表憑證.GrantedAuthority
, 代表權限.UserDetails
, 對使用者資訊進行封裝.UserDetailsService
,對使用者資訊進行管理.
整個過程:
Authentication認證
1、使用者進入登入頁面,輸入使用者名稱和密碼,security首先會進入UsernamePasswordAuthenticationFilter,呼叫
attemptAuthentication方法,將使用者名稱和密碼作為pricaipal和critial組合成UsernamePasswordAuthenticationToken例項
2、將令牌傳遞給AuthenticationManage例項進行驗證,根據使用者名稱查詢到使用者,在進行密碼比對
3、校驗成功後,會把查詢到的user物件填充到authenticaton物件中,並將標誌authenticated設為true
4、通過呼叫 SecurityContextHolder.getContext().setAuthentication(...) 建立安全上下文的例項,傳遞到返回的身份認證物件上
AbstractAuthenticationProcessingFilter 抽象類
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
呼叫requestsAuthentication()決定是否需要進行校驗,如果需要驗證,則會呼叫attemptAuthentication()進行校驗,有三種結果:
1、驗證成功,返回一個填充好的Authentication物件(通常帶上authenticated=true),接著執行successfulAuthentication()
2、驗證失敗,丟擲AuthenticationException,接著執行unsuccessfulAuthentication()
3、返回null,表示身份驗證不完整。假設子類做了一些必要的工作(如重定向)來繼續處理驗證,方法將立即返回。假設後一個請求將被這種方法接收,其中返回的Authentication物件不為空。
AuthenticationException
是執行時異常,它通常由應用程式按通用方式處理,使用者程式碼通常不用特意被捕獲和處理這個異常。
UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子類)
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
attemptAuthentication ()
方法將 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 物件,用於 AuthenticationManager
的驗證(即 this.getAuthenticationManager().authenticate(authRequest) )
預設情況下注入 Spring 容器的 AuthenticationManager
是 ProviderManager
。
ProviderManager(AuthenticationManager的實現類)
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
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) {
// Allow the parent to try.
try {
// 如果沒有任何一個 Provider 驗證成功,則使用父型別AuthenticationManager進行驗證
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// 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 e) {
lastException = e;
}
}
if (result != null) {
// 擦除敏感資訊
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
AuthenticationManager
的預設實現是ProviderManager
,它委託一組AuthenticationProvider
例項來實現認證。AuthenticationProvider
和AuthenticationManager
類似,都包含authenticate
,但它有一個額外的方法supports
,以允許查詢呼叫方是否支援給定Authentication
型別:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
ProviderManager包含一組AuthenticationProvider
,執行authenticate時,遍歷Providers,然後呼叫supports,如果支援,則執行遍歷當前provider的authenticate方法,如果一個provider認證成功,則break。如果最後所有的 AuthenticationProviders 都沒有成功驗證 Authentication 物件,將丟擲 AuthenticationException。
由 provider 來驗證 authentication, 核心點方法是Authentication result = provider.authenticate(authentication);此處的 provider
是 AbstractUserDetailsAuthenticationProvider,它
是AuthenticationProvider的實現,看看它的 authenticate(authentication)
方法
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 {
// retrieveUser抽象方法,獲取使用者
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 {
// 預先檢查 DefaultPreAuthenticationChecks,檢查使用者是否被lock或者賬號是否可用
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;
}
}
// 後置檢查 DefaultPostAuthenticationChecks,檢查isCredentialsNonExpired
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
三步驗證工作
1. preAuthenticationChecks
2. additionalAuthenticationChecks(抽象方法,子類實現)
3. postAuthenticationChecks
AbstractUserDetailsAuthenticationProvider
內建了快取機制,從快取中獲取不到的 UserDetails 資訊的話,就呼叫retrieveUser()方法獲取使用者資訊,然後和使用者傳來的資訊進行對比來判斷是否驗證成功。retrieveUser()
方法在 DaoAuthenticationProvider
中實現, DaoAuthenticationProvider
是 AbstractUserDetailsAuthenticationProvider
的子類。這個類的核心是讓開發者提供UserDetailsService來獲取UserDetails以及 PasswordEncoder來檢驗密碼是否有效:
private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
具體實現如下
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
可以看到此處的返回物件 userDetails
是由 UserDetailsService
的 loadUserByUsername(username)
來獲取的。
再來看驗證:
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
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();
// 比較passwordEncoder後的密碼是否和userdetails的密碼一致
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
小結:要自定義認證,使用DaoAuthenticationProvider,只需要為其提供PasswordEncoder和UserDetailsService就可以了。
時序圖
補充:SecurityContextHolder
的工作原理
這是一個工具類,只提供一些靜態方法,目的是用來儲存應用程式中當前使用人的安全上下文。
預設工作模式 MODE_THREADLOCAL
我們知道,一個應用同時可能有多個使用者,每個使用者對應不同的安全上下文,那麼SecurityContextHolder
是怎麼儲存這些安全上下文的呢 ?預設情況下,SecurityContextHolder
使用了ThreadLocal
機制來儲存每個使用者的安全上下文。這意味著,只要針對某個使用者的邏輯執行都是在同一個執行緒中進行,即使不在各個方法之間以引數的形式傳遞其安全上下文,各個方法也能通過SecurityContextHolder
工具獲取到該安全上下文。只要在處理完當前使用者的請求之後注意清除ThreadLocal
中的安全上下文,這種使用ThreadLocal
的方式是很安全的。當然在Spring Security
中,這些工作已經被Spring Security
自動處理,開發人員不用擔心這一點。
SecurityContextHolder
原始碼
public class SecurityContextHolder {
// ~ Static fields/initializers
// =====================================================================================
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
// ~ Methods
// ========================================================================================================
/**
* Explicitly clears the context value from the current thread.
*/
public static void clearContext() {
strategy.clearContext();
}
/**
* Obtain the current <code>SecurityContext</code>.
*
* @return the security context (never <code>null</code>)
*/
public static SecurityContext getContext() {
return strategy.getContext();
}
/**
* Primarily for troubleshooting purposes, this method shows how many times the class
* has re-initialized its <code>SecurityContextHolderStrategy</code>.
*
* @return the count (should be one unless you've called
* {@link #setStrategyName(String)} to switch to an alternate strategy.
*/
public static int getInitializeCount() {
return initializeCount;
}
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
}
else {
// Try to load a custom strategy
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
initializeCount++;
}
/**
* Associates a new <code>SecurityContext</code> with the current thread of execution.
*
* @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
*/
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
/**
* Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
* a given JVM, as it will re-initialize the strategy and adversely affect any
* existing threads using the old strategy.
*
* @param strategyName the fully qualified class name of the strategy that should be
* used.
*/
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
/**
* Allows retrieval of the context strategy. See SEC-1188.
*
* @return the configured strategy for storing the security context.
*/
public static SecurityContextHolderStrategy getContextHolderStrategy() {
return strategy;
}
/**
* Delegates the creation of a new, empty context to the configured strategy.
*/
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
public String toString() {
return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount="
+ initializeCount + "]";
}
}
Spring security獲取當前使用者
前面介紹過用SecurityContextHolder.getContext().getAuthentication() .getPrincipal();獲取當前使用者,還有一種方法:
經過spring security認證後,spring security會把一個SecurityContextImpl物件儲存到session中,此物件中有當前使用者的各種資料
SecurityContextImpl securityContextImpl =
(SecurityContextImpl) request.getSession.getAttribute("SPRING_SECURITY_CONTEXT");
//登入名
System.out.println("Username:" + securityContextImpl.getAuthentication().getName());
//登入密碼,未加密的
System.out.println("Credentials:" + securityContextImpl.getAuthentication().getCredentials());
SecurityContextImpl原始碼
/**
* Base implementation of {@link SecurityContext}.
* <p>
* Used by default by {@link SecurityContextHolder} strategies.
*
* @author Ben Alex
*/
public class SecurityContextImpl implements SecurityContext {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private Authentication authentication;
// ~ Methods
// ========================================================================================================
public boolean equals(Object obj) {
if (obj instanceof SecurityContextImpl) {
SecurityContextImpl test = (SecurityContextImpl) obj;
if ((this.getAuthentication() == null) && (test.getAuthentication() == null)) {
return true;
}
if ((this.getAuthentication() != null) && (test.getAuthentication() != null)
&& this.getAuthentication().equals(test.getAuthentication())) {
return true;
}
}
return false;
}
public Authentication getAuthentication() {
return authentication;
}
public int hashCode() {
if (this.authentication == null) {
return -1;
}
else {
return this.authentication.hashCode();
}
}
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString());
if (this.authentication == null) {
sb.append(": Null authentication");
}
else {
sb.append(": Authentication: ").append(this.authentication);
}
return sb.toString();
}
}
可以看出,主要就兩個方法getAuthentication()和setAuthentication()
問題: 如果修改了登入使用者的資訊,怎麼更新到security context中呢
目前想到的解決辦法是重新認證:
//首先在WebSecurityConfig中注入AuthenticationManager
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//在Controller中注入
@Autowired
private AuthenticationManager authenticationManager;
//接下來就是在會修改使用者資訊的地方設定重新認證
//也可以通過SecurityContextHolder獲取
SecurityContextImpl securityContext = (SecurityContextImpl) request.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(登入名,登入密碼);
Authentication authentication = authenticationManager.authenticate(token);
//重新設定authentication
securityContext.setAuthentication(authentication);
因為我做的是前後端分離,返回的都是json格式的資料,所以在專案中還遇到的問題有:
參考文章: