SpringSecurity学习日记(4):记住我(remember-me)功能

图片摘自此处,在此感谢
在这里插入图片描述

在用户认证成功后,会调用AbstractAuthenticationProcessingFilter类的successfulAuthentication()方法

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}

		SecurityContextHolder.getContext().setAuthentication(authResult);
		// 记住我功能
		rememberMeServices.loginSuccess(request, response, authResult);

		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

在这个方法中rememberMeServices.loginSuccess(request, response, authResult)便跟记住我功能有关。
RememberMeServices接口提供了记住我功能。具体功能由其实现类实现。我们再回到上面的loginSuccess()方法。它由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);
	}

首先我们要通过rememberMeRequested(request, parameter)这个方法判断我们是否选择了记住我功能。

protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
		if (alwaysRemember) {
			return true;
		}
		// parameter值为 "remember-me"
		// 获取前台传入的 "remember-me" 的值
		String paramValue = request.getParameter(parameter);
		// 非空判断
		if (paramValue != null) {
			// 获取到的值为 true,on,yes,1 中的任意一种,即表示开启记住我功能
			if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
					|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
				return true;
			}
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Did not send remember-me cookie (principal did not set parameter '"
					+ parameter + "')");
		}

		return false;
	}
// parameter 的值为 "remember-me"
private String parameter = DEFAULT_PARAMETER;
public static final String DEFAULT_PARAMETER = "remember-me";

要实现记住我功能,从这个方法中我们可以得出两点:

  1. 前台标签中的 name 属性的值必须为 “remember-me”(当然我们也可以自定义,下面会讲到)
  2. 前台标签的 value 属性的值必须为 true, on, yes, 1 中的一种

如果满足上述条件,继续调用onLoginSuccess()方法,该方法具体由AbstractRememberMeServices 的子类PersistentTokenBasedRememberMeServices实现

protected void onLoginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
		String username = successfulAuthentication.getName();

		logger.debug("Creating new persistent login for user " + username);

		PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
				username, generateSeriesData(), generateTokenData(), new Date());
		try {
			// 在数据库中保存cookie信息
			tokenRepository.createNewToken(persistentToken);
			// 在浏览器中保存cookie信息
			addCookie(persistentToken, request, response);
		}
		catch (Exception e) {
			logger.error("Failed to save persistent token ", e);
		}
	}

该方法做了两件事,一是在数据库中保存cookie信息,二是在浏览器中保存cookie信息。
tokenRepository.createNewToken(persistentToken)添加cookie信息到数据库中,具体方法由子类JdbcTokenRepositoryImpl实现

public void createNewToken(PersistentRememberMeToken token) {
		// 获取Spring中的JdbcTemplate执行update()操作
		getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(),
				token.getTokenValue(), token.getDate());
	}

我们看一下这个insertTokenSql

public static final String DEF_INSERT_TOKEN_SQL = 
"insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";

所以这里就是调用JdbcTemplate类执行update()操作向persistent_logins 这个表中插入你的用户信息以及cookie信息。而当你下次登录时,又会调用查询语句比对数据库中的cookie信息与浏览器中的cookie信息是否一致,来完成自动登录。到这里我们已经完成了cookie的保存操作。再来看一下自动登录时怎么实现的。
我们回到RememberMeAuthenticationFilterdoFilter()方法

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
					response);
			...
		}
			
	}

首先会先判断前面的过滤器是否进行过认证(SecurityContext中是否有认证信息,认证后的信息会保存在SecurityContext中),未进行过认证的话会调用RememberMeServicesautoLogin()方法。该方法具体由子类AbstractRememberMeServices实现

public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {
		// 从浏览器的请求域中获取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);
			// 进行自动登录验证
			user = processAutoLoginCookie(cookieTokens, request, response);
			// 检查user的用户信息是否可用
			userDetailsChecker.check(user);

			logger.debug("Remember-me cookie accepted");

			return createSuccessfulAuthentication(request, user);
		}
		...
	}

在该方法中又调用了 processAutoLoginCookie()方法比对浏览器中的cookie信息和数据库中保存的cookie信息是否一致

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
			HttpServletRequest request, HttpServletResponse response) {

		if (cookieTokens.length != 2) {
			throw new InvalidCookieException("Cookie token did not contain " + 2
					+ " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
		}

		final String presentedSeries = cookieTokens[0];
		final String presentedToken = cookieTokens[1];
		// 获取数据库中保存的cookie信息
		PersistentRememberMeToken token = tokenRepository
				.getTokenForSeries(presentedSeries);

		if (token == null) {
			// No series match, so we can't authenticate using this cookie
			throw new RememberMeAuthenticationException(
					"No persistent token found for series id: " + presentedSeries);
		}

		// We have a match for this user/series combination
		if (!presentedToken.equals(token.getTokenValue())) {
			// Token doesn't match series value. Delete all logins for this user and throw
			// an exception to warn them.
			tokenRepository.removeUserTokens(token.getUsername());

			throw new CookieTheftException(
					messages.getMessage(
							"PersistentTokenBasedRememberMeServices.cookieStolen",
							"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
		}

		if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
				.currentTimeMillis()) {
			throw new RememberMeAuthenticationException("Remember-me login has expired");
		}


		if (logger.isDebugEnabled()) {
			logger.debug("Refreshing persistent login token for user '"
					+ token.getUsername() + "', series '" + token.getSeries() + "'");
		}

		PersistentRememberMeToken newToken = new PersistentRememberMeToken(
				token.getUsername(), token.getSeries(), generateTokenData(), new Date());

		try {
			tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
					newToken.getDate());
			addCookie(newToken, request, response);
		}
		catch (Exception e) {
			logger.error("Failed to update token: ", e);
			throw new RememberMeAuthenticationException(
					"Autologin failed due to data access problem");
		}
		// 登录用户
		return getUserDetailsService().loadUserByUsername(token.getUsername());
	}

tokenRepository.getTokenForSeries(presentedSeries)用于获取数据库中保存的cookie信息。如果比对失败则会抛出异常。比对成功,会调用tokenRepository.updateToken()方法更新cookie信息,用于下一次比对。最后调用getUserDetailsService().loadUserByUsername(token.getUsername());进行用户信息验证,登录用户。这里便实现了用户的自动登录。

再说一下processAutoLoginCookie()方法中的具体步骤

  • 解析前端传来的Cookie,里面包含了Token和seriesId,它会使用seriesId查找数据库的Token
  • 检查Cookie中的Token和数据库查出来的Token是否相同
  • 相同的话再检查数据库中的Token是否已过期
  • 如果以上都符合的话,会使用旧的用户名和series重新new一个Token,这时过期时间也重新刷新
  • 然后将新的Token保存回数据库,同时添加回Cookie中
  • 最后再调用UserDetailsServiceloadUserByUsername()方法返回UserDetails对象完成登录

代码实现
persistent_logins表(这张表也可以由springsecurity为我们创建,下面有讲到)

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

pom.xml

<dependencies>
    <!-- SpringSecurity -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    <!-- mysql-connector-java -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
</dependencies>

application.yml

server:
  port: 8080
spring:
  datasource:
    # 设置数据库连接池类型
    type: com.alibaba.druid.pool.DruidDataSource
    # 设置驱动类(因为我用的是 mysql-connector 8,所以要加 cj)
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 记得加 serverTimezone
    url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai
    username: root
    password: 990515

UserService

@Service
public class UserService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("admin");
        return new User("guest",
                new BCryptPasswordEncoder().encode("123"),
                authorities);
    }
}

UserController

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/login")
    public String login() {
        return "登录成功";
    }
}

SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 注入加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 注入数据源
     */
    @Autowired
    private DataSource dataSource;

    /**
     * 注入 PersistentTokenRepository
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository= new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 可以为我们自动创建表 persistent_logins (若数据库已存在该表,执行该语句则会报错)
        // tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    /**
     * 认证用户
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    /**
     * 拦截http请求
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 放行请求
                .antMatchers("/", "/login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                // 设置登录页面
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/user/login")
                .and()
                // 开启记住我功能
                .rememberMe()
                // (可选)设置前端传递过来的 remember-me 功能的属性名
                // 默认的属性名为 remember-me
                .rememberMeParameter("my-remember-me") 
                // (可选)设置 remember-me 功能对应的 cookie 名
                // 默认的 cookie 名为 remember-me
                .rememberMeCookieName("my-remember-me-cookie") 
                // 设置 PersistentTokenRepository (将cookie信息存储到数据库中)
                .tokenRepository(persistentTokenRepository())
                // 设置 Cookie 的有效期为1小时
                .tokenValiditySeconds(60*60)
                // 设置 UserDetailsService
                .userDetailsService(userDetailsService)
                .and()
                // 关闭csrf
                .csrf().disable();
    }
}

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
    <form action="/user/login" method="post">
        用户名: <input type="text" name="username"> 
        <br/>
        密码: <input type="password" name="password"> 
        <br/>
        <input type="checkbox" name="my-remember-me"> 记住我 
        <br/>
        <input type="submit" value="登录">
    </form>
</body>
</html>

在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值