SpringSecurity中最要的三個實作
- 寫在前面
- 一、核心配置類 WebSecurityConfigurerAdapter
- 二、使用者/權限加載接口 UserDetailsService
- 三、接入控制管理(AccessDecisionManager)
- 四、請求攔截(OncePerRequestFilter)
- 五、核心驗證器(AuthenticationManager)
- 5.1、AuthenticationManager
- 5.2、ProviderManager
- 5.3、AuthenticationProvider
- 六、驗證流程圖,分析
寫在前面
一、核心配置類 WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//.and()
/*.formLogin()
.successHandler(loginSuccessHandler())
.failureHandler(authenticationFailureHandler())*/
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.userDetailsService(userDetailsService)
//OPTIONS請求全部放行
.authorizeRequests()
.antMatchers(/*HttpMethod.OPTIONS,*/ "/**").permitAll()
.antMatchers("/auth/login").permitAll()
.antMatchers("/auth/logout").permitAll()
.accessDecisionManager(accessDecisionManager());
//使用自定義的 Token過濾器 驗證請求的Token是否合法
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
super.configure(http);
}
@Bean
public JwtTokenFilter authenticationTokenFilterBean() {
return new JwtTokenFilter();
}
@Bean
public JwtTokenProvider jwtTokenProvider() {
return new JwtTokenProvider();
}
}
二、使用者/權限加載接口 UserDetailsService
@Component("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String userCode) throws UsernameNotFoundException {
UserInfo userInfo = userService.findByUserCode(userCode);
if (userInfo == null) {
throw new UsernameNotFoundException("-------UserDetailsService---------->" + userCode);
}
List<String> resources = userService.findAllResourcesByUserCode(userCode);
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
resources.forEach(code -> {
GrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(code);
grantedAuthorities.add(simpleGrantedAuthority);
});
return new User(userCode, userInfo.getPassword(), grantedAuthorities);
}
}
三、接入控制管理(AccessDecisionManager)
@Component("accessDecisionManager")
public class AuthAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
// 鑒權驗證
if (!configAttributes.isEmpty()) {
String authorize = null;
for (ConfigAttribute configAttribute : configAttributes) {
System.out.println("-------AccessDecisionManager--------------" + configAttribute.toString());
authorize = configAttribute.toString();
break;
}
if (Constants.NO_AUTHORIZE.equals(authorize)) {
return;
}
}
// 登陸驗證
SecurityUtils.getCurrentUserLogin());
if (Constants.ANONYMOUS_USER.equals(SecurityUtils.getCurrentUserLogin())) {
throw new AccessDeniedException(" 沒有權限通路! ");
}
//驗證權限
if (object instanceof FilterInvocation) {
FilterInvocation web = (FilterInvocation) object;
String uri = web.getRequestUrl();
System.out.println("-------AccessDecisionManager--------------" + uri);
String urlCode = this.getUrlCode(uri);
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
List<? extends GrantedAuthority> collect = authorities.stream().collect(Collectors.toList());
for (GrantedAuthority grantedAuthority : collect) {
String authority = grantedAuthority.getAuthority();
if (authority.equals(urlCode)) {
return;
}
}
}
throw new AccessDeniedException(" 沒有權限通路! ");
}
}
四、請求攔截(OncePerRequestFilter)
是封裝在 org.springframework.web.filter 這個包下的,可用于攔截請求,驗證請求頭中的資訊,或者初始基本資訊
springSecurity中可用這個配置。如下的環境
五、核心驗證器(AuthenticationManager)
5.1、AuthenticationManager
該對象提供了認證方法的入口,接收一個Authentiaton對象作為參數;
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
5.2、ProviderManager
它是 AuthenticationManager 的一個實作類,提供了基本的認證邏輯和方法;它包含了一個 List 對象,通過 AuthenticationProvider 接口來擴充出不同的認證提供者(當Spring Security預設提供的實作類不能滿足需求的時候可以擴充AuthenticationProvider 覆寫supports(Class<?> authentication) 方法);
驗證邏輯
AuthenticationManager 接收 Authentication 對象作為參數,并通過 authenticate(Authentication) 方法對其進行驗證;AuthenticationProvider實作類用來支撐對 Authentication 對象的驗證動作;UsernamePasswordAuthenticationToken實作了 Authentication主要是将使用者輸入的使用者名和密碼進行封裝,并供給 AuthenticationManager 進行驗證;驗證完成以後将傳回一個認證成功的 Authentication 對象;
Authentication
Authentication對象中的主要方法
public interface Authentication extends Principal, Serializable {
//#1.權限結合,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_ADMIN")傳回字元串權限集合
Collection<? extends GrantedAuthority> getAuthorities();
//#2.使用者名密碼認證時可以了解為密碼
Object getCredentials();
//#3.認證時包含的一些資訊。
Object getDetails();
//#4.使用者名密碼認證時可了解時使用者名
Object getPrincipal();
#5.是否被認證,認證為true
boolean isAuthenticated();
#6.設定是否能被認證
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
...
...
}
ProviderManager是AuthenticationManager的實作類,提供了基本認證實作邏輯和流程;
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//#1.擷取目前的Authentication的認證類型
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
//#2.周遊所有的providers使用supports方法判斷該provider是否支援目前的認證類型,不支援的話繼續周遊
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
#3.支援的話調用provider的authenticat方法認證
result = provider.authenticate(authentication);
if (result != null) {
#4.認證通過的話重新生成Authentication對應的Token
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 {
#5.如果#1 沒有驗證通過,則使用父類型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;
}
}
#6. 是否擦出敏感資訊
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;
}
-
1.周遊所有的 Providers,然後依次執行該 Provider 的驗證方法
如果某一個 Provider 驗證成功,則跳出循環不再執行後續的驗證;
如果驗證成功,會将傳回的 result 既 Authentication 對象進一步封裝為 Authentication Token; 比如 UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken 等;這些 Authentication Token 也都繼承自 Authentication 對象;
- 2.如果 #1 沒有任何一個 Provider 驗證成功,則試圖使用其 parent Authentication Manager 進行驗證;
- 3.是否需要擦除密碼等敏感資訊;
5.3、AuthenticationProvider
ProviderManager 通過 AuthenticationProvider 擴充出更多的驗證提供的方式;而 AuthenticationProvider 本身也就是一個接口,從類圖中我們可以看出它的實作類AbstractUserDetailsAuthenticationProvider 和AbstractUserDetailsAuthenticationProvider的子類DaoAuthenticationProvider 。DaoAuthenticationProvider 是Spring Security中一個核心的Provider,對所有的資料庫提供了基本方法和入口。
DaoAuthenticationProvider
DaoAuthenticationProvider主要做了以下事情
對使用者身份盡心加密操作;
#1.可直接傳回BCryptPasswordEncoder,也可以自己實作該接口使用自己的加密算法核心方法String encode(CharSequence rawPassword);和boolean matches(CharSequence rawPassword, String encodedPassword);
private PasswordEncoder passwordEncoder;
實作了 AbstractUserDetailsAuthenticationProvider 兩個抽象方法,
擷取使用者資訊的擴充點
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
主要是通過注入UserDetailsService接口對象,并調用其接口方法 loadUserByUsername(String username) 擷取得到相關的使用者資訊。UserDetailsService接口非常重要。
實作 additionalAuthenticationChecks 的驗證方法(主要驗證密碼);
AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider為DaoAuthenticationProvider提供了基本的認證方法;
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
“AbstractUserDetailsAuthenticationProvider.onlySupports”,
“Only UsernamePasswordAuthenticationToken is supported”));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
#1.擷取使用者資訊由子類實作即DaoAuthenticationProvider
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 {
#2.前檢查由DefaultPreAuthenticationChecks類實作(主要判斷目前使用者是否鎖定,過期,當機User接口)
preAuthenticationChecks.check(user);
#3.子類實作
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;
}
}
#4.檢測使用者密碼是否過期對應#2 的User接口
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
AbstractUserDetailsAuthenticationProvider主要實作了AuthenticationProvider的接口方法 authenticate 并提供了相關的驗證邏輯;
擷取使用者傳回UserDetails AbstractUserDetailsAuthenticationProvider定義了一個抽象的方法
protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
三步驗證工作
preAuthenticationChecks
additionalAuthenticationChecks(抽象方法,子類實作)
postAuthenticationChecks
将已認證驗證的使用者資訊封裝成 UsernamePasswordAuthenticationToken 對象并傳回;該對象封裝了使用者的身份資訊,以及相應的權限資訊,相關源碼如下,
protected Authentication createSuccessAuthentication(Object principal,
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;