Spring Security 解析(三) —— 個性化認證 以及 RememberMe 實現

Spring Security 解析(三) —— 個性化認證 以及 RememberMe 實現

  在學習Spring Cloud 時,遇到了受權服務oauth 相關內容時,老是隻知其一;不知其二,所以決定先把Spring Security 、Spring Security Oauth2 等權限、認證相關的內容、原理及設計學習並整理一遍。本系列文章就是在學習的過程當中增強印象和理解所撰寫的,若有侵權請告知。html

項目環境:git

- JDK1.8github

- Spring boot 2.xspring

- Spring Security 5.x數據庫

1、個性化認證


(一) 配置登陸

   在 受權過程 和 認證過程 中咱們都是使用的 Security 默認的一個登陸頁面(/login),那麼若是咱們想自定義一個登陸頁面該如何實現呢?其實很簡單,咱們新建 FormAuthenticationConfig 配置類,而後在configure(HttpSecurity http) 方法中實現如下設置:json

http.formLogin()
                //能夠設置自定義的登陸頁面 或者 (登陸)接口
                // 注意1: 通常來講設置成(登陸)接口後,該接口會配置成無權限便可訪問,因此會走匿名filter, 也就意味着不會走認證過程了,因此咱們通常不直接設置成接口地址
                // 注意2: 這裏配置的 地址必定要配置成無權限訪問,不然將出現 一直重定向問題(由於無權限後又會重定向到這裏配置的登陸頁url)
                .loginPage(securityProperties.getLogin().getLoginPage())
                //.loginPage("/loginRequire")
                // 指定驗證憑據的URL(默認爲 /login) ,
                // 注意1:這裏修改後的 url 會意味着  UsernamePasswordAuthenticationFilter 將 驗證此處的 url
                // 注意2: 與 loginPage設置的接口地址是有 區別, 一但 loginPage 設置了的是訪問接口url,那麼此處配置將無任何意義
                // 注意3: 這裏設置的 Url 是有默認無權限訪問的
                .loginProcessingUrl(securityProperties.getLogin().getLoginUrl())
                //分別設置成功和失敗的處理器
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler);複製代碼

  最後在 SpringSecurityConfig 的 configure(HttpSecurity http) 方法中 調用 formAuthenticationConfig.configure(http) 便可;安全

   正如看到的同樣,咱們經過 loginPage()設置 登陸頁面接口, 經過 loginProcessingUrl() 設置 UsernamePasswordAuthenticationFilter 要匹配的 接口地址(必定是Post)(看過受權過程的同窗應該都知道其默認的是/login)。 這裏有如下幾點值得注意:cookie

- loginPage() 這裏配置的 地址(不論是接口url仍是登陸頁面)必定要配置成無權限訪問,不然將出現 一直重定向問題(由於無權限後又會重定向到這裏配置的登陸頁urlapp

- loginPage() 通常來講不直接設置成(登陸)接口,由於設置了接口會配置成無權限便可訪問(固然設置成登陸頁面也須要配置無權限訪問),因此會走匿名filter, 也就意味着不會走認證過程了,因此咱們通常不直接設置成接口地址ide

- loginProcessingUrl() 這裏修改後的 url 會意味着 UsernamePasswordAuthenticationFilter 將 驗證此處的 url

- loginProcessingUrl() 這裏設置的 Url 是有默認無權限訪問的,與 loginPage設置的接口地址是有 區別, 一但 loginPage 設置了的是接口url,那麼此處配置將無任何意義

- successHandler() 和 failureHandler 分別 設置認證成功處理器 和 認證失敗處理器 (若是對這2個處理器沒印象的話,建議回顧下受權過程)

(二) 配置成功和失敗處理器

   在受權過程當中,咱們增簡單說起到過這2個處理器,在Security中默認的處理器分別是SavedRequestAwareAuthenticationSuccessHandler 和 SimpleUrlAuthenticationFailureHandler ,此次咱們自定義這2個處理器,分別爲 CustomAuthenticationSuccessHandler ( extends SavedRequestAwareAuthenticationSuccessHandler ) 重寫 onAuthenticationSuccess() 方法 :

@Component("customAuthenticationSuccessHandler")
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private SecurityProperties securityProperties;

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登陸成功");
        // 若是設置了loginSuccessUrl,老是跳到設置的地址上
        // 若是沒設置,則嘗試跳轉到登陸以前訪問的地址上,若是登陸前訪問地址爲空,則跳到網站根路徑上
        if (!StringUtils.isEmpty(securityProperties.getLogin().getLoginSuccessUrl())) {
            requestCache.removeRequest(request, response);
            setAlwaysUseDefaultTargetUrl(true);
            setDefaultTargetUrl(securityProperties.getLogin().getLoginSuccessUrl());
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }

}複製代碼

