An article takes you to use SpringBoot to implement security risk control during automatic login

Have learned: SpringBoot cooperates with SpringSecurity to realize automatic login function

Knowing some of the security risks of Spring Boot's automatic login, in practical applications, we must minimize these security risks. Today, I will talk to you about how to reduce security risks.

(1) Persistent token scheme
(2) Secondary verification

1. Persistent token

1. Principle

To understand the persistent token, you must first understand the basic gameplay of automatic login: SpringBoot cooperates with SpringSecurity to realize the automatic login function

Persistent token is based on the basic automatic login function, andAdded new calibration parameters to improve the security of the system, These are all done by the developer in the background. For users, the login experience is the same as the normal automatic login experience.

In the persistent token, two new verification parameters calculated by the MD5 hash function are added, one is seriesand the other is token. Among them, seriesonly when users log in using a username / password will be generated or updated, but tokenas long as there is a new session, it will regenerate, so you can avoid a multi-terminal user while logged in as mobile QQ, a mobile phone Once you log in, it will kick off the login of another mobile phone, so that the user will easily find out whether the account is leaked (I saw a small partner in the Song Ge exchange group discussing how to prohibit multi-terminal login, in fact, you can learn from the idea here ).

Persistent tokens specific processing class PersistentTokenBasedRememberMeServices, the last article we talked about the specific automated login process is in class TokenBasedRememberMeServices, they have a common parent:

Insert picture description here
The processing class used to save the token is PersistentRememberMeToken, the definition of this class is also very concise command:

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

Date here represents the time when automatic login was used last time.

2. Code Demo

Next, I will show you the specific usage of persistent tokens through code.

First of all, we need a table to record token information. This table can be completely customized, or we can use the default JDBC provided by the system to operate. If we use the default JDBC, that is JdbcTokenRepositoryImpl, we can analyze the definition of this class:

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
  PersistentTokenRepository {
    
    
 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)";
 public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
 public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
 public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
 public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
}

According to this SQL definition, we can analyze the structure of the table. Here is a SQL script:

CREATE TABLE `persistent_logins` (
  `username` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `series` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

First, we prepare this table in the database: copy the script and execute it directly.

Insert picture description here
Since we want to connect to the database, we also need to prepare jdbc and mysql dependencies, as follows:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

Then modify application.properties to configure database connection information:

spring.datasource.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

Next, we modify SecurityConfig as follows:

@Autowired
DataSource dataSource;
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository() {
    
    
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .rememberMe()
            .key("yolo")
            .tokenRepository(jdbcTokenRepository())
            .and()
            .csrf().disable();
}

Provide a JdbcTokenRepositoryImpl instance, and configure the DataSource data source for it, and finally include the JdbcTokenRepositoryImpl instance into the configuration through the tokenRepository.

OK, after doing all this, we can test.

3. Testing

We still go to the /hello interface first, then it will automatically jump to the login page, and then we perform the login operation, remember to check the "remember me" option, after the login is successful, we can restart the server, and then close the browser Open it again, and then visit the /hello interface, and find that it can still be accessed, indicating that our persistent token configuration has taken effect.

View remember-me token:

Insert picture description here
After the token is parsed, the format is as follows:

@Test
void contextLoads() {
    
    
     String s = new String(
            Base64.getDecoder().decode("UE8yVWZveUxyQWxJZUJqSnNTT0I2USUzRCUzRDpQdGdHV1R5SHNWUXprdEoxNzBUNWdnJTNEJTNE"));
     System.out.println("s = " + s);
    }
PO2UfoyLrAlIeBjJsSOB6Q%3D%3D:PtgGWTyHsVQzktJ170T5gg%3D%3D

Among them, %3Dmeans =, so the above characters can actually be translated into the following:

PO2UfoyLrAlIeBjJsSOB6Q==:PtgGWTyHsVQzktJ170T5gg==

At this point, looking at the database, we found that a record was generated in the previous table

Insert picture description here
The records in the database are consistent with the remember-me token we saw after parsing.

4. Source code analysis

The source code analysis here is basically the same as the process in the previous article, but the implementation class has changed, that is, the implementation of generating tokens/parsing tokens has changed, so here I mainly show you the differences, process problems, everyone You can refer to the previous article.

The main implementation class this time is: PersistentTokenBasedRememberMeServices, let's first look at a few methods related to token generation:

protected void onLoginSuccess(HttpServletRequest request,
  HttpServletResponse response, Authentication successfulAuthentication) {
    
    
 String username = successfulAuthentication.getName();
 PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
   username, generateSeriesData(), generateTokenData(), new Date());
 tokenRepository.createNewToken(persistentToken);
 addCookie(persistentToken, request, response);
}
protected String generateSeriesData() {
    
    
 byte[] newSeries = new byte[seriesLength];
 random.nextBytes(newSeries);
 return new String(Base64.getEncoder().encode(newSeries));
}
protected String generateTokenData() {
    
    
 byte[] newToken = new byte[tokenLength];
 random.nextBytes(newToken);
 return new String(Base64.getEncoder().encode(newToken));
}
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request,
  HttpServletResponse response) {
    
    
 setCookie(new String[] {
    
     token.getSeries(), token.getTokenValue() },
   getTokenValiditySeconds(), request, response);
}

