Spring Security implements RememberMe function and principle exploration

In most websites, the function of RememberMe will be implemented, which is convenient for users to log in directly at the next login, and avoid entering the user name and password again to log in. The following mainly explains how to use Spring Security to realize the function of remembering me and explore it in-depth source code principle.

First, the following figure shows the schematic diagram of Spring Security's implementation of the RememberMe function:
write picture description here
write picture description here

First, you enter the login page, enter the user name and password to log in. From the previous articles, we know that Spring Security will first enter the UsernamePasswordAuthenticationFilter for verification. If the authentication is successful, it will call a class called RememberMeService, which will call the TokenRepository class. , and this class will write a generated Token to the database. When you enter the website next time, it will directly enter the RemeberMeAuthticationFilter filter, read the previous Token through the cookie, and then take the Token to find the user information in the database through the RememberService, if the Token exists, take it out The user name and other information corresponding to the Token are put into UserDetailService, and then enter the calling URL.

Next, let's take a look at how to implement this function, and finally look at its implementation principle through the source code.
It is relatively simple for Spring Security to implement the RememberMe function. First define a Repository class in the configuration class to read and write the database. Implement a UserServiceDetail class, and then configure it.

package cn.shinelon.security.browser;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import cn.shinelon.security.authenticate.ImoocAuthenctiationFailureHandler;
import cn.shinelon.security.authenticate.ImoocAuthenticationSuccessHandler;
import cn.shinelon.security.core.SecurityProperties;
import cn.shinelon.security.core.validate.code.ValidateCodeFilter;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new SCryptPasswordEncoder();
    }
    @Autowired
    private DataSource dataSource;

    //记住我后的登录页面
    @Autowired
    private UserDetailsService userDetailsService;
    //记住我的功能
    @Bean
    public PersistentTokenRepository getPersistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
        jdbcTokenRepositoryImpl.setDataSource(dataSource);
        //启动时创建一张表,这个参数到第二次启动时必须注释掉,因为已经创建了一张表
//      jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
        return jdbcTokenRepositoryImpl;
    }   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
            .formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/autentication/forms")
            .and()
        .rememberMe() 
            .tokenRepository(getPersistentTokenRepository())
            .tokenValiditySeconds(3600)   //Token过期时间为一个小时
            .userDetailsService(userDetailsService)
            .and()
            .authorizeRequests()
            .antMatchers(
                    "login.html"
                    ).permitAll()       //因为所有的请求都会需要认证处理,所以允许访问这些页面
            .antMatchers("/login.html").permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .csrf().disable();      //取消放置csrf攻击的防护
    }

}

Then there is the implementation class of UserDetailService:

package cn.shinelon.security.browser;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class MyUserDetailServices implements UserDetailsService{
    @Autowired
    private PasswordEncoder passwordEncoder;

    private Logger log=LoggerFactory.getLogger(getClass());
    /**
     * 从数据库中获取密码与前端提交的密码进行对比
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("用户名:"+username);
        //指定用户授权登录信息,这里密码指定了,实际中需要到数据库中验证密码
        String password=passwordEncoder.encode("123456");
        log.info("数据库中密码是:"+password);
        return new User(username, password,
                true,true,true,true,        //用户被锁定     
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

Information about connecting to the database in the resource file:
application.properties:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springboot?useUnicode=yes&characterEncoding=UTF8
spring.datasource.username=root
spring.datasource.password=123456

log in page:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
    <h1>登录</h1>
    <form action="/autentication/forms" method="POST">
    用户名:<input type="text" name="username"/></br>
    密码 :<input type="password" name="password"/></br>
    图形验证:<input type="text" name="imageCode" /><img src="/code/image"/></br>
    <input type="submit" value="登录"/>
    <!-- name必须是remeber-me -->
    <input type="checkbox" name="remember-me" value="true"/>记住我
    </form>
</body>
</html>

At this point, the RememberMe function has been configured. Start the project and enter the login page and click the Remember Me option. You will find that Spring Security automatically creates a table and records the Token in the database.
write picture description here
write picture description here
Then you can restart the project and revisit the resources you need without being blocked to the login page and can be accessed directly because it has already stored the Token in the database.

Next, let's dig into the source code to see how Spring Security implements this function.

Readers who don't know about Spring Security's authentication process can check the previous article. When logging in, it will enter UsernamePasswordAuthticationFilter to verify user information. After the verification is successful, it will call an AbstractAuthenticationProcessingFilter filter and enter a successfulAuthentication method to convert the user's information. The information is stored in the session, and then the RememberService is called to call the loginSuccess method. In this method, it will call the rememberMeRequested method. This method returns a Boolean value to determine whether the RemeberMe function is set. In the rememberMeRequested method, it will get the remember-me parameter sent by the login page, because we set value=”true”, so it is not empty and the method returns true.

write picture description here
write picture description here
write picture description here

The above method returns all the way to a class that enters a PersistentTokenBasedRememberMeServices and calls TokenRepository to store the Token in the database and store the Token in the cookie. Calling the createNewToken method of the JdbcTokenRepositoryImpl bean we implemented above in the TokenRepository actually uses the JdbcTemplate to insert the current Token in the update database.

write picture description here
write picture description here
When storing a cookie, it reads the set expiration parameter, and then creates a cookie and responds to the browser and stores it in the cookie through the response.
write picture description here

When we restart the project to access the page, since the session is destroyed, but there is a Token in the cookie, it will go to RememberMeAuthenticationFilter to read the value in the session. Since the project is restarted, the session is destroyed, so the session is empty, it will go to Check whether there is a Token in the cookie, traverse the cookie to obtain the Token of RememberMe, and then obtain the user information according to the Token and return the calling URL information.
write picture description here
write picture description here
write picture description here
write picture description here

The above is the principle of Spring Security's implementation of the RemeberMe function. The key is to grasp the schematic diagram of the function, and then view the source code to clearly understand its process.


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325449363&siteId=291194637