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類型。

 

 

 
        

 








 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM