Spring Security(四):认证(Authentication)-记住我

登录页面一般都会有记住我(或者是保持登录)这样的一个选项,用于在某段时间范围内退出浏览器不用再重新登录,仍旧保持登录的状态。

1. pom.xml

增加mybatis和mysql依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

2. application.yml

# 数据源配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root123

3. login.html

增加记住我复选框,注意name必须为"remember-me"

<!DOCTYPE html>
<html lang="en"
      xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <title>登录</title>
</head>
<body>
<form method="post" action="/login">
    <h2 class="form-signin-heading">登录</h2>
    <span th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span>
    <p>
        <label for="username">用户名</label>
        <input type="text" id="username" name="username" required autofocus>
    </p>
    <p>
        <label for="password">密码</label>
        <input type="password" id="password" name="password" required>
    </p>
    <input type="checkbox" name="remember-me"/>记住我<br>
    <button type="submit">登录</button>
</form>
</body>
</html>

4. SecurityConfiguration

增加rememberMe配置:

  • tokenRepository 令牌仓库
  • tokenValiditySeconds token保存时间,单位秒
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private DataSource dataSource;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                // 配置需要认证的请求
                .authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    .and()
                // 登录表单相关配置
                .formLogin()
                    .loginPage("/login")
                    .usernameParameter("username")
                    .passwordParameter("password")
                    .successHandler(myAuthenticationSuccessHandler)
                    .failureUrl("/login?error")
                    .permitAll()
                    .and()
                 .rememberMe()
                    .userDetailsService(myUserDetailsService)
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(60 * 60 * 60 * 30)
                    .and()
                // 登出相关配置
                .logout()
                    .permitAll();

    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/static/**");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);

        return tokenRepository;
    }
}

5. 创建表persistent_logins

此表Spring Security中会使用到,用于持久化用户登录的信息。

create table persistent_logins (
	username varchar(64) not null, 
	series varchar(64) primary key, 
	token varchar(64) not null, 
	last_used timestamp not null
);

6. 测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7. 源码分析

在这里插入图片描述
过程一

| 用户输入用户名密码并选中"记住我",点击登录
	| UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter#doFilter
		| successfulAuthentication(request, response, chain, authResult)
			| AbstractRememberMeServices#loginSuccess(request, response, authResult)
				| onLoginSuccess(request, response, successfulAuthentication)
					| PersistentTokenBasedRememberMeServices#onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication)
						| PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date())
						| tokenRepository.createNewToken(persistentToken)
							| JdbcTokenRepositoryImpl#createNewToken(persistentToken)
								| public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)"
								| private String insertTokenSql = DEF_INSERT_TOKEN_SQL
								| getJdbcTemplate().update(insertTokenSql, token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate())
						| addCookie(persistentToken, request, response)
							| AbstractRememberMeServices#setCookie(new String[] { token.getSeries(), token.getTokenValue() }, getTokenValiditySeconds(), request, response)
								| response.addCookie(cookie)
								
  1. 用户输入用户名密码并选中"记住我",点击登录
  2. 登录被UsernamePasswordAuthenticationFilter过滤器拦截,进行认证,认证成功(successfulAuthentication)后会调用AbstractRememberMeServices#loginSuccess()
  3. AbstractRememberMeServices#loginSuccess()会调用PersistentTokenBasedRememberMeServices#onLoginSuccess()
  4. onLoginSuccess()方法首先会创建PersistentRememberMeToken对象,会生成series、token: generateSeriesData()、generateTokenData()
  5. 接下来调用JdbcTokenRepositoryImpl#createNewToken(persistentToken)像persistent_logins表插入一条数据
  6. 接下来调用addCookie(persistentToken, request, response)像reponse对象中添加Cookie, response.addCookie(cookie)

过程二

| 关闭服务器再重新启动,然后访问任意一个接口
	| RememberMeAuthenticationFilter#doFilter
		| Authentication rememberMeAuth = AbstractRememberMeServices#autoLogin(request, response)
			| String rememberMeCookie = extractRememberMeCookie(request)
			| String[] cookieTokens = decodeCookie(rememberMeCookie)
			| UserDetails user = PersistentTokenBasedRememberMeServices#processAutoLoginCookie(cookieTokens, request, response)
				| final String presentedSeries = cookieTokens[0]
				| PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries)
				| PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date())
				| tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
				| addCookie(newToken, request, response)
				| getUserDetailsService().loadUserByUsername(token.getUsername())
			| AbstractRememberMeServices#createSuccessfulAuthentication(request, user)
				| RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(key, user, authoritiesMapper.mapAuthorities(user.getAuthorities()));
					| public RememberMeAuthenticationToken(String key, Object principal, Collection<? extends GrantedAuthority> authorities) {
						 this.keyHash = key.hashCode();
						 setAuthenticated(true);
					  }
				| auth.setDetails(authenticationDetailsSource.buildDetails(request));	
		| rememberMeAuth = ProviderManager#authenticate(rememberMeAuth)
			| RememberMeAuthenticationProvider#authenticate(authentication)
				| if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
					throw new BadCredentialsException(messages.getMessage("RememberMeAuthenticationProvider.incorrectKey", "The presented RememberMeAuthenticationToken does not contain the expected key"));
				  }
				  

访问任意一个请求都会被RememberMeAuthenticationFilter过滤器拦截,通过remember-me Cookie可以解析出series和username字段,通过series去更新这条记录,通过用户名获取用户信息,然后创建成功认证的token(RememberMeAuthenticationToken), 最终调RememberMeAuthenticationProvider#authenticate(authentication)来认证,记住我认证逻辑只判断key是否一致,如果一致就认证通过,如果不一致就抛异常。记住我认证和用户名密码认证完全不一样,记住我认证只判断key是否一致,不会判断用户名和密码是否正确。

发布了308 篇原创文章 · 获赞 936 · 访问量 133万+

猜你喜欢

转载自blog.csdn.net/vbirdbest/article/details/90178409