在 Spring Boot 应用中使用 Spring Security 并实现 Remember Me 记住密码功能,实现自动登录
前置条件:在 Spring Boot 应用中已正确配置 Spring Security
##在页面添加记住密码的复选框
<input type="checkbox" name="remember-me"/> Remember me
##在 Security Config 配置文件中启用记住密码功能(验证信息存放在内存中)
- SecurityConfig
import cn.com.hellowood.springsecurity.security.CustomAuthenticationProvider;
import cn.com.hellowood.springsecurity.security.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
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.WebSecurityConfigurerAdater;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 任何用户都可以访问以下URI
http.authorizeRequests()
.antMatchers("/", "/login", "/login-error", "/css/**", "/index")
.permitAll();
// 其他URI均需要权限校验
http.authorizeRequests()
.anyRequest()
.authenticated();
// 只需要以下配置即可启用记住密码
http.authorizeRequests()
.and()
.rememberMe();
http.formLogin()
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.successForwardUrl("/user/index")
.failureUrl("/login-error");
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
// 为了使用用户名密码校验实现了AuthenticationProvider和UserDetailsService类
auth.authenticationProvider(customAuthenticationProvider);
try {
auth.userDetailsService(userDetailsService);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这样就可以使用记住密码了,选择记住密码登录后会在本地保存 Cookie,下次登录的时候通过 Cookie 校验用户信息;用户登录的信息保存在内存中,当内存断电或被清除之后该 Cookie 即使在有效期内也无法登录。
通过数据库存放校验信息实现记住密码登录
###创建保存校验信息的表(表名和字段值必须为以下内容)
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) NOT NULL PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
配置 Security Config
import cn.com.hellowood.springsecurity.security.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
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.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import javax.sql.DataSource;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Autowired
private CustomUserDetailsService userDetailsService;
// 数据源是为了JdbcRememberMeImpl实例而注入的,如果不设置数据源会在登陆的时候抛空指针异常
@Autowired
@Qualifier("dataSource")
DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 任何用户都可以访问以下URI
http.authorizeRequests()
.antMatchers("/", "/login", "/login-error", "/css/**", "/index")
.permitAll();
// 其他URI需要权限验证
http.authorizeRequests()
.anyRequest()
.authenticated();
// 当通过JDBC方式记住密码时必须设置 key,key 可以为任意非空(null 或 "")字符串,但必须和 RememberMeService 构造参数的
// key 一致,否则会导致通过记住密码登录失败
http.authorizeRequests()
.and()
.rememberMe()
.rememberMeServices(rememberMeServices())
.key("INTERNAL_SECRET_KEY");
// 当登录成功后会被重定向到 /user/index, 所以 loginPage 和 loginProcessingUrl 相同
http.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.successForwardUrl("/user/index")
.failureUrl("/login-error");
}
/**
* 返回 RememberMeServices 实例
*
* @return the remember me services
*/
@Bean
public RememberMeServices rememberMeServices() {
JdbcTokenRepositoryImpl rememberMeTokenRepository = new JdbcTokenRepositoryImpl();
// 此处需要设置数据源,否则无法从数据库查询验证信息
rememberMeTokenRepository.setDataSource(dataSource);
// 此处的 key 可以为任意非空值(null 或 ""),单必须和起前面
// rememberMeServices(RememberMeServices rememberMeServices).key(key)的值相同
PersistentTokenBasedRememberMeServices rememberMeServices =
new PersistentTokenBasedRememberMeServices("INTERNAL_SECRET_KEY", userDetailsService, rememberMeTokenRepository);
// 该参数不是必须的,默认值为 "remember-me", 但如果设置必须和页面复选框的 name 一致
rememberMeServices.setParameter("remember-me");
return rememberMeServices;
}
/**
* Configure global.
*
* @param auth the auth
* @throws Exception the exception
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
// 为了使用用户名密码校验实现了AuthenticationProvider和UserDetailsService类
auth.authenticationProvider(customAuthenticationProvider);
try {
auth.userDetailsService(userDetailsService);
} catch (Exception e) {
logger.error("Set userDetailService failed, {}", e.getMessage());
e.printStackTrace();
}
}
}
这样就可以实现将校验信息保存在数据库中,下次通过记住密码登录后再次访问页面会根据 Cookie 查找该用户的登录信息,如果登录信息有效则登录成功, 否则重定向到登录页面
- 自定义实现 AuthenticationProvider 接口
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
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.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* The type Custom authentication provider.
*
* @author HelloWood
*/
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private CustomUserDetailsService userDetailsService;
/**
* Validate user info is correct form database
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
logger.info("start validate user {} login", username);
// 通过用户名和密码校验,如果校验不通过会抛出 AuthenticationException
userDetailsService.loadUserByUsernameAndPassword(username, password);
Authentication auth = new UsernamePasswordAuthenticationToken(username, password, grantedAuthorities);
return auth;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
- 自定义实现 UserDetailsService 接口
import cn.com.hellowood.springsecurity.mapper.UserMapper;
import cn.com.hellowood.springsecurity.model.UserModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
/**
* The type Custom user details service.
*
* @author HelloWood
*/
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private UserMapper userMapper;
@Autowired
private HttpSession session;
/**
* 通过用户名和密码加载用户信息并校验
*
* @param username the username
* @param password the password
* @return the user model
* @throws AuthenticationException the authentication exception
*/
public UserModel loadUserByUsernameAndPassword(String username, String password) throws AuthenticationException {
logger.info("user {} is login by username and password", username);
UserModel user = userMapper.getUserByUsernameAndPassword(username, password);
validateUser(username, user);
return user;
}
/**
* 通过用户名加载用户信息,重写该方法用于记住密码后通过 Cookie 登录
*
* @param username
* @param user
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("user {} is login by remember me cookie", username);
UserModel user = userMapper.getUserByUsername(username);
validateUser(username, user);
return new User(user.getUsername(), user.getPassword(), new ArrayList<GrantedAuthority>());
}
/**
* 校验用户信息并将用户信息放在 Session 中
*
* @param username
* @param user
*/
private void validateUser(String username, UserModel user) {
if (user == null) {
logger.error("user {} login failed, username or password is wrong", username);
throw new BadCredentialsException("Username or password is not correct");
} else if (!user.getEnabled()) {
logger.error("user {} login failed, this account had expired", username);
throw new AccountExpiredException("Account had expired");
}
// TODO There should add more logic to determine locked, expired and others status
logger.info("user {} login success", username);
// 当用户信息有效时放入 Session 中
session.setAttribute("user", user);
}
}