和 CustomAuthenticationFailureHandler( extends SimpleUrlAuthenticationFailureHandler) 重寫 onAuthenticationFailure() 方法 :

@Component("customAuthenticationFailureHandler")
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {

        logger.info("登陸失敗");
        if (StringUtils.isEmpty(securityProperties.getLogin().getLoginErrorUrl())){

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));

        } else {
            // 跳轉設置的登錄失敗頁面
            redirectStrategy.sendRedirect(request,response,securityProperties.getLogin().getLoginErrorUrl());
        }

    }
}複製代碼

(三) 自定義的登錄頁面

這裏就再也不描述,直接貼代碼:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
</head>
<body>
<h2>登陸頁面</h2>
<form action="/loginUp" method="post">  
    <table>
        <tr>
            <td>用戶名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan='2'><input name="remember-me" type="checkbox" value="true"/>記住我</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登陸</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>複製代碼

  注意這裏請求的地址是 loginProcessingUrl() 配置的地址

(四)測試驗證

  這裏就不在貼結果圖了,只要咱們明白結果流程就行是這樣的就能夠:localhost:8080 ——> 點擊 測試驗證Security 權限控制 ————> 跳轉到 咱們自定義的 /loginUp.html 登陸頁,登陸後 ————> 有配置loginSuccessUrl,則跳轉到 loginSuccess.html;反之則直接跳轉到 /get_user/test 接口返回結果。 整個流程就全面涉及到了咱們自定義的登陸頁面、自定義的登陸成功/失敗處理器。

2、 RememberMe (記住我)功能解析

(一)RememberMe 功能實現配置

首先咱們一股腦的將rememberMe配置加上,而後看下現象:複製代碼

一、 建立 persistent_logins 表,用於存儲token和用戶的關聯信息:

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);複製代碼

2 、 添加rememberMe配置 信息

@Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 若是token表不存在,使用下面語句能夠初始化 persistent_logins(ddl在db目錄下) 表;若存在,請註釋掉這條語句,不然會報錯。
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
    
     @Override
    protected void configure(HttpSecurity http) throws Exception {

        formAuthenticationConfig.configure(http);
        http.   ....
                .and()
                // 開啓 記住我功能,意味着 RememberMeAuthenticationFilter 將會 從Cookie 中獲取token信息
                .rememberMe()
                // 設置 tokenRepository ,這裏默認使用 jdbcTokenRepositoryImpl,意味着咱們將從數據庫中讀取token所表明的用戶信息
                .tokenRepository(persistentTokenRepository())
                // 設置  userDetailsService , 和 認證過程的同樣,RememberMe 有專門的 RememberMeAuthenticationProvider ,也就意味着須要 使用UserDetailsService 加載 UserDetails 信息
                .userDetailsService(userDetailsService)
                // 設置 rememberMe 的有效時間,這裏經過 配置來設置
                .tokenValiditySeconds(securityProperties.getLogin().getRememberMeSeconds())
                .and()
                .csrf().disable(); // 關閉csrf 跨站(域)攻擊防控
    }複製代碼

