zoukankan      html  css  js  c++  java
  • Spring Security之Remember me详解

    Remember me功能就是勾选"记住我"后,一次登录,后面在有效期内免登录。

    先看具体配置:

    pom文件:

    <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

     Security的配置:

        @Autowired
        private UserDetailsService myUserDetailServiceImpl; // 用户信息服务
    
        @Autowired
        private DataSource dataSource; // 数据源
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // formLogin()是默认的登录表单页,如果不配置 loginPage(url),则使用 spring security
            // 默认的登录页,如果配置了 loginPage()则使用自定义的登录页
            http.formLogin() // 表单登录
                    .loginPage(SecurityConst.AUTH_REQUIRE)
                    .loginProcessingUrl(SecurityConst.AUTH_FORM) // 登录请求拦截的url,也就是form表单提交时指定的action
                    .successHandler(loginSuccessHandler)
                    .failureHandler(loginFailureHandler)
                    .and()
                .rememberMe()
                    .userDetailsService(myUserDetailServiceImpl) // 设置userDetailsService
                    .tokenRepository(persistentTokenRepository()) // 设置数据访问层
                    .tokenValiditySeconds(60 * 60) // 记住我的时间(秒)
                    .and()
                .authorizeRequests() // 对请求授权
                    .antMatchers(SecurityConst.AUTH_REQUIRE, securityProperty.getBrowser().getLoginPage()).permitAll() // 允许所有人访问login.html和自定义的登录页
                    .anyRequest() // 任何请求
                    .authenticated()// 需要身份认证
                    .and()
                .csrf().disable() // 关闭跨站伪造
            ;
        }
    
        /**
         * 持久化token
         * 
         * Security中,默认是使用PersistentTokenRepository的子类InMemoryTokenRepositoryImpl,将token放在内存中
         * 如果使用JdbcTokenRepositoryImpl,会创建表persistent_logins,将token持久化到数据库
         */
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource); // 设置数据源
    //        tokenRepository.setCreateTableOnStartup(true); // 启动创建表,创建成功后注释掉
            return tokenRepository;
        }

    上面的myUserDetailServiceImpl是自己实现的UserDetailsService接口,dataSource会自动读取数据库配置。过期时间设置的3600秒,即一个小时

    在登录页面加一行(name必须是remeber-me):

    "记住我"基本原理:

    1、第一次发送认证请求,会被UsernamePasswordAuthenticationFilter拦截,然后身份认证。认证成功后,在AbstracAuthenticationProcessingFilter中,有个RememberMeServices接口。该接口默认实现类是NullRememberMeServices,这里会调用另一个实现抽象类AbstractRememberMeServices

        // ...
    
        private RememberMeServices rememberMeServices = new NullRememberMeServices();
    
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                Authentication authResult) throws IOException, ServletException {
    
            // ...
    
            SecurityContextHolder.getContext().setAuthentication(authResult);
    
            // 登录成功后,调用RememberMeServices保存Token相关信息
            rememberMeServices.loginSuccess(request, response, authResult);
    
            // ...
        }

    2、调用AbstractRememberMeServices的loginSuccess方法。可以看到如果request中name为"remember-me"为true时,才会调用下面的onLoginSuccess()方法。这也是为什么上面登录页中的表单,name必须是"remember-me"的原因:

    3、在Security中配置了rememberMe()之后, 会由PersistentTokenBasedRememberMeServices去实现父类AbstractRememberMeServices中的抽象方法。PersistentTokenBasedRememberMeServices中,有一个PersistentTokenRepository,会生成一个Token,并将这个Token写到cookie里面返回浏览器。PersistentTokenRepository的默认实现类是InMemoryTokenRepositoryImpl,该默认实现类会将token保存到内存中。这里我们配置了它的另一个实现类JdbcTokenRepositoryImpl,该类会将Token持久化到数据库中

        // ...
    
        private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
    
        protected void onLoginSuccess(HttpServletRequest request,
                HttpServletResponse response, Authentication successfulAuthentication) {
            String username = successfulAuthentication.getName();
    
            logger.debug("Creating new persistent login for user " + username);
    
            // 创建一个PersistentRememberMeToken
            PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                    username, generateSeriesData(), generateTokenData(), new Date());
            try {
                // 保存Token
                tokenRepository.createNewToken(persistentToken);
                // 将Token写到Cookie中
                addCookie(persistentToken, request, response);
            }
            catch (Exception e) {
                logger.error("Failed to save persistent token ", e);
            }
        }

    4、JdbcTokenRepositoryImpl将Token持久化到数据库

       /** The default SQL used by <tt>createNewToken</tt> */
        public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
    
        public void createNewToken(PersistentRememberMeToken token) {
            getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(),
                    token.getTokenValue(), token.getDate());
        }

    查看数据库,可以看到往persistent_logins 中插入了一条数据:

    5、重启服务,发送第二次认证请求,只会携带Cookie。所以直接会被RememberMeAuthenticationFilter拦截,并且此时内存中没有认证信息。可以看到,此时的RememberMeServices是由PersistentTokenBasedRememberMeServices实现

    6、在PersistentTokenBasedRememberMeServices中,调用processAutoLoginCookie方法,获取用户相关信息

    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) + "'");
            }
    
            // 从Cookie中获取Series和Token
            final String presentedSeries = cookieTokens[0];
            final String presentedToken = cookieTokens[1]; 
    
            //在数据库中,通过Series查询PersistentRememberMeToken
            PersistentRememberMeToken token = tokenRepository
                    .getTokenForSeries(presentedSeries);
    
            if (token == null) {
                throw new RememberMeAuthenticationException(
                        "No persistent token found for series id: " + presentedSeries);
            }
    
            // 校验数据库中Token和Cookie中的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."));
            }
    
            // 判断Token是否超时
            if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
                    .currentTimeMillis()) {
                throw new RememberMeAuthenticationException("Remember-me login has expired");
            }
    
            if (logger.isDebugEnabled()) {
                logger.debug("Refreshing persistent login token for user '"
                        + token.getUsername() + "', series '" + token.getSeries() + "'");
            }
            
            // 创建一个新的PersistentRememberMeToken
            PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                    token.getUsername(), token.getSeries(), generateTokenData(), new Date());
    
            try {
                //更新数据库中Token
                tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                        newToken.getDate());
                //重新写到Cookie
                addCookie(newToken, request, response);
            }
            catch (Exception e) {
                logger.error("Failed to update token: ", e);
                throw new RememberMeAuthenticationException(
                        "Autologin failed due to data access problem");
            }
            //调用UserDetailsService获取用户信息
            return getUserDetailsService().loadUserByUsername(token.getUsername());
        }

    7、获取用户相关信息后,再调用AuthenticationManager去认证授权,授权细节可参考:AuthenticationManager、ProviderManager

     

  • 相关阅读:
    ubuntu下libjson-c库的使用问题备忘
    SAX PULL解析实例
    C# 自己定义 implicit和explicit转换
    游戏行业创业分析
    【kotlin】long转化为date类型 或者date字符串
    【kotlin】报错 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type List<String>?
    【spring boot】注解@ApiParam @PathVariable @RequestParam三者区别
    【Mybatis】 Mybatis在xml文件中处理大于号小于号的方法【问题】
    【hql】spring data jpa中 @Query使用hql查询 问题
    【IntelliJ IDEA】2017.3.4版本永久破解
  • 原文地址:https://www.cnblogs.com/xuwenjin/p/9933218.html
Copyright © 2011-2022 走看看