zoukankan      html  css  js  c++  java
  • spring security采用基于简单加密 token 的方法实现的remember me功能

    记住我功能,相信大家在一些网站已经用过,一些安全要求不高的都可以使用这个功能,方便快捷。
    spring security针对该功能有两种实现方式,一种是简单的使用加密来保证基于 cookie 的 token 的安全,另一种是通过数据库或其它持久化存储机制来保存生成的 token。
    下面是基于简单加密 token 的方法的实现,基于前篇的限制登录次数的功能之上加入remember me功能
    项目结构如下:
    基本的结构没有变化,主要在于一些类的修改和配置。
     一、修改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;
        }
    }

    这里需要指出几点:

    1、使用remember-me功能必须指定UserDetailsService
    2、修改登录成功以后的逻辑,具体见注释
    3、添加remember me的配置,key("hongxf"),这里的key用于加密,可以进行指定
    二、修改admin.html,添加
    <div sec:authorize="isRememberMe()">
            <h2>该用户是通过remember me cookies登录的</h2>
        </div>
        <div sec:authorize="isFullyAuthenticated()">
            <h2>该用户是通过输入用户名和密码登录的</h2>
        </div>

    用于展示

    三、修改登录页面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保持一致

    /auth/login_check(targetUrl=${session.targetUrl})会被解析成/auth/login_check?targetUrl=XXX 其中targetUrl的从session中获取
    四、编写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>

    五、修改HelloController类

    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;
        }
    }

    六、进行测试

    启动应用,访问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的值跳转到之前访问的页面
     
     
     
     
  • 相关阅读:
    Identifier expected after this token
    需要整理的
    Context
    SharedPreferences
    一些常规注意事项
    一个点亮屏幕的service
    BroadcastReceiver中调用Service
    BroadcastReceiver
    Service
    微服务简介
  • 原文地址:https://www.cnblogs.com/hongxf1990/p/6605683.html
Copyright © 2011-2022 走看看