spring security採用基於簡單加密 token 的方法實現的remember me功能

記住我功能,相信你們在一些網站已經用過,一些安全要求不高的均可以使用這個功能,方便快捷。
spring security針對該功能有兩種實現方式, 一種是簡單的使用加密來保證基於 cookie 的 token 的安全,另外一種是經過數據庫或其它持久化存儲機制來保存生成的 token。
下面是基於簡單加密 token 的方法的實現,基於前篇的限制登陸次數的功能之上加入remember me功能
項目結構以下:
基本的結構沒有變化,主要在於一些類的修改和配置。
 1、修改SecurityConfig配置文件
package com.petter.config;
import com.petter.handler.CustomAuthenticationProvider;
import com.petter.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import javax.annotation.Resource;
/**
 * 至關於spring-security.xml中的配置
 * @author hongxf
 * @since 2017-03-08 9:30
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private CustomAuthenticationProvider authenticationProvider;
    @Resource
    private CustomUserDetailsService userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }
    /**
     * 配置權限要求
     * 採用註解方式,默認開啓csrf
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/dba/**").hasAnyRole("ADMIN", "DBA")
            .and()
                .formLogin().successHandler(savedRequestAwareAuthenticationSuccessHandler())
                .loginPage("/login") //指定自定義登陸頁
                .failureUrl("/login?error") //登陸失敗的跳轉路徑
                .loginProcessingUrl("/auth/login_check") //指定了登陸的form表單提交的路徑,需與表單的action值保存一致,默認是login
                .usernameParameter("user-name").passwordParameter("pwd")
            .and()
                .logout().logoutSuccessUrl("/login?logout")
            .and()
                .exceptionHandling().accessDeniedPage("/403")
            .and()
                .csrf()
            .and()
                .rememberMe().rememberMeParameter("remember-me") //其實默認就是remember-me,這裏能夠指定更換
                .tokenValiditySeconds(1209600)
                .key("hongxf");
    }
    //使用remember-me必須指定UserDetailsService
    @Override
    protected UserDetailsService userDetailsService() {
        return userDetailsService;
    }
    /**
     * 這裏是登陸成功之後的處理邏輯
     * 設置目標地址參數爲targetUrl
     * /auth/login_check?targetUrl=/admin/update
     * 這個地址就會被解析跳轉到/admin/update,不然就是默認頁面
     *
     * 本示例中訪問update頁面時候會判斷用戶是手動登陸仍是remember-me登陸的
     * 若是是remember-me登陸的則會跳轉到登陸頁面進行手動登陸再跳轉
     * @return
     */
    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler() {
        SavedRequestAwareAuthenticationSuccessHandler auth = new SavedRequestAwareAuthenticationSuccessHandler();
        auth.setTargetUrlParameter("targetUrl");
        return auth;
    }
}

這裏須要指出幾點:html

一、使用remember-me功能必須指定UserDetailsService
二、修改登陸成功之後的邏輯,具體見註釋
三、添加remember me的配置,key("hongxf"),這裏的key用於加密,能夠進行指定
2、修改admin.html,添加
<div sec:authorize="isRememberMe()">
        <h2>該用戶是經過remember me cookies登陸的</h2>
    </div>
    <div sec:authorize="isFullyAuthenticated()">
        <h2>該用戶是經過輸入用戶名和密碼登陸的</h2>
    </div>

用於展現java

3、修改登陸頁面login.html
form表單須要進行相應的修改
<form name='loginForm' th:action="@{/auth/login_check(targetUrl=${session.targetUrl})}" method='POST'>
            <table>
                <tr>
                    <td>用戶名:</td>
                    <td><input type='text' name='user-name' /></td>
                </tr>
                <tr>
                    <td>密碼:</td>
                    <td><input type='password' name='pwd' /></td>
                </tr>
                <!-- 若是是進行更新操做跳轉過來的頁面則不顯示記住我 -->
                <div th:if="${loginUpdate} eq null">
                    <tr>
                        <td></td>
                        <td>記住我: <input type="checkbox" name="remember-me" /></td>
                    </tr>
                </div>
                <tr>
                    <td colspan='2'>
                        <input type="submit" value="提交" />
                    </td>
                </tr>
            </table>
        </form>

注意action的值,首先請求路徑是/auth/login_check,與SecurityConfig配置的loginProcessingUrl保持一致web

/auth/login_check(targetUrl=${session.targetUrl})會被解析成 /auth/login_check? targetUrl=XXX 其中 targetUrl的從session中獲取
4、編寫update.html頁面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>update</title>
</head>
<body>
    <h1>Title : 更新頁面</h1>
    <h1>只有經過用戶名和密碼登陸的用戶才容許進入這個頁面,remember me登陸的用戶不容許,防止被盜用cookie</h1>
    <h2>更新帳號信息</h2>
</body>
</html>

5、修改HelloController類spring

package com.petter.web;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
 * @author hongxf
 * @since 2017-03-08 9:29
 */
