Spring Security 主要實現了Authentication(認證,解決who are you? ) 和 Access Control(訪問控制,也就是what are you allowed to do?,也稱為Authorization)。Spring Security在架構上將認證與授權分離,並提供了擴展點。
核心對象
主要代碼在spring-security-core
包下面。要了解Spring Security,需要先關注里面的核心對象。
SecurityContextHolder, SecurityContext 和 Authentication
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與UserDetailsService
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;
}
GrantedAuthority
在UserDetails里說了,GrantedAuthority可以理解為角色,例如 ROLE_ADMINISTRATOR
or ROLE_HR_SUPERVISOR
。
小結
SecurityContextHolder
, 用來訪問SecurityContext
.SecurityContext
, 用來存儲Authentication
.Authentication
, 代表憑證.GrantedAuthority
, 代表權限.UserDetails
, 用戶信息.UserDetailsService
,獲取用戶信息.
Authentication認證
AuthenticationManager
實現認證主要是通過AuthenticationManager接口,它只包含了一個方法:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
authenticate()方法主要做三件事:
- 如果驗證通過,返回Authentication(通常帶上authenticated=true)。
- 認證失敗拋出
AuthenticationException
- 如果無法確定,則返回null
AuthenticationException
是運行時異常,它通常由應用程序按通用方式處理,用戶代碼通常不用特意被捕獲和處理這個異常。
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。
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 {
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;
}
從上面的代碼可以看出, ProviderManager
有一個可選parent,如果parent不為空,則調用parent.authenticate(authentication)
AuthenticationProvider
AuthenticationProvider
有多種實現,大家最關注的通常是DaoAuthenticationProvider
,繼承於AbstractUserDetailsAuthenticationProvider
,核心是通過UserDetails
來實現認證,DaoAuthenticationProvider
默認會自動加載,不用手動配。
先來看AbstractUserDetailsAuthenticationProvide
r,看最核心的authenticate
:
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);
}
上面的檢驗主要基於UserDetails實現,其中獲取用戶和檢驗邏輯由具體的類去實現,默認實現是DaoAuthenticationProvider,這個類的核心是讓開發者提供UserDetailsService來獲取UserDetails以及 PasswordEncoder來檢驗密碼是否有效:
private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
看具體的實現,retrieveUser
,直接調用userDetailsService獲取用戶:
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;
}
再來看驗證:
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就可以了。
定制 Authentication Managers
Spring Security提供了一個Builder類AuthenticationManagerBuilder
,借助它可以快速實現自定義認證。
看官方源碼說明:
SecurityBuilder used to create an AuthenticationManager . Allows for easily building in memory authentication, LDAP authentication, JDBC based authentication, adding UserDetailsService , and adding AuthenticationProvider's.
AuthenticationManagerBuilder可以用來Build一個AuthenticationManager,可以創建基於內存的認證、LDAP認證、 JDBC認證,以及添加UserDetailsService和AuthenticationProvider。
簡單使用:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,TokenProvider tokenProvider,CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.userDetailsService = userDetailsService;
this.tokenProvider = tokenProvider;
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
}
@PostConstruct
public void init() {
try {
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/account/reset-password/init").permitAll()
.antMatchers("/api/account/reset-password/finish").permitAll()
.antMatchers("/api/profile-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.apply(securityConfigurerAdapter());
}
}
授權與訪問控制
一旦認證成功,我們可以繼續進行授權,授權是通過AccessDecisionManager
來實現的。框架有三種實現,默認是AffirmativeBased,通過AccessDecisionVoter
決策,有點像ProviderManager
委托給AuthenticationProviders
來認證。
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
// 遍歷DecisionVoter
for (AccessDecisionVoter voter : getDecisionVoters()) {
// 投票
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
// 一票否決
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
來看AccessDecisionVoter:
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
object是用戶要訪問的資源,ConfigAttribute則是訪問object要滿足的條件,通常payload是字符串,比如ROLE_ADMIN 。所以我們來看下RoleVoter的實現,其核心就是從authentication提取出GrantedAuthority,然后和ConfigAttribute比較是否滿足條件。
public boolean supports(ConfigAttribute attribute) {
if ((attribute.getAttribute() != null)
&& attribute.getAttribute().startsWith(getRolePrefix())) {
return true;
}
else {
return false;
}
}
public boolean supports(Class<?> clazz) {
return true;
}
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
if(authentication == null) {
return ACCESS_DENIED;
}
int result = ACCESS_ABSTAIN;
// 獲取GrantedAuthority信息
Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
// 默認拒絕訪問
result = ACCESS_DENIED;
// Attempt to find a matching granted authority
for (GrantedAuthority authority : authorities) {
// 判斷是否有匹配的 authority
if (attribute.getAttribute().equals(authority.getAuthority())) {
// 可訪問
return ACCESS_GRANTED;
}
}
}
}
return result;
}
這里要疑問,ConfigAttribute哪來的?其實就是上面ApplicationSecurity的configure里的。
web security 如何實現
Web層中的Spring Security(用於UI和HTTP后端)基於Servlet Filters
,下圖顯示了單個HTTP請求的處理程序的典型分層。
Spring Security通過FilterChainProxy
作為單一的Filter注冊到web層,Proxy內部的Filter。
FilterChainProxy相當於一個filter的容器,通過VirtualFilterChain來依次調用各個內部filter
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
private static class VirtualFilterChain implements FilterChain {
private final FilterChain originalChain;
private final List<Filter> additionalFilters;
private final FirewalledRequest firewalledRequest;
private final int size;
private int currentPosition = 0;
private VirtualFilterChain(FirewalledRequest firewalledRequest,
FilterChain chain, List<Filter> additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);
}
}
}
參考
- https://spring.io/guides/topicals/spring-security-architecture/
- https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#overall-architecture
作者:Jadepeng
出處:jqpeng的技術記事本--http://www.cnblogs.com/xiaoqi
您的支持是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。