can be seen:

(1) After the login is successful, first obtain the user name, which is username.
(2) Next, a configuration example PersistentRememberMeToken, generateSeriesDataand generateTokenDatamethods are used to obtain seriesand token, in fact, a specific call generation process SecureRandomgenerates a random number for re-Base64 encoding, different from our previously used Math.randomor java.util.Randomsuch a pseudo-random number, SecureRandom is used It is similar to the random number generation rules of cryptography, and its output results are more difficult to predict, suitable for use in scenarios such as login.
(3) calls the tokenRepositoryinstance createNewTokenmethod, tokenRepositoryin fact we start configuration JdbcTokenRepositoryImpl, so this line of code will actually PersistentRememberMeTokenstored in the database.
(4) Finally addCookie, as you can see, series and token have been added.

This is the process of token generation, as well as the process of token verification. Also in this class, the method is processAutoLoginCookie:

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
  HttpServletRequest request, HttpServletResponse response) {
    
    
 final String presentedSeries = cookieTokens[0];
 final String presentedToken = cookieTokens[1];
 PersistentRememberMeToken token = tokenRepository
   .getTokenForSeries(presentedSeries);
 if (!presentedToken.equals(token.getTokenValue())) {
    
    
  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");
 }
 PersistentRememberMeToken newToken = new PersistentRememberMeToken(
   token.getUsername(), token.getSeries(), generateTokenData(), new Date());
 tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
    newToken.getDate());
 addCookie(newToken, request, response);
 return getUserDetailsService().loadUserByUsername(token.getUsername());
}

(1) First, parse the series and token from the cookie sent from the front end.
(2) Query a PersistentRememberMeToken instance from the database according to the series.
(3) If the detected token is different from the token sent from the front end, it means that the account may be stolen (after someone else logs in with your token, the token will change). At this time, the relevant token is removed according to the user name, which is equivalent to having to re-enter the user name and password to log in to obtain the new automatic login permission.
(4) Next, check whether the token has expired.
(5) Construct a new PersistentRememberMeToken object, and update the token in the database (this is what we said at the beginning of the article, a new session will correspond to a new token).
(6) Re-add the new token to the cookie and return.
(7) Query user information based on the user name, and then go through the login process.

Second, the second check

Compared with the previous article, the method of persisting tokens is actually much safer, but there is still the problem of user identity being stolen. This problem is actually difficult to solve perfectly. What we can do is only when user identity occurs. When such things are stolen, the loss is minimized.

Therefore, let's look at another solution, which is the second check.

The second check is a little more complicated to implement, so let me talk about the ideas first.

In order to make it easy for users to use, we have enabled the automatic login function, but the automatic login function brings security risks. One way to avoid it is that if the user uses the automatic login function, we can only let him do some conventional insensitive operations. For example, data browsing and viewing, but he is not allowed to do any modification or deletion operations. If the user clicks the modify or delete button, we can jump back to the login page, let the user re-enter the password to confirm his identity, and then allow him to perform sensitive operations.

This function has a more convenient filter to configure in Shiro, and Spring Security is of course the same. For example, I now provide three access interfaces:

(1) The first /hellointerface after certification as long as you can access, either through automated or login authentication, as long as the certification, you can access by user name and password authentication.
(2) The second /admininterface can be accessed after a user name-password authentication, if the user is automatically logged by certified, you must re-enter your user name and password to access the interface.
(3) The third /remembermeinterface to access must be through the automatic login authentication, if the user is a user name / password authentication, you can not access the interface.

@Override
protected void configure(HttpSecurity http) throws Exception {
    
    
    http.authorizeRequests()
            .antMatchers("/rememberme").rememberMe()
            .antMatchers("/admin").fullyAuthenticated()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .rememberMe()
            .key("javaboy")
            .tokenRepository(jdbcTokenRepository())
            .and()
            .csrf().disable();
}

(1) /remembermeinterface is a need rememberMeto access.
(2) /adminis required fullyAuthenticated, fullyAuthenticatedunlike authenticated, fullyAuthenticateddoes not contain auto login form, and authenticatedcomprising automatic login form.
(3) The last remaining Interface (/ hello) are authenticatedable to access.

OK, after the configuration is complete, restart the test

Insert picture description here

Guess you like

Origin blog.csdn.net/nanhuaibeian/article/details/108763168