SpringSecurity(五):RememberMe以及源码分析

1.原理:参考博文:http://blog.csdn.net/fangchao2061/article/details/51179393


2.Security的实现

添加remember功能之前,先启动项目进行测试。

授权登录后,访问localhost:8080/user1可以得到用户信息,关闭浏览器,再次访问localhost:8080/user1会跳到鉴权页面。

修改项目,添加rememberMe功能。


login.html添加

			<tr>
				<td><input type="checkbox" name="remember-me" value="true" />记住我
				</td>
			</tr>
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) + "'");
		}
	}

退出浏览器以后,再次请求localhost:8080/user1,就会请求autoLogin(....)方法

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);
		}
	}


PersistentRememberMeToken类

public class PersistentRememberMeToken {
	private final String username;
	private final String series;
	private final String tokenValue;
	private final Date date;
	//get /set 略
}

接下来是JdbcTokenRepositoryImpl

/**
 * 基于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 <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 = ?";

	// ~ 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<PersistentRememberMeToken>() {
						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;
	}
}

ok,原理知道了,接下来写自己的实现



启动项目测试

新建表如下:


登陆以后:


重启项目,重启浏览器,继续访问localhost:8080/user1,访问成功。

源码地址:

https://gitee.com/mengcan/SpringSecurity.git

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值