這裏解釋下配置:

  • rememberMe() 開啓 記住我功能,意味着 RememberMeAuthenticationFilter 將會 從Cookie 中獲取token信息
  • tokenRepository() 配置 token的獲取策略,這裏配置成從數據庫中讀取
  • userDetailsService() 配置 UserDetaisService (若是不熟悉該對象,建議回顧認證過程)
  • tokenValiditySeconds() 設置 rememberMe 的有效時間,這裏經過 配置來設置

另外一個重要的配置在登陸頁面,這裏的 必須是 name="remember-me" ,rememberMe就是經過驗證這個配置來開啓remermberMe功能的。

<input name="remember-me" type="checkbox" value="true"/>記住我</td>複製代碼

  實操結果應該爲:進入登錄頁面 ——> 勾選記住我後登陸 ——> 成功後,查看persistentlogins 表發現有一條數據——> 重啓項目 ——> 從新訪問須要登陸才能訪問的頁面,發現無需登陸便可訪問——> 刪除 persistentlogins 表數據,等待token設置的有效時間過時,而後從新刷新頁面發現跳轉到登錄頁面。

(二) RembemberMe 實現源碼解析

   首先咱們查看UsernamePasswordAuthenticationFiler(AbstractAuthenticationProcessingFilter) 的 successfulAuthentication() 方法內部源碼:

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        
        // 1 設置 認證成功的Authentication對象到SecurityContext中
        SecurityContextHolder.getContext().setAuthentication(authResult);
        
        // 2 調用 RememberMe 相關service處理
        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }
        //3 調用成功處理器
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }複製代碼

其中咱們發現咱們本次重點關注的一行代碼: rememberMeServices.loginSuccess(request, response, authResult) , 查看這個方法內部源碼:

@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);
    }複製代碼

經過 rememberMeRequested() 判斷是否勾選了記住我。onLoginSuccess() 方法 最終會調用到 PersistentTokenBasedRememberMeServices 的 onLoginSuccess() 方法,貼出其方法源碼以下:

protected void onLoginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        // 1 獲取帳戶名
        String username = successfulAuthentication.getName();
        
        // 2 建立  PersistentRememberMeToken 對象
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                username, generateSeriesData(), generateTokenData(), new Date());
        try {
            // 3 經過 tokenRepository 存儲 persistentRememberMeToken 信息
            tokenRepository.createNewToken(persistentToken);
            // 4 將 persistentRememberMeToken 信息添加到Cookie中
            addCookie(persistentToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to save persistent token ", e);
        }
    }複製代碼

分析下源碼步驟:

  • 獲取 帳戶信息 username
  • 傳入 username 建立 PersistentRememberMeToken 對象
  • 經過 tokenRepository 存儲 persistentRememberMeToken信息
  • 將 persistentRememberMeToken 信息添加到Cookie中

  這裏的 tokenRepository 就是咱們配置 rememberMe功能所設置的。通過上面的解析咱們看到了rememberServices 將 建立一個 token 信息,並存儲到數據庫(由於咱們配置的是數據庫存儲方式 JdbcTokenRepositoryImpl )中,並將token信息添加到Cookie中了。到這裏,咱們看到了RememberMe實現前的一些業務處理,那麼後面如何實現RememberMe,我想你們內心大概都有個底了。這裏直接拋出以前受權過程當中咱們沒有說起到的 filter 類 RememberMeAuthenticationFilter,它是介於 UsernamePasswordAuthenticationFilter 和 AnonymousAuthenticationFilter 之間的一個filter,它主要負責的就是前面的filter都沒有認證成功後從Cookie中獲取token信息而後再經過tokenRepository 獲取 登陸用戶名,而後UserDetailsServcie 加載 UserDetails 信息 ,最後建立 Authticaton(RememberMeAuthenticationToken) 信息再調用 AuthenticationManager.authenticate() 進行認證過程。

