1.原理:参考博文:http://blog.csdn.net/fangchao2061/article/details/51179393
2.Security的实现
添加remember功能之前,先启动项目进行测试。
授权登录后,访问localhost:8080/user1可以得到用户信息,关闭浏览器,再次访问localhost:8080/user1会跳到鉴权页面。
修改项目,添加rememberMe功能。
login.html添加
记住我
name默认是remember-me
SecurityConfig:
启动项目测试:
报错
注入自己实现的userDetailsService,再次重复之前测试,勾选记住我,发现关闭浏览器之后再开,访问localhost:8080/user1可以正常访问,不勾选记住我,结果同之前一样,说明功能已经实现
在第二节,我们讲了,登录功能实现以后,会调用 this.rememberMeServices.loginSuccess(request, response, authResult);
RememberMeService接口:
/**
* 实现也决定了记住我的Cookie的有效期。
* 这个接口被设计为适应任何这些记忆我的模型.
* 这个接口没有定义如何记住我的服务应该提供一个“取消所有记住我的令牌”类型的能力,因为这将是具体的实现,不需要挂钩到Spring Security.
*
* 你可以自己实现该接口,来实现记住我功能的持久化
*
* 默认采用的存放在cookie中
*
* @author Ben Alex
*/
public interface RememberMeServices {
// ~ Methods
// ========================================================================================================
/**
* 退出浏览器再次访问的时候就会调用该方法
* @return
*/
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
/**
* 当进行交互式身份验证尝试时调用,但用户提供的凭据丢失或无效。
* 实现方法应该使任何和所有记住我的令牌无效
*/
void loginFail(HttpServletRequest request, HttpServletResponse response);
/**
* 登录成功以后调用该方法
*/
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
}
首次认证登录时候:
实现类AbstractRememberMeServices:
主要代码:
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
该类默认定义请求传入的参数名为:
public static final String DEFAULT_PARAMETER = "remember-me";
这就是为什么页面name为rember-me的原因。
onLoginSuccess(.....)方法的默认是实现是TokenBasedRememberMeServices
主要代码:
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
/**
* 获取用户名密码
*/
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
// 如果找不到用户名和密码,就要中止TokenBasedRememberMeServices在这种情况下无法构造有效的令牌.
if (!StringUtils.hasLength(username)) {
logger.debug("Unable to retrieve username");
return;
}
if (!StringUtils.hasLength(password)) {
UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
logger.debug("Unable to obtain password for user: " + username);
return;
}
}
/**
* token的生命周期,默认是两周
*/
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
// SEC-949
expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
//给token签名
String signatureValue = makeTokenSignature(expiryTime, username, password);
//设置进cookie
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue },
tokenLifetime, request, response);
if (logger.isDebugEnabled()) {
logger.debug("Added remember-me cookie for user '" + username
+ "', expiry: '" + new Date(expiryTime) + "'");
}
}
AbstractRememberMeServices.autoLogin(....)方法:
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
/**
* 找到Spring Security在请求中记住我的cookie并返回其值。
* 通过名称搜索cookie,并通过将上下文路径匹配到cookie路径。
*/
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}
UserDetails user = null;
try {
//解码cookie
String[] cookieTokens = decodeCookie(rememberMeCookie);
//从cookie中获取用户名,使用UserDetailsService.loadUserByUsername(username)获得用户信息
user = processAutoLoginCookie(cookieTokens, request, response);
//检查用户信息,是否可用,过期等等,如果是抛出对应异常
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
//认证成功,将用户信息放入token
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
cancelCookie(request, response);
throw cte;
}
catch (UsernameNotFoundException noUser) {
logger.debug("Remember-me login was valid but corresponding user not found.",
noUser);
}
catch (InvalidCookieException invalidCookie) {
logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
}
catch (AccountStatusException statusInvalid) {
logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
}
catch (RememberMeAuthenticationException e) {
logger.debug(e.getMessage());
}
//如果失败就清楚cookie
cancelCookie(request, response);
return null;
}
RememberMe的持久化
下面看看onLoginSuccess(request, response, successfulAuthentication);方法的持久化实现。
PersistentTokenBasedRememberMeServices类主要代码:
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 {
//调用tokenRepository,默认是InMemoryTokenRepositoryImpl
//基于数据库的需要改成JdbcTokenRepositoryImpl
tokenRepository.createNewToken(persistentToken);
//添加至cookie
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
public class PersistentRememberMeToken {
private final String username;
private final String series;
private final String tokenValue;
private final Date date;
//get /set 略
}
/**
* 基于JDBC的持久性登录令牌库实现
*
* @author Luke Taylor
* @since 2.0
*/
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
PersistentTokenRepository {
// ~ Static fields/initializers
// =====================================================================================
/** 用于创建数据库表以存储令牌的默认SQL */
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 getTokenBySeries 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 createNewToken */
public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
/** The default SQL used by updateToken */
public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
/** The default SQL used by removeUserTokens */
public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
// ~ Instance fields
// ================================================================================================
private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL;
private String insertTokenSql = DEF_INSERT_TOKEN_SQL;
private String updateTokenSql = DEF_UPDATE_TOKEN_SQL;
private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL;
private boolean createTableOnStartup;
/**
* 如果createTableOnStartup为true
* 初始化数据库
*/
protected void initDao() {
if (createTableOnStartup) {
getJdbcTemplate().execute(CREATE_TABLE_SQL);
}
}
public void createNewToken(PersistentRememberMeToken token) {
getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(),
token.getTokenValue(), token.getDate());
}
public void updateToken(String series, String tokenValue, Date lastUsed) {
getJdbcTemplate().update(updateTokenSql, tokenValue, lastUsed, series);
}
/**
* 加载所提供的系列标识符的令牌数据.
*/
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
try {
return getJdbcTemplate().queryForObject(tokensBySeriesSql,
new RowMapper() {
public PersistentRememberMeToken mapRow(ResultSet rs, int rowNum)
throws SQLException {
return new PersistentRememberMeToken(rs.getString(1), rs
.getString(2), rs.getString(3), rs.getTimestamp(4));
}
}, seriesId);
}
catch (EmptyResultDataAccessException zeroResults) {
if (logger.isDebugEnabled()) {
logger.debug("Querying token for series '" + seriesId
+ "' returned no results.", zeroResults);
}
}
catch (IncorrectResultSizeDataAccessException moreThanOne) {
logger.error("Querying token for series '" + seriesId
+ "' returned more than one value. Series" + " should be unique");
}
catch (DataAccessException e) {
logger.error("Failed to load token for series " + seriesId, e);
}
return null;
}
public void removeUserTokens(String username) {
getJdbcTemplate().update(removeUserTokensSql, username);
}
public void setCreateTableOnStartup(boolean createTableOnStartup) {
this.createTableOnStartup = createTableOnStartup;
}
}
启动项目测试
新建表如下:
登陆以后:
重启项目,重启浏览器,继续访问localhost:8080/user1,访问成功。源码地址:
https://gitee.com/mengcan/SpringSecurity.git