@Controller
public class HelloController {
    @RequestMapping(value = { "/", "/welcome" }, method = RequestMethod.GET)
    public ModelAndView welcomePage() {
        ModelAndView model = new ModelAndView();
        model.addObject("title", "Spring Security Hello World");
        model.addObject("message", "This is welcome page!");
        model.setViewName("hello");
        return model;
    }
    @RequestMapping(value = "/admin", method = RequestMethod.GET)
    public ModelAndView adminPage() {
        ModelAndView model = new ModelAndView();
        model.addObject("title", "Spring Security Hello World");
        model.addObject("message", "This is protected page - Admin Page!");
        model.setViewName("admin");
        return model;
    }
    @RequestMapping(value = "/dba", method = RequestMethod.GET)
    public ModelAndView dbaPage() {
        ModelAndView model = new ModelAndView();
        model.addObject("title", "Spring Security Hello World");
        model.addObject("message", "This is protected page - Database Page!");
        model.setViewName("admin");
        return model;
    }
    /**
     * 登陸頁面只容許使用密碼登陸
     * 若是用戶經過remember me的cookie登陸則跳轉到登陸頁面輸入密碼
     * 爲了不盜用remember me cookie 來更新信息
     */
    @RequestMapping(value = "/admin/update", method = RequestMethod.GET)
    public ModelAndView updatePage(HttpServletRequest request) {
        ModelAndView model = new ModelAndView();
        if (isRememberMeAuthenticated()) {
            //把targetUrl放入session中,登陸頁面使用${session.targetUrl}獲取
            setRememberMeTargetUrlToSession(request);
            //跳轉到登陸頁面
            model.addObject("loginUpdate", true);
            model.setViewName("login");
        } else {
            model.setViewName("update");
        }
        return model;
    }
    /**
     * 判斷用戶是否是經過remember me方式登陸,參考
     * org.springframework.security.authentication.AuthenticationTrustResolverImpl
     */
    private boolean isRememberMeAuthenticated() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication != null && RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass());
    }
    /**
     * 保存請求的頁面targetUrl到session中
     */
    private void setRememberMeTargetUrlToSession(HttpServletRequest request){
        HttpSession session = request.getSession(false);
        if(session != null){
            session.setAttribute("targetUrl", request.getRequestURI());
        }
    }
    //獲取session存儲的SPRING_SECURITY_LAST_EXCEPTION的值,自定義錯誤信息
    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView login(
            @RequestParam(value = "error", required = false) String error,
            @RequestParam(value = "logout", required = false) String logout,
            HttpServletRequest request) {
        ModelAndView model = new ModelAndView();
        if (error != null) {
            model.addObject("error", getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION"));
            //在update的登陸頁面上,判斷targetUrl是否有值,沒有則顯示記住我,有則不顯示
            String targetUrl = getRememberMeTargetUrlFromSession(request);
            System.out.println(targetUrl);
            if(StringUtils.hasText(targetUrl)){
                model.addObject("loginUpdate", true);
            }
        }
        if (logout != null) {
            model.addObject("msg", "你已經成功退出");
        }
        model.setViewName("login");
        return model;
    }
    /**
     * 從session中獲取targetUrl
     */
    private String getRememberMeTargetUrlFromSession(HttpServletRequest request){
        String targetUrl = "";
        HttpSession session = request.getSession(false);
        if(session != null){
            targetUrl = session.getAttribute("targetUrl") == null ? "" :session.getAttribute("targetUrl").toString();
        }
        return targetUrl;
    }
    //自定義錯誤類型
    private String getErrorMessage(HttpServletRequest request, String key){
        Exception exception = (Exception) request.getSession().getAttribute(key);
        String error;
        if (exception instanceof BadCredentialsException) {
            error = "不正確的用戶名或密碼";
        }else if(exception instanceof LockedException) {
            error = exception.getMessage();
        }else{
            error = "不正確的用戶名或密碼";
        }
        return error;
    }
    @RequestMapping(value = "/403", method = RequestMethod.GET)
    public ModelAndView accessDenied() {
        ModelAndView model = new ModelAndView();
        //檢查用戶是否已經登陸
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (!(auth instanceof AnonymousAuthenticationToken)) {
            UserDetails userDetail = (UserDetails) auth.getPrincipal();
            model.addObject("username", userDetail.getUsername());
        }
        model.setViewName("403");
        return model;
    }
}

6、進行測試數據庫

啓動應用,訪問 http://localhost:8080/admin 會跳轉到登陸頁
 輸入正確的用戶名和密碼,勾選記住我,登陸成功進入admin頁面
 

 
能夠查看此時的cookie中的值
 

 能夠看到remember-me的值以及失效日期
想要嘗試記住我免登陸功能,重啓應用,訪問 http://localhost:8080/admin ,能夠看到
 

 如今嘗試訪問 http://localhost:8080/admin/update 則會跳轉到登陸頁面
 
 注意上面的訪問地址就是 http://localhost:8080/admin/update 只是返回的是登陸頁內容,而且隱藏了記住我選項,查看頁面源代碼,能夠看到
 
<form name='loginForm' action="/auth/login_check?targetUrl=/admin/update" method='POST'>
當輸入爭取的用戶名和密碼時候security會根據targetUrl的值跳轉到以前訪問的頁面
相關文章
相關標籤/搜索