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
Article Directory
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 series
and the other is token
. Among them, series
only when users log in using a username / password will be generated or updated, but token
as 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:
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.
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:
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, %3D
means =, 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
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,generateSeriesData
andgenerateTokenData
methods are used to obtainseries
andtoken
, in fact, a specific call generation processSecureRandom
generates a random number for re-Base64 encoding, different from our previously usedMath.random
orjava.util.Random
such 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 thetokenRepository
instancecreateNewToken
method,tokenRepository
in fact we start configurationJdbcTokenRepositoryImpl
, so this line of code will actuallyPersistentRememberMeToken
stored in the database.
(4) FinallyaddCookie
, 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
/hello
interface 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/admin
interface 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/rememberme
interface 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)
/rememberme
interface is a needrememberMe
to access.
(2)/admin
is requiredfullyAuthenticated
,fullyAuthenticated
unlikeauthenticated
,fullyAuthenticated
does not contain auto login form, andauthenticated
comprising automatic login form.
(3) The last remaining Interface (/ hello) areauthenticated
able to access.
OK, after the configuration is complete, restart the test