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
![](data:image/png;base64,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)
而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类型。