1. Spring Security簡(jiǎn)介
??Spring Security,是一種基于 SpringAOP 和 Servlet 過濾器的安全框架。它提供全面的安全性解決方案,同時(shí)在 Web 請(qǐng)求級(jí)和方法調(diào)用級(jí)處理身份確認(rèn)和授權(quán)。
??它主要包含以下三個(gè)功能:
??(1)認(rèn)證(你是誰?)。
??(2)授權(quán)(你可以做什么?)。
??(3)攻擊防護(hù)(防止偽造身份)。
??原理簡(jiǎn)述:如下圖所示,spring security會(huì)自動(dòng)創(chuàng)建一組過濾器鏈。其中綠色的過濾器用來根據(jù)請(qǐng)求參數(shù)封裝用戶的認(rèn)證信息,如果該請(qǐng)求包含某過濾器所需要的參數(shù),比如用戶名和密碼時(shí),該過濾器會(huì)獲取這些信息,封裝成對(duì)應(yīng)的認(rèn)證對(duì)象;藍(lán)色的過濾器會(huì)捕獲訪問的異常,當(dāng)對(duì)受保護(hù)的資源進(jìn)行訪問時(shí),如果線程中沒有對(duì)應(yīng)的認(rèn)證信息,spring security將會(huì)自動(dòng)使用匿名用戶對(duì)該資源進(jìn)行訪問,如果權(quán)限不夠則會(huì)拋出異常,而異常將會(huì)被捕獲,然后根據(jù)你的配置信息將會(huì)跳轉(zhuǎn)到認(rèn)證頁面進(jìn)行認(rèn)證;橘色的過濾器是資源的前一個(gè)過濾器,主要用作鑒權(quán),如果當(dāng)前用戶的權(quán)限不夠,將會(huì)拋出異常。

2. 自定義用戶認(rèn)證邏輯
??(1)自定義獲取用戶信息
??在之前的測(cè)試中,我們都是通過配置spring.security.user.name | password將用戶名和密碼寫死到文件中,我們?nèi)绾螐臄?shù)據(jù)庫中獲取用戶名和密碼并對(duì)表單中輸入的信息進(jìn)行驗(yàn)證呢?我們只需要實(shí)現(xiàn)UserDetailsService接口,并交由spring工廠管理即可。
@Component
public class MyUserDetailService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
logger.info("從數(shù)據(jù)庫中查詢用戶名為'{}'的用戶信息",s);
return new User(s, "123456",AuthorityUtils.createAuthorityList("USER"));
}
}
??(2)處理用戶密碼的加解密
??用戶加密解密的類是PasswordEncoder,你可以實(shí)現(xiàn)該接口來定義自己的加解密邏輯。
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
??注意:就算相同的密碼加密之后的結(jié)果也是不相同的,如下所示都為"123456"加密后的結(jié)果
$2a$10$eQ5pNpNPDnvMvOs1w5Xz9.hdjwdDSFw7kmXIPcpvsxgkOIcDigmqu
$2a$10$aYTOucDNIAweM8sHyiEVye40761Nz4sdiizncSOBA.39xdxdKjZBO
??因?yàn)檫@些加密后的字符串本身就包含加密所使使用到的鹽值的信息,這些鹽值是隨機(jī)生成的。在和原密碼進(jìn)行比對(duì)的過程中,spring security會(huì)將這些鹽值重新取出來按照相同的算法對(duì)表單輸入的密碼進(jìn)行加密,然后判斷是否和數(shù)據(jù)庫中加密后的字符串相等。
3. 個(gè)性化用戶認(rèn)證流程
??(1)自定義登錄頁面
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/imooc-sighIn.html") //配置登錄頁面
.loginProcessingUrl("/authentication/form") //自定義登錄處理url,默認(rèn)為 /login
.and()
.authorizeRequests()
.antMatchers("/imooc-sighIn.html").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
}
??(2)自定義登錄成功(失敗)處理
??SpringSecurity登錄成功會(huì)自動(dòng)跳轉(zhuǎn)到引發(fā)跳轉(zhuǎn)的頁面,我們可以實(shí)現(xiàn)AuthenticationSuccessHandler接口,實(shí)現(xiàn)自己的登錄成功的處理,在登錄成功后執(zhí)行自己的邏輯,比如用戶自動(dòng)簽到、用戶登錄日志等。
@Component
public class ImoocAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper ;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("用戶{}簽到成功!", authentication.getName());
response.setContentType("application/json;charSet=UTF-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(authentication));
writer.flush();
}
}
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
......
.successHandler(imoocAuthenticationSuccessHandler)
......
}
??失敗處理器同成功處理器,需要實(shí)現(xiàn)AuthenticationFailureHandler接口,然后添加到spring security配置類中。
4. 用戶認(rèn)證流程流程
??核心類簡(jiǎn)介,我們首先看下我們的配置。
protected void configure(HttpSecurity http) throws Exception {
String loginPage = securityProperties.getBrowser().getLoginPage();
http.formLogin()
.loginPage("/authentication/require") //配置登錄頁面
.loginProcessingUrl("/authentication/form") //自定義登錄處理url,默認(rèn)為 /login
.successHandler(imoocAuthenticationSuccessHandler)
.failureHandler(imoocAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/authentication/require", loginPage).permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}

??在第一次訪問一個(gè)受保護(hù)的對(duì)象時(shí),請(qǐng)求經(jīng)過的過濾器鏈為SecurityContextPersistenceFilter -->AnonymousAuthenticationFilter --> ExceptionTranslationFilter --> FilterSecurityInterceptor。
??(1)SecurityContextPersistenceFilter過濾器的代碼如下,
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
......
//1. 該過濾器會(huì)從session獲取SecurityContext,如果該SecurityContext為空,則創(chuàng)建一個(gè)空的SecurityContext
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
//2. 將該SecurityContext放到線程中,然后執(zhí)行下一個(gè)過濾器
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
//3. 在返回響應(yīng)時(shí),因?yàn)榫€程池的原因,從線程中移除SecurityContext,如果session中沒有SecurityContext則將SecurityContext放到session中。
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
}
}
??(2)AnonymousAuthenticationFilter過濾器在所有的身份認(rèn)證過濾器的最后,當(dāng)經(jīng)過了所有的過濾器,線程中還是沒有認(rèn)證信息時(shí),該過濾器會(huì)往 SecurityContext 中增加一個(gè)匿名用戶的認(rèn)證信息。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
}
chain.doFilter(req, res);
}
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
??(3)ExceptionTranslationFilter過濾器就是用來捕獲鑒權(quán)過濾器拋出的異常,如果是未授權(quán)異常,則引導(dǎo)用戶跳轉(zhuǎn)到登錄頁面,代碼如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
......
try {
//1. 該過濾器的下一個(gè)過濾器就是鑒權(quán)過濾器
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// 根據(jù)捕獲的異常類型,做對(duì)應(yīng)的處理,比如認(rèn)證異常、訪問拒絕異常等等。如何處理,自己看。
}
}
??(4)FilterSecurityInterceptor,用戶鑒權(quán),判斷當(dāng)前認(rèn)證的用戶是否可以訪問該資源。如果無法訪問,拋出異常。
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
//1. 鑒權(quán)動(dòng)作
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//2. 訪問受保護(hù)的資源
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
protected InterceptorStatusToken beforeInvocation(Object object) {
......
//1. 首先從線程中獲取認(rèn)證信息,如果沒有認(rèn)證則是匿名用戶身份。
Authentication authenticated = authenticateIfRequired();
try {
//2. 判斷當(dāng)前用戶是否可以訪問該資源,詳細(xì)之后再說
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
throw accessDeniedException;
}
......
}
??由于第一次訪問的原因, 所以是由匿名用戶進(jìn)行訪問,會(huì)拋出訪問拒絕異常,此時(shí)ExceptionTranslationFilter會(huì)引導(dǎo)用戶跳轉(zhuǎn)到認(rèn)證頁面,當(dāng)用戶輸入完成認(rèn)證信息點(diǎn)擊登錄時(shí),請(qǐng)求再一次經(jīng)過這些攔截器鏈,不過不同的是,過濾器鏈中會(huì)新增一個(gè)過濾器,由于WebSecurityConfigurerAdapter配置類中的配置,提交表單(表單提交的地址是/authentication/form)的請(qǐng)求會(huì)被UsernamePasswordAuthenticationFilter過濾器攔截(默認(rèn)攔截/login請(qǐng)求)。所以如上圖所示,該請(qǐng)求的過濾器鏈中會(huì)新增一個(gè)用戶名密碼認(rèn)證過濾器。
??過濾器UsernamePasswordAuthenticationFilter的代碼如下:
1. 首先會(huì)調(diào)用父類的AbstractAuthenticationProcessingFilter的doFilter方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
//1. 如果請(qǐng)求中沒有包含所需要的信息,比如用戶名、密碼,則跳過,進(jìn)入下一個(gè)過濾器
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
//2. 使用模板方法模式對(duì)用戶進(jìn)行認(rèn)證
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
//3. 木雞啊
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
//4. 調(diào)用失敗處理器
unsuccessfulAuthentication(request, response, failed);
return;
}
//5. 將認(rèn)證信息放到線程中,然后調(diào)用成功處理器、調(diào)用rememberMeServiece服務(wù)等
successfulAuthentication(request, response, chain, authResult);
}
2. 調(diào)用UsernamePasswordAuthenticationFilter方法進(jìn)行封裝token,該類包含兩個(gè)構(gòu)造器
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//1. 從請(qǐng)求中獲取用戶名和密碼參數(shù)值
String username = obtainUsername(request);
String password = obtainPassword(request);
//2. 根據(jù)用戶名和密碼封裝UsernamePasswordAuthenticationToken 對(duì)象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
//3. 將請(qǐng)求的一些信息添加到token中 ,比如請(qǐng)求地址、sessionid等
setDetails(request, authRequest);
//4. 通過AuthenticationManager對(duì)象認(rèn)證token,并返回用戶認(rèn)證信息。
return this.getAuthenticationManager().authenticate(authRequest);
}
(1)UsernamePasswordAuthenticationToken包括兩個(gè)構(gòu)造方法,一種是已認(rèn)證、一種是未認(rèn)證
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false); //look here !!!!
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
??在上述代碼的第四步的認(rèn)證過程中,會(huì)獲取當(dāng)前過濾器的認(rèn)證管理器AuthenticationManager,然后通過它進(jìn)行對(duì)token的認(rèn)證,而認(rèn)證管理器包含了多個(gè)認(rèn)證提供者AuthenticationProvider,認(rèn)證管理器實(shí)際上是使用它進(jìn)行身份認(rèn)證的,認(rèn)證管理器認(rèn)證的代碼如下。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
......
for (AuthenticationProvider provider : getProviders()) {
//1. 從AuthenticationManagers 中獲取 可以處理該token類型的AuthenticationProvider
if (!provider.supports(Authentication)) {
continue;
}
//2. (用戶名和密碼方式使用的是DaoAuthenticationProvider)認(rèn)證管理器開始對(duì)token進(jìn)行驗(yàn)證
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
throw e
}
}
//3. 如果該認(rèn)證管理器中沒有可處理的提供者,交由父管理器處理
if (result == null && parent != null) {
try {
result = parent.authenticate(authentication);
}
......
}
if (result != null) {
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// 4. 如果沒有可以處理的該token類型的提供者,則拋出異常
......
throw lastException;
}
??選取到可以處理該token類型的認(rèn)證提供者后,就開始處理該token。DaoAuthenticationProvider的處理過程如下:
1. 首先調(diào)用父類AbstractUserDetailsAuthenticationProvider的authenticate方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//1. 獲取token的憑證,也就是表單的用戶名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
//2. 從緩存中根據(jù)用戶名獲得UserDetails
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//3. 調(diào)用DaoAuthenticationProvider的retrieveUser方法進(jìn)行處理
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {}
}
//4. 檢測(cè)用戶是否有效,比如user的賬號(hào)是否凍結(jié)、是否過期等,user密碼是否和認(rèn)證信息中的密碼一致(表單中輸入的密碼),如果無效拋出異常
......代碼太長(zhǎng)略
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//5. 根據(jù)認(rèn)證信息重新封裝token
return createSuccessAuthentication(principalToReturn, authentication, user);
}
//認(rèn)證成功之后,調(diào)用第二個(gè)構(gòu)造器進(jìn)行構(gòu)造UsernamePasswordAuthenticationToken
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
2. 在上個(gè)方法的第二步中,調(diào)用的retrieveUser方法如下所示
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
//使用你自定義的UserDetailsService獲取用戶信息
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
throw notFound;
}
return loadedUser;
}
??認(rèn)證的整體流程如下所示:

5. 記住我功能
- 記住我基本原理

??在AbstractAuthenticationProcessingFilter類的方法doFilter()方法認(rèn)證完成之后,會(huì)調(diào)用successfulAuthentication方法,如下所示,該方法主要做了三件事:將認(rèn)證的用戶信息放入線程;調(diào)用記住我服務(wù)(默認(rèn)是NullRememberMeServices,什么都不做);調(diào)用成功處理器。
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
}
??如上圖所示,如果配置了記住我功能,在對(duì)用戶認(rèn)證完成之后,會(huì)先將認(rèn)證后的token持久化到介質(zhì)中(可以是數(shù)據(jù)庫、session等),然后再將該token寫入到瀏覽器的cookie中。當(dāng)下次請(qǐng)求到來時(shí)如果沒有過濾器可以從請(qǐng)求中獲取認(rèn)證信息時(shí),則嘗試使用該記住我過濾器從持久化的介質(zhì)中讀取該token信息,然后根據(jù)token的信息使用自定義的UserDetailServcie進(jìn)行登錄流程,登錄完成之后將該認(rèn)證信息放到線程中,如果所有的瀏覽器都無法獲取認(rèn)證信息,則使用匿名身份進(jìn)行訪問。該過濾器的位置如下所示。

- 記住我基本實(shí)現(xiàn)
1. 在表單中添加一個(gè)單選框,name屬性必須為 "remember-me",也可以在配置中指定
<input type="checkbox" name="remember-me" value="true"/>記住我
2. 指定token的持久化方式
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
jdbcTokenRepository.setCreateTableOnStartup(true); //在項(xiàng)目啟動(dòng)時(shí),是否創(chuàng)建表。也可以手動(dòng)執(zhí)行,sql語句在該類的CREATE_TABLE_SQL屬性。
return jdbcTokenRepository;
}
3. 配置rememberMe的功能
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private SecurityProperties securityProperties;
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
......
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(30)
.userDetailsService(userDetailsService)
.....
}
- 記住我源碼解析
??在用戶登錄完成之后,會(huì)調(diào)用AbstractAuthenticationProcessingFilter類的successfulAuthentication方法,如下所示:
1. 用戶登錄完成的處理
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
}
2. 然后會(huì)調(diào)用記住我服務(wù)(PersistentTokenBasedRememberMeServices類)的loginSuccess方法
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
//1. 將用戶信息封裝成持久化的token
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
//2. 將該token持久化到數(shù)據(jù)庫中
tokenRepository.createNewToken(persistentToken);
//3. 將該token添加到cookie中
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
??在登錄完成之后,下次請(qǐng)求到來時(shí),會(huì)被RememberMeAuthenticationFilter過濾器進(jìn)行攔截。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//1. 說明該過濾器之前沒有過濾器可以從請(qǐng)求中獲取認(rèn)證信息。
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//2. 嘗試使用cookie中的信息從數(shù)據(jù)庫中獲取認(rèn)證信息
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
try {
//3. 根據(jù)使用認(rèn)證管理器對(duì)認(rèn)證信息進(jìn)行認(rèn)證,認(rèn)證成功后將認(rèn)證信息放入線程
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
if (successHandler != null) {
successHandler.onAuthenticationSuccess(request, response,
rememberMeAuth);
return;
}
}
catch (AuthenticationException authenticationException) {
//4. 調(diào)用認(rèn)證服務(wù)的失敗處理
rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response,
authenticationException);
}
}
chain.doFilter(request, response);
}
else {
//如果從之前的過濾器已經(jīng)獲取到認(rèn)證信息,則進(jìn)入下一個(gè)過濾器
chain.doFilter(request, response);
}
}
??上述代碼的第二步從數(shù)據(jù)庫中獲取用戶認(rèn)證信息的代碼如下所示:
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
//1. 首先從請(qǐng)求的cookie中獲取rememberMe的token的相關(guān)信息(比如token在數(shù)據(jù)庫的唯一標(biāo)識(shí))
String rememberMeCookie = extractRememberMeCookie(request);
UserDetails user = null;
try {
String[] cookieTokens = decodeCookie(rememberMeCookie);
//2. 從數(shù)據(jù)庫中獲取記住我的用戶信息
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
//3. 構(gòu)建RememberMeAuthenticationToken類型的認(rèn)證對(duì)象
return createSuccessfulAuthentication(request, user);
}
catch (Exception cte) {
throw cte;
}
}
protected UserDetails processAutoLoginCookie(String[] cookieTokens,
HttpServletRequest request, HttpServletResponse response) {
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
//1. 從數(shù)據(jù)庫中查詢用戶信息
PersistentRememberMeToken token = tokenRepository
.getTokenForSeries(presentedSeries);
//2. 根據(jù)用戶信息token封裝一個(gè)新的token
PersistentRememberMeToken newToken = new PersistentRememberMeToken(
token.getUsername(), token.getSeries(), generateTokenData(), new Date());
try {
//3. 將新token更新到數(shù)據(jù)庫中
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
newToken.getDate());
addCookie(newToken, request, response);
}
//4. 調(diào)用自定義的UserDetailService進(jìn)行查詢用戶
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
6. 實(shí)戰(zhàn)開發(fā)手機(jī)登錄
??在上邊看過UsernamePasswordAuthenticationFilter的執(zhí)行流程之后,我們開始根據(jù)用戶名和密碼驗(yàn)證的方式進(jìn)行開發(fā)。

??由上圖可知,我們構(gòu)建認(rèn)證用戶信息需要三個(gè)類:從請(qǐng)求中獲取用戶信息的SmsAuthenticationFilter;驗(yàn)證并構(gòu)建認(rèn)證信息的SmsAuthenticationProvider;封裝的手機(jī)號(hào)的認(rèn)證信息的類SmsAuthenticationToken。在這該SmsAuthenticationFilter過濾器之前,應(yīng)該還需要驗(yàn)證手機(jī)的驗(yàn)證碼是否正確的過濾器。所以總共需要四個(gè)類。
??(1)驗(yàn)證手機(jī)號(hào)是否正確的過濾器SmsCheckFilter
public class SmsCheckFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if(表單提交的驗(yàn)證碼.equals( session中的驗(yàn)證碼 ))
filterChain.doFilter(httpServletRequest, httpServletResponse);
}else{
驗(yàn)證碼錯(cuò)誤的失敗處理?。?!
}
}
}
??(2)從請(qǐng)求中獲取用戶信息的過濾器SmsCodeAuthenticationFilter(仿照UsernamePasswordAuthenticationFilter)。
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
.....
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher( "手機(jī)登錄表單提交的url", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//1. 從請(qǐng)求中獲取用戶登錄所使用的手機(jī)號(hào)
String mobile = obtainMobile(request);
//2. 使用手機(jī)號(hào)封裝認(rèn)證信息
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
//3. 將請(qǐng)求信息附加到token中
setDetails(request, authRequest);
// 4. 開始對(duì)token進(jìn)行驗(yàn)證
return this.getAuthenticationManager().authenticate(authRequest);
}
......
}
??(3)封裝用戶信息的tokenSmsCodeAuthenticationToken(仿照UsernamePasswordAuthenticationToken)
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private Object mobile;
public SmsCodeAuthenticationToken(Object mobile) {
super(null);
this.mobile = mobile;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.mobile = principal;
super.setAuthenticated(true); // must use super, as we override
}
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.mobile;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
??(4)驗(yàn)證并構(gòu)建已認(rèn)證的認(rèn)證信息的類SmsCodeAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Object principal = authentication.getPrincipal();
UserDetails userDetails = userDetailsService.loadUserByUsername(principal.toString());
if(userDetails == null){
throw new RuntimeException("手機(jī)號(hào)未綁定??!");
}
SmsCodeAuthenticationToken smsCodeAuthenticationToken = new SmsCodeAuthenticationToken(principal, userDetails.getAuthorities());
smsCodeAuthenticationToken.setDetails(authentication.getDetails());
return smsCodeAuthenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
??(5)將這以上所有類聯(lián)系起來,并添加到配置中
@Component
public class SmsCodeFilterConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
//1. 認(rèn)證時(shí)使用該UserDetailService進(jìn)行查詢手機(jī)號(hào)對(duì)應(yīng)的用戶
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
//2. 將spring security的認(rèn)證管理器添加到自定義過濾器中,用來篩選合適的認(rèn)證器對(duì)token進(jìn)行驗(yàn)證
smsCodeAuthenticationFilter.setAuthenticationManager(authenticationManager);
//3. 將認(rèn)證提供者添加到認(rèn)證管理器中,并將該自定義過濾器添加到過濾器鏈中
http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
將以上配置和核對(duì)手機(jī)號(hào)的過濾器添加到spring security的配置中。
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
protected void configure(HttpSecurity http) throws Exception {
String loginPage = securityProperties.getBrowser().getLoginPage();
http.addFilterBefore(new SmsCheckFilter(), UsernamePasswordAuthenticationFilter.class)
.....
.apply(smsCodeFilterConfig);
}
}