RememberMeAuthenticationFilter

  咱們來看下 RememberMeAuthenticationFilter 的dofiler方法源碼:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            //  1 調用 rememberMeServices.autoLogin() 獲取Authtication 信息
            Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
                    response);

            if (rememberMeAuth != null) {
                // Attempt authenticaton via AuthenticationManager
                try {
                    // 2 調用 authenticationManager.authenticate() 認證
                    rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
                    
                    ......
                    }

                }
                catch (AuthenticationException authenticationException) {
                .....
            }

            chain.doFilter(request, response);
        }複製代碼

咱們主要關注 rememberMeServices.autoLogin(request,response) 方法實現,查看器源碼:

@Override
    public final Authentication autoLogin(HttpServletRequest request,
            HttpServletResponse response) {
        // 1 從Cookie 中獲取 token 信息
        String rememberMeCookie = extractRememberMeCookie(request);

        if (rememberMeCookie == null) {
            return null;
        }
        
        if (rememberMeCookie.length() == 0) {
            cancelCookie(request, response);
            return null;
        }

        UserDetails user = null;

        try {
            // 2 解析 token信息
            String[] cookieTokens = decodeCookie(rememberMeCookie);
            // 3 經過 token 信息 生成 Uerdetails 信息
            user = processAutoLoginCookie(cookieTokens, request, response);
            userDetailsChecker.check(user);

            logger.debug("Remember-me cookie accepted");
            // 4 經過 UserDetails 信息建立 Authentication 
            return createSuccessfulAuthentication(request, user);
        } 
        .....
    }複製代碼

內部實現步驟:

  • 從Cookie中獲取 token 信息並解析
  • 經過 解析的token 生成 UserDetails (processAutoLoginCookie() 方法實現 )
  • 經過 UserDetails 生成 Authentication ( createSuccessfulAuthentication() 建立 RememberMeAuthenticationToken )

其中最關鍵的一部是 processAutoLoginCookie() 方法是如何生成UserDetails 對象的,咱們查看這個方法源碼實現:

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
            HttpServletRequest request, HttpServletResponse response) {
        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];
        // 1 經過 tokenRepository 加載數據庫token信息
        PersistentRememberMeToken token = tokenRepository
                .getTokenForSeries(presentedSeries);

        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                token.getUsername(), token.getSeries(), generateTokenData(), new Date());
        // 2 判斷 用戶傳入token和數據中的token是否一致,不一致可能存在安全問題
        if (!presentedToken.equals(token.getTokenValue())) {
            tokenRepository.removeUserTokens(token.getUsername());
            throw new CookieTheftException(
                    messages.getMessage(
                            "PersistentTokenBasedRememberMeServices.cookieStolen",
                            "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
        }
        try {
            // 3 更新 token 並添加到Cookie中
            tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                    newToken.getDate());
            addCookie(newToken, request, response);
        }
        catch (Exception e) {
            throw new RememberMeAuthenticationException(
                    "Autologin failed due to data access problem");
        }
        // 4 經過 UserDetailsService().loadUserByUsername() 方法加載UserDetails 信息並返回
        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }複製代碼

咱們看下其內部步驟:

  • 經過 tokenRepository 加載數據庫token信息
  • 判斷 用戶傳入token和數據中的token是否一致,不一致可能存在安全問題
  • 更新 token 並添加到Cookie中
  • 經過 UserDetailsService().loadUserByUsername() 方法加載UserDetails 信息並返回

   看到這裏相信你們如下就明白了,當初爲啥在啓用rememberMe功能時要配置 tokenRepository 和 UserDetailsService了。

這裏我就再也不演示整個實現的流程了,老規矩,上流程圖:

https://user-gold-cdn.xitu.io/2019/8/28/16cd892ee6146b4c?w=1283&h=504&f=jpeg&s=96642

   本文介紹個性化認證和RememberMe的代碼能夠訪問代碼倉庫中的 security 模塊 ,項目的github 地址 : https://github.com/BUG9/spring-security

         若是您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!

複製代碼
相關文章
相關標籤/搜索