當用戶登陸發起認證請求時,會經過UsernamePasswordAuthenticationFilter
進行用戶認證,認證成功以後,SpringSecurity 調用前期配置好的記住我功能,實際是調用了RememberMeService
接口,其接口的實現類會將用戶的信息生成Token
並將它寫入 response 的Cookie
中,在寫入的同時,內部的TokenRepositoryTokenRepository
會將這份Token
再存入數據庫一份。html
當用戶再次訪問服務器資源的時候,首先會通過RememberMeAuthenticationFiler
過濾器,在這個過濾器裏面會讀取當前請求中攜帶的 Cookie,這裏存着上次服務器保存 的Token
,而後去數據庫中查找是否有相應的 Token,若是有,則再經過UserDetailsService
獲取用戶的信息。前端
從圖中能夠得知記住個人過濾器在過濾鏈的中部,注意是在UsernamePasswordAuthenticationFilter
以後。java
在 html 中增長記住我複選框checkbox控件,注意其中複選框的name
必定必須爲remember-me
git
<input type="checkbox" name="remember-me" value="true"/>
複製代碼
本例中使用了 springboot 管理的數據庫源,因此注意要配置spring-boot-starter-jdbc
的依賴:github
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
複製代碼
若是不配置會報編譯異常:spring
The type org.springframework.jdbc.core.support.JdbcDaoSupport cannot be resolved. It is indirectly referenced from required .class files
複製代碼
記住個人安全認證配置:sql
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 將自定義的驗證碼過濾器放置在 UsernamePasswordAuthenticationFilter 以前
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/login") // 設置登陸頁面
.loginProcessingUrl("/user/login") // 自定義的登陸接口
.successHandler(myAuthenctiationSuccessHandler)
.failureHandler(myAuthenctiationFailureHandler)
.defaultSuccessUrl("/home").permitAll() // 登陸成功以後,默認跳轉的頁面
.and().authorizeRequests() // 定義哪些URL須要被保護、哪些不須要被保護
.antMatchers("/", "/index", "/user/login", "/code/image").permitAll() // 設置全部人均可以訪問登陸頁面
.anyRequest().authenticated() // 任何請求,登陸後能夠訪問
.and().csrf().disable() // 關閉csrf防禦
.rememberMe() // 記住我配置
.tokenRepository(persistentTokenRepository()) // 配置數據庫源
.tokenValiditySeconds(3600)
.userDetailsService(userDetailsService);
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl persistentTokenRepository = new JdbcTokenRepositoryImpl();
// 將 DataSource 設置到 PersistentTokenRepository
persistentTokenRepository.setDataSource(dataSource);
// 第一次啓動的時候自動建表(能夠不用這句話,本身手動建表,源碼中有語句的)
// persistentTokenRepository.setCreateTableOnStartup(true);
return persistentTokenRepository;
}
}
複製代碼
注意:在數據庫源配置以前,建議手動在數據庫中新增一張保存的cookie
表,其數據庫腳本在JdbcTokenRepositoryImpl
的靜態屬性中配置了:數據庫
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
/** Default SQL for creating the database table to store the tokens */
public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
+ "token varchar(64) not null, last_used timestamp not null)";
}
複製代碼
所以能夠事先執行如下sql 腳本建立表:安全
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
複製代碼
固然,JdbcTokenRepositoryImpl
自身還有一個setCreateTableOnStartup()
方法進行開啓自動建表操做,可是不建議使用。springboot
當成功登陸以後,RememberMeService
會將成功登陸請求的cookie
存儲到配置的數據庫中:
首先進入到AbstractAuthenticationProcessingFilter
過濾器中的doFilter()
方法:
public abstract class AbstractAuthenticationProcessingFilter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
……
try {
authResult = attemptAuthentication(request, response);
……
}
catch (InternalAuthenticationServiceException failed) {
……
}
successfulAuthentication(request, response, chain, authResult);
}
}
複製代碼
其中當用戶認證成功以後,會進入successfulAuthentication()
方法,在用戶信息被保存在了SecurityContextHolder
以後,其中就調用了rememberMeServices.loginSuccess()
:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
……
SecurityContextHolder.getContext().setAuthentication(authResult);
// 調用記住我服務接口的登陸成功方法
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
複製代碼
在這個RememberMeServices
有個抽象實現類,在抽象實現類loginSuccess()
方法中進行了記住我功能判斷,爲何前端的複選框控件的 name 必須爲remember-me
,緣由就在此:
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
public static final String DEFAULT_PARAMETER = "remember-me";
private String parameter = DEFAULT_PARAMETER;
@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
}
複製代碼
當識別到記住我功能開啓的時候,就會進入onLoginSuccess()
方法,其具體的方法實如今PersistentTokenBasedRememberMeServices
類中:
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
// 保存cookie到數據庫
tokenRepository.createNewToken(persistentToken);
// 將cookie回寫一份到響應中
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
}
複製代碼
上面的tokenRepository.createNewToken()
和addCookie()
就將 cookie 保存到數據庫並回顯到響應中。
當第二次請求傳到服務器的時候,請求會被RememberMeAuthenticationFilter
過濾器進行過濾:過濾器首先斷定以前的過濾器都沒有認證經過當前用戶,也就是SecurityContextHolder
中沒有已經認證的信息,因此會調用rememberMeServices.autoLogin()
的自動登陸接口拿到已經過認證的rememberMeAuth
進行用戶認證登陸:
public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// SecurityContextHolder 不存在已經認證的 authentication,表示前面的過濾器沒有作過任何身份認證
if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 調用自動登陸接口
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
……
}
catch (AuthenticationException authenticationException) {
……
}
}
chain.doFilter(request, response);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(request, response);
}
}
}
複製代碼
這個自動登陸的接口,又由其抽象實現類進行實現:
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
// 從請求中獲取cookie
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}
UserDetails user = null;
try {
// 解碼請求中的cookie
String[] cookieTokens = decodeCookie(rememberMeCookie);
// 根據 cookie 找到用戶認證
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
……
}
cancelCookie(request, response);
return null;
}
}
複製代碼
processAutoLoginCookie()
的具體實現仍是由PersistentTokenBasedRememberMeServices
來實現,總得來講就是一頓斷定當前的cookieTokens
是否是在數據庫中存在tokenRepository.getTokenForSeries(presentedSeries)
,並判斷是否是同樣的,若是同樣,就是把當前請求的新 token 更新保存到數據庫,最後經過當前請求token中的用戶名調用UserDetailsService.loadUserByUsername()
進行用戶認證。
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2
+ " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}
final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];
// 從數據庫查詢上次保存的token
PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
// 查詢不到拋異常
throw new RememberMeAuthenticationException(……);
}
// token 不匹配拋出異常
// We have a match for this user/series combination
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete all logins for this user and throw
// an exception to warn them.
tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(……);
}
// 過時判斷
if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date());
try {
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
addCookie(newToken, request, response);
}
catch (Exception e) {
……
}
return getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
複製代碼
我的博客:woodwhale's blog
博客園:木鯨魚的博客