springsecurity4+springboot 实现remember-me 发现springsecurity 的BUG

  前言:现在开发中,记住我这个功能是普遍的,用户不可能每次登录都要输入用户名密码。昨天准备用spring security的记住我功能,各种坑啊,吐血 。

  先看下具体实现吧。

spring security 对remember-me 进行了封装 ,大概流程是 首先用户的表单必须有这个记住我的字段。

1.安全配置

以下是代码 (红色字体注释是关键)

 

package com.ycmedia.security;

import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.ycmedia.constants.MySQLConfig;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter  {

    @Autowired
    @Qualifier("customUserDetailsService")
    UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationProvider authenticationProvider;
    //注入数据源
    @Autowired
    @Qualifier("mysqlDS")
    private DataSource dataSource;
    
     @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
     @Override
        protected void configure(HttpSecurity http) throws Exception {
            //允许所有用户访问”/”和”/home”
            http.authorizeRequests().
            antMatchers( "/css/**", "/js/**", "/images/**",
            "/lib/**", "/skin/**"
            , "/bootstrap/**"
            , "/build/**"
            , "/documentation/**"
            , "/pages/**"
            )
            .permitAll()
            //其他地址的访问均需验证权限
            .anyRequest().authenticated()
            .and()
            .formLogin()
            //指定登录页是”/login”
            .loginPage("/login")        
            .permitAll()
            //登录成功后可使用loginSuccessHandler()存储用户信息,可选。
            .successHandler(loginSuccessHandler())//code3
            .and()
            .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
    //退出登录后的默认网址是”/home”
            .logoutSuccessUrl("/home")
            .permitAll()
            .invalidateHttpSession(true)
            .and()
            //登录后记住用户,下次自动登录
            //数据库中必须存在名为persistent_logins的表
            //建表语句见code15

// 这里是核心 .rememberMe() .tokenValiditySeconds(1209600) //指定记住登录信息所使用的数据源 .tokenRepository(tokenRepository());//code4 } // @Override // protected void configure(HttpSecurity http) throws Exception { // //允许访问静态资源 // http.authorizeRequests() // .antMatchers( "/css/**", "/js/**", "/images/**", // "/resources/**", "/lib/**", "/skin/**", "/template/**" // , "/bootstrap/**" // , "/build/**" // , "/documentation/**" // , "/pages/**" // , "/plugins/**" // , "/skin/**") // .permitAll() // //登录和注册页面不需要权限验证 // .antMatchers("/login", "/registration","/registrationUser").permitAll() // //其他地址的访问均需验证权限 // .anyRequest().authenticated(). // //访问失败页url // and().formLogin() // //.failureUrl("/login?error"). // //默认访问页 // .loginPage("/login") // .permitAll().successHandler(loginSuccessHandler()). // and().logout() // .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // //退出登录后的默认网址是”/home” // .logoutSuccessUrl("/home"). // // 注销会删除cookie // deleteCookies("remember-me") // .invalidateHttpSession(true) // //注销失败跳转到登录页面 // .permitAll().and() // .rememberMe() // .tokenValiditySeconds(1209600) // //指定记住登录信息所使用的数据源 // .tokenRepository(tokenRepository());//code4 // // // // //设置session //// http.sessionManagement().maximumSessions(1); //// http.sessionManagement().invalidSessionUrl("/login"); // } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/resources/**"); web.ignoring().antMatchers("/webjars/**"); } // @Override // protected void configure(AuthenticationManagerBuilder auth) // throws Exception { // //采用自定义验证 // auth.authenticationProvider(authenticationProvider); // // //需要采用加密 //// auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); // } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(4); }
//spring security 内部都写死了,这里要把 这个DAO 注入 @Bean
public JdbcTokenRepositoryImpl tokenRepository(){ JdbcTokenRepositoryImpl j=new JdbcTokenRepositoryImpl(); j.setDataSource(dataSource); return j; } /** * 用户或者管理员登录日志 */ @Bean public LoginSuccessHandler loginSuccessHandler(){ return new LoginSuccessHandler(); } @Autowired public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider); auth.userDetailsService(userDetailsService); } }

 

 2 前端页面, 这个简单

   <div class="checkbox">
          <label><input type="checkbox" id="rememberme" name="remember-me"/> Remember Me</label>  
     </div>

 3 、数据库表,因为 spring security 内部把表写死了, 可以看源码

 * @author Luke Taylor
 * @since 2.0
 */
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
        PersistentTokenRepository {
    // ~ Static fields/initializers
    // =====================================================================================

    /** 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)";
    /** The default SQL used by the <tt>getTokenBySeries</tt> query */
    public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
    /** 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(?,?,?,?)";
    /** The default SQL used by <tt>updateToken</tt> */
    public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
    /** The default SQL used by <tt>removeUserTokens</tt> */
    public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";

这个表名必须 是persistent_logins

CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

==========================================================================================其实到这里基本是没问题的,如果用 springsecurity 默认 授权验证

springsecurity 默认都是用户名+密码登录, 但是现在系统很多都是用户手机号+手机验证码登录,我这里就是这么实现的,手机验证码必须从第三方短信获取, 所以必须自定义授权验证



先看下我的自定义验证类
package com.ycmedia.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;
import com.ycmedia.constants.Constants;
import com.ycmedia.entity.Customer;
import com.ycmedia.entity.Role;
import com.ycmedia.service.UserService;
import com.ycmedia.utils.HttpRequest;

/**
 * @author 自定义验证
 *
 */
@Component
public class YcAnthencationProder implements AuthenticationProvider {
    @Autowired
    private UserService userService;
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    @Autowired
    private Environment env;

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
//        CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication
//                .getDetails(); // 如上面的介绍,这里通过authentication.getDetails()获取详细信息
        // 用户名
        String username = authentication.getName();
        // 验证码
        String password = (String) authentication.getCredentials();
        Customer user = userService.getUserByname(username);
        List<SimpleGrantedAuthority> auths = new ArrayList<>();
        
        System.out.println("用户"+authentication.getName()+"正在获取权限");
        //游客=》提示用户去注册
        if(user==null){
            //授权
            auths.add(new SimpleGrantedAuthority(Role.ROLE_TOURIST.toString()));
            auths.add(new SimpleGrantedAuthority(username));
            auths.add(new SimpleGrantedAuthority(password));
            return new UsernamePasswordAuthenticationToken(new Customer(), password,
                    auths);
        }else{
            //存在此用户,调用登录接口
            String data = HttpRequest.sendGet(env.getProperty("login.url"),
                    "mobile=" + username+"&smsCode="+password);
            JSONObject json = JSONObject.parseObject(data);
            if(json.getBoolean("success")==true)){
                //验证码和手机号码正确,返回用户权限
                switch(user.getRole()){
                  case 0:auths.add(new SimpleGrantedAuthority(Role.ROLE_USER.toString()));
                  case 1:auths.add(new SimpleGrantedAuthority(Role.ROLE_CHANNEL.toString()));
                  case 2:auths.add(new SimpleGrantedAuthority(Role.ROLE_ADMIN.toString()));
                }
            }else{
                //验证消息放到权限里面, 页面提示
                auths.add(new SimpleGrantedAuthority(Role.ROLE_WRONGCODE.toString()));
                auths.add(new SimpleGrantedAuthority(username));
                auths.add(new SimpleGrantedAuthority(password));
            }
        }
        
        return new RememberMeAuthenticationToken(username, user, auths);
//        return new UsernamePasswordAuthenticationToken(user, password,
//                auths);

    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}
实现 这个接口就行AuthenticationProvider
但是,结果
        return new UsernamePasswordAuthenticationToken(user, password,
                auths);
返回一个 UsernamePasswordAuthenticationToken 对象, 这个对象,是继承了 AbstractAuthenticationToken


而AbstractAuthenticationToken 又是 Authentication 的子类, 

这个UsernamePasswordAuthenticationToken  看下他的源码


/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.authentication;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

/**
 * An {@link org.springframework.security.core.Authentication} implementation that is
 * designed for simple presentation of a username and password.
 * <p>
 * The <code>principal</code> and <code>credentials</code> should be set with an
 * <code>Object</code> that provides the respective property via its
 * <code>Object.toString()</code> method. The simplest such <code>Object</code> to use is
 * <code>String</code>.
 *
 * @author Ben Alex
 */
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // ~ Instance fields
    // ================================================================================================

    private final Object principal;
    private Object credentials;

    // ~ Constructors
    // ===================================================================================================

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     *
     */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     *
     * @param principal
     * @param credentials
     * @param authorities
     */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

    // ~ Methods
    // ========================================================================================================

    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        credentials = null;
    }
}

发现上springsecurity 的问题主要是

   private final Object principal; 

principal 是个 object 类型的, 可以存的信息有两种,第一这个用户对象, 第二, 用户名。 spring security 默认存的是 用户对象。
    @RequestMapping(value = "/order-list")
    public ModelAndView addSystemUser(Model model) {
        
        Customer user= (Customer) AuthUtils.getAuthenticationObject().getPrincipal();
        model.addAttribute("role", user.getRole());
        model.addAttribute("username", user.getMobile());
        return new ModelAndView("order-list");
    }
这是我的一个 controller 方法,  Customer user= (Customer) AuthUtils.getAuthenticationObject().getPrincipal(); 返回客户所有, 包括角色 ,权限,然后扔给前端,,这样看起来没错 啊。

最最坑的 的地方来了

这里是重点。
先看下这个类 PersistentTokenBasedRememberMeServices
看名字, 持久化 token 记住我 service;
再看他的类前部分
//继承了 AbstractRememberMeServices 
public
class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices { // 还记得之前安全配置注入的DAo吗, 在这里 private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl(); private SecureRandom random; public static final int DEFAULT_SERIES_LENGTH = 16; public static final int DEFAULT_TOKEN_LENGTH = 16; private int seriesLength = DEFAULT_SERIES_LENGTH; private int tokenLength = DEFAULT_TOKEN_LENGTH; public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) { super(key, userDetailsService); random = new SecureRandom(); this.tokenRepository = tokenRepository; }

 


=====================================这里看起来正常
再看看核心方法
// 当一个用户登录成功, 并使用记住我会调用这个份方法    

protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
//这里就是spring security 有BUG的地方, 因为 Authentication successfulAuthentication 这个对象默认存的是 用户对象, getName() 返回的就是一个这个对象的地址值 String username
= successfulAuthentication.getName(); logger.debug("Creating new persistent login for user " + username); //这里是存储这个token到数据库 PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( username, generateSeriesData(), generateTokenData(), new Date()); try { tokenRepository.createNewToken(persistentToken); addCookie(persistentToken, request, response); } catch (Exception e) { logger.error("Failed to save persistent token ", e); } }

这是数据库的数据, username 是一个对象

,当用户关闭浏览器的后,

再次打开浏览器进入网址 ,立即报错

2016-11-23 12:18:44.056 [http-nio-7070-exec-1] DEBUG c.y.dao.UserDao.findUserByMobile - ==> Parameters: com.ycmedia.entity.Customer@42feadb2(String)
2016-11-23 12:18:44.087 [http-nio-7070-exec-1] DEBUG c.y.dao.UserDao.findUserByMobile - <==      Total: 0
2016-11-23 12:18:44.087 [http-nio-7070-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
java.lang.NullPointerException: null

第二次登录的时候根据这个名字去查 ,因为不是用户名, 而是用户名地址值, 肯定报错!

 

好吧, 用 UsernamePasswordAuthenticationToken  这个 子类做    记住我, 感觉不靠谱,

看下他还有别的兄弟没

发现, 大概只有一个兄弟, 感觉有点靠谱, 点进去

 

这是他的构造

    public RememberMeAuthenticationToken(String key, Object principal,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);

        if ((key == null) || ("".equals(key)) || (principal == null)
                || "".equals(principal)) {
            throw new IllegalArgumentException(
                    "Cannot pass null or empty values to constructor");
        }

        this.keyHash = key.hashCode();
        this.principal = principal;
        setAuthenticated(true);
    }

三个参数吧, key , Object principal  用户信息 authorities 用户权限。

但是, 即使这里我把 key 设置成  用户名,principal   保存用户的对象,

//        return new RememberMeAuthenticationToken(username, user, auths);

但是,

这里依然还是调用  获取 principal    而不是, key

看下去头都大了

最后没办法, 你改变不了他, 只能适应他

        return new UsernamePasswordAuthenticationToken(username, password,
                auths);
把 用户名存进去。

 当别的 接口要获取 要多做一步查询根据用户名去查权限

    @RequestMapping(value = "/customer-list")
    public ModelAndView addSystemUser(Model model) {
        String userName=  (String) AuthUtils.getAuthenticationObject().getPrincipal();
        Customer user=userService.getUserByname(userName);
        model.addAttribute("role", user.getRole());
        model.addAttribute("username", user.getMobile());
        return new ModelAndView("customer-list");
    }

 

而不是比较方便的

 

    @RequestMapping(value = "/customer-list")
    public ModelAndView addSystemUser(Model model) {
        Customer user= (Customer) AuthUtils.getAuthenticationObject().getPrincipal();
        model.addAttribute("role", user.getRole());
        model.addAttribute("username", user.getMobile());
        return new ModelAndView("customer-list");
    }

 

 综上 我感觉 spring security 的 Authentication  对象,      Object getPrincipal();  获取用户的信息, 要么就是一个对象 , 要么新加一个字段存用户名, 这样搞成object类型。

 

 


 








 

posted @ 2016-11-23 12:35  猪哥哥厉害  阅读(9368)  评论(0编辑  收藏  举报