Automatic login is a very common function when we are developing software. For example, we log in to QQ mailbox:
On many websites, we will see similar options when logging in. After all, it is very troublesome to always let users enter their username and password.
The automatic login function means that after the user logs in successfully, within a certain period of time, if the user closes the browser and reopens it, or the server restarts, there is no need for the user to log in again, and the user can still directly access the interface data
As a common feature, our Spring Security must also provide corresponding support. In this article, we will look at how to implement this feature in Spring Security.
Article Directory
1. Join remember-me
For configuration convenience, just add two dependencies:
Add the following code to the configuration class:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("yolo")
.password("123").roles("admin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.and()
.csrf().disable();
}
}
We can see that here just add a .rememberMe()
can, automatic login feature is added successfully entered.
Next, we randomly add a test interface:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello Yolo !!!";
}
}
At this time, everyone found that the default login page has one more option, which is to remember me. We enter the username and password, check the Remember me box, and then click the login button to perform the login operation.
As you can see, in the login data, in addition to username and password, there is also a remember-me. The reason for showing this is to tell everyone if you need to customize the login page, what about the key of RememberMe option? write.
After the login is successful, it will automatically jump to the hello interface. We note that when the system accesses the hello interface, the cookie carried:
It was noted here one more remember-me
, which is the core implemented here, on this remember-me
I will explain, let's test results.
Next, we close the browser, and then reopen the browser. Under normal circumstances, the browser is closed and reopened. If we need to access the hello interface again, we need to log in again. But at this time, we go to visit the hello interface again and find that we can access it directly without logging in again. This shows that our RememberMe configuration has taken effect (that is, the automatic login function will take effect next time).
2. Principle analysis
It stands to reason that if the browser is closed and then reopened, it is necessary to log in again. Now there is no need to wait. So how does this function work?
First, let's analyze the extra one in the cookie remember-me
. This value is a Base64 transcoded string. We can use some online tools on the Internet to decode it. You can simply write two lines of code to decode it yourself:
@Test
void contextLoads() {
String s = new String(
Base64.getDecoder().decode("eW9sbzoxNjAxNDczNTY2NTA1OjlmMGY5YjBjOTAzYmNjYmU3ZjMwYWM0NjVlZjEzNmQ5"));
System.out.println("s = " + s);
}
Execute this code, the output is as follows:
s = yolo:1601473566505:9f0f9b0c903bccbe7f30ac465ef136d9
It can be seen this fact with Base64 string :
separated into three parts:
(1) The first paragraph is the user name, this need not be questioned.
(2) The second paragraph seems to be a timestamp. After analyzing it through online tools or Java code, we found that this is a data two weeks later.
(3) In the third paragraph, I won’t sell it. This is the value calculated using the MD5 hash function. Its plaintext format is
username + ":" + tokenExpiryTime + ":" + password + ":" + key
that the last key is a hash salt value, which can be used to prevent the token from being modified.
Learned a cookie remember-me
after meaning, then we have to remember my login process and very easy to guess the.
After the browser is closed, and then reopened, users go to access the interface hello, then will carry the cookie remember-me
to the server, after the service to get the value, you can easily calculate the user name and expiration date, and then according to the user The user’s password is retrieved by name, and then the hash value is calculated through the MD5 hash function, and then the calculated hash value is compared with the hash value passed by the browser to confirm whether the token is valid.
The process is such a process. Next, we will verify the correctness of this process by analyzing the source code.
Three, source code analysis
Next, we use the source code to verify that what we said above is correct.
Here are mainly introduced from two aspects, one is the process of remember-me token generation, and the other is the process of parsing it.
1. Generate
The core processing methods generated are:TokenBasedRememberMeServices#onLoginSuccess:
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
if (!StringUtils.hasLength(password)) {
UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
}
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
String signatureValue = makeTokenSignature(expiryTime, username, password);
setCookie(new String[] {
username, Long.toString(expiryTime), signatureValue },
tokenLifetime, request, response);
}
protected String makeTokenSignature(long tokenExpiryTime, String username,
String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
MessageDigest digest;
digest = MessageDigest.getInstance("MD5");
return new String(Hex.encode(digest.digest(data.getBytes())));
}
(1) First, extract the user name/password from the Authentication for successful login.
(2) After the login is successful, the password may be erased. Therefore, if the password is not obtained at the beginning, reload the user from UserDetailsService and obtain the password again.
(3) Next, get the validity period of the token, which is two weeks by default.
(4) The next invokedmakeTokenSignature
method to calculate a hash value, a hash value is actually calculated in accordance with the username, and the token is valid password, key. If we do not own to set this key, the default isRememberMeConfigurer#getKey
set methods, its value is a UUID string.
(5) Finally, put the user name, token validity period, and calculated hash value into the Cookie.
Regarding the fourth point, let me talk about it again.
Since we do not set their own key, key default value is a UUID string, it will bring a problem, if the server is restarted, the key becomes, thus leading to distribute out before all remember-me
automatic logon token is invalid, so, We can specify this key. The way to specify is as follows:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("yolo")
.and()
.csrf().disable();
}
If you configure the key, you can still access the hello interface even if the server restarts, even if the browser is opened and then closed
This is the process of remember-me token generation. As for how to get to the onLoginSuccess method, here can give you a little reminder:
AbstractAuthenticationProcessingFilter#doFilter -> AbstractAuthenticationProcessingFilter#successfulAuthentication -> AbstractRememberMeServices#loginSuccess -> TokenBasedRememberMeServices#onLoginSuccess。
2. Analysis
When the user turns off and then open the browser, re-access /hello
the interface, then the certification process is kind of how it?
As we said before, a series of functions in Spring Security are implemented through a filter chain, and RememberMe
this function is of course no exception.
Spring Security provides RememberMeAuthenticationFilter
classes designed to do related things, we look at RememberMeAuthenticationFilter
the doFilter
method:
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);
if (rememberMeAuth != null) {
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
if (this.eventPublisher != null) {
eventPublisher
.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext()
.getAuthentication(), this.getClass()));
}
if (successHandler != null) {
successHandler.onAuthenticationSuccess(request, response,
rememberMeAuth);
return;
}
}
chain.doFilter(request, response);
}
else {
chain.doFilter(request, response);
}
}
This method is the most critical part is that, if the SecurityContextHolder
can not get the user to log in the current instance, then invoke rememberMeServices.autoLogin
logic to log in, we look at this method:
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
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 {
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
throw cte;
}
cancelCookie(request, response);
return null;
}
You can see, this is to extract cookie
information, and the cookie
information is decoded, after decoding, and then call the processAutoLoginCookie
method to do the check, processAutoLoginCookie
the code approach I will not put up, the core process is to first obtain a user name and expiration date, then the user name The user password is queried, and then the hash value is calculated through the MD5 hash function, and then the obtained hash value is compared with the hash value passed by the browser to confirm whether the token is valid, and then whether to log in effective.
Four, summary
Read the above article, it may have found that if we open the RememberMe
function, the most central thing is to put cookie
in a token, and the token is breaking the session
limits, even if the server is restarted, even if the browser is closed and reopened , As long as the token has not expired, the data can be accessed.
Once the token is lost, others can take this token to log in to our system at will, which is a very dangerous operation.
But in fact this is a paradox. In order to improve the user experience (less login), our system inevitably leads to some security problems, but we can reduce the security risks to a minimum through technology.