一篇文章带你使用 SpringBoot 实现自动登录时的安全风险控制

已经学习了: SpringBoot 配合 SpringSecurity 实现自动登录功能

知道了 Spring Boot 自动登录存在的一些安全风险,在实际应用中,我们肯定要把这些安全风险降到最低,今天就来和大家聊一聊如何降低安全风险的问题。

(1)持久化令牌方案
(2)二次校验

一、持久化令牌

1. 原理

要理解持久化令牌,一定要先搞明白自动登录的基本玩法: SpringBoot 配合 SpringSecurity 实现自动登录功能

持久化令牌就是在基本的自动登录功能基础上,又增加了新的校验参数,来提高系统的安全性,这一些都是由开发者在后台完成的,对于用户来说,登录体验和普通的自动登录体验是一样的。

在持久化令牌中,新增了两个经过 MD5 散列函数计算的校验参数,一个是 series,另一个是 token。其中,series 只有当用户在使用用户名/密码登录时,才会生成或者更新,而 token 只要有新的会话,就会重新生成,这样就可以避免一个用户同时在多端登录,就像手机 QQ ,一个手机上登录了,就会踢掉另外一个手机的登录,这样用户就会很容易发现账户是否泄漏(之前看到松哥交流群里有小伙伴在讨论如何禁止多端登录,其实就可以借鉴这里的思路)。

持久化令牌的具体处理类在 PersistentTokenBasedRememberMeServices 中,上篇文章我们讲到的自动化登录具体的处理类是在 TokenBasedRememberMeServices 中,它们有一个共同的父类:

Fügen Sie hier eine Bildbeschreibung ein
而用来保存令牌的处理类则是 PersistentRememberMeToken,该类的定义也很简洁命令:

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

这里的 Date 表示上一次使用自动登录的时间。

2. 代码演示

Als nächstes werde ich Ihnen die spezifische Verwendung von persistenten Token durch Code zeigen.

Zunächst benötigen wir eine Tabelle zum Aufzeichnen von Token-Informationen. Diese Tabelle kann vollständig angepasst werden oder wir können den vom System bereitgestellten Standard-JDBC für den Betrieb verwenden. Wenn wir den Standard-JDBC verwenden, JdbcTokenRepositoryImplkönnen wir die Definition dieser Klasse analysieren:

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 = ?";
}

Gemäß dieser SQL-Definition können wir die Struktur der Tabelle analysieren. Hier ist ein SQL-Skript:

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;

Zuerst bereiten wir diese Tabelle in der Datenbank vor: Kopieren Sie das Skript und führen Sie es direkt aus.

Fügen Sie hier eine Bildbeschreibung ein
Da wir eine Verbindung zur Datenbank herstellen möchten, müssen wir auch die Abhängigkeiten von jdbc und mysql wie folgt vorbereiten:

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

Ändern Sie dann application.properties, um Informationen zur Datenbankverbindung zu konfigurieren:

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

Als Nächstes ändern wir SecurityConfig wie folgt:

@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();
}

Stellen Sie eine JdbcTokenRepositoryImpl-Instanz bereit, konfigurieren Sie die DataSource-Datenquelle dafür und fügen Sie schließlich die JdbcTokenRepositoryImpl-Instanz über das tokenRepository in die Konfiguration ein.

OK, nachdem wir das alles gemacht haben, können wir testen.

3. Testen

Wir gehen immer noch zuerst zur / hello-Oberfläche, dann springt sie automatisch zur Anmeldeseite und führt dann den Anmeldevorgang durch. Denken Sie daran, die Option "An mich erinnern" zu aktivieren. Nach erfolgreicher Anmeldung können wir den Server neu starten und dann den Browser schließen Öffnen Sie es erneut, besuchen Sie die / hello-Oberfläche und stellen Sie fest, dass weiterhin darauf zugegriffen werden kann. Dies zeigt an, dass unsere permanente Token-Konfiguration wirksam geworden ist.

Erinnerungs-Token anzeigen:

Fügen Sie hier eine Bildbeschreibung ein
Nachdem das Token analysiert wurde, lautet das Format wie folgt:

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

Unter ihnen %3Dbedeutet =, so dass die obigen Zeichen tatsächlich in Folgendes übersetzt werden können:

PO2UfoyLrAlIeBjJsSOB6Q==:PtgGWTyHsVQzktJ170T5gg==

Zu diesem Zeitpunkt haben wir beim Betrachten der Datenbank festgestellt, dass in der vorherigen Tabelle ein Datensatz generiert wurde

Fügen Sie hier eine Bildbeschreibung ein
Die Datensätze in der Datenbank stimmen mit dem Remember-Me-Token überein, das wir nach dem Parsen gesehen haben.

4. Quellcode-Analyse

Die Quellcode-Analyse hier ist im Grunde die gleiche wie im vorherigen Artikel, aber die Implementierungsklasse hat sich geändert, dh die Implementierung des Generierens von Token / Parsing-Token hat sich geändert. Daher zeige ich Ihnen hier hauptsächlich die Unterschiede, Prozessprobleme und alle Sie können auf den vorherigen Artikel verweisen.

这次的实现类主要是:PersistentTokenBasedRememberMeServices,我们先来看里边几个和令牌生成相关的方法:

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);
}

可以看到:

(1)在登录成功后,首先还是获取到用户名,即 username。
(2)接下来构造一个 PersistentRememberMeToken 实例,generateSeriesDatagenerateTokenData 方法分别用来获取 seriestoken,具体的生成过程实际上就是调用 SecureRandom 生成随机数再进行 Base64 编码,不同于我们以前用的 Math.random 或者 java.util.Random 这种伪随机数,SecureRandom 则采用的是类似于密码学的随机数生成规则,其输出结果较难预测,适合在登录这样的场景下使用。
(3)调用 tokenRepository 实例中的 createNewToken 方法,tokenRepository 实际上就是我们一开始配置的 JdbcTokenRepositoryImpl,所以这行代码实际上就是将 PersistentRememberMeToken 存入数据库中。
(4)最后 addCookie,大家可以看到,就是添加了 series 和 token。

这是令牌生成的过程,还有令牌校验的过程,也在该类中,方法是: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) Analysieren Sie zunächst die Serie und den Token aus dem vom Frontend gesendeten Cookie.
(2) Fragen Sie eine PersistentRememberMeToken-Instanz aus der Datenbank gemäß der Serie ab.
(3) Wenn sich das erkannte Token von dem vom Frontend gesendeten Token unterscheidet, bedeutet dies, dass das Konto möglicherweise gestohlen wird (nachdem sich eine andere Person mit Ihrem Token angemeldet hat, ändert sich das Token). Zu diesem Zeitpunkt wird das relevante Token entsprechend dem Benutzernamen entfernt. Dies entspricht der erneuten Eingabe des Benutzernamens und des Kennworts, um sich anzumelden und die neue automatische Anmeldeberechtigung zu erhalten.
(4) Überprüfen Sie anschließend, ob das Token abgelaufen ist.
(5) Erstellen Sie ein neues PersistentRememberMeToken-Objekt und aktualisieren Sie das Token in der Datenbank (dies haben wir am Anfang des Artikels gesagt, eine neue Sitzung entspricht einem neuen Token).
(6) Fügen Sie das neue Token erneut zum Cookie hinzu und kehren Sie zurück.
(7) Fragen Sie Benutzerinformationen basierend auf dem Benutzernamen ab und führen Sie dann den Anmeldevorgang durch.

Zweitens die zweite Prüfung

Im Vergleich zum vorherigen Artikel ist die Methode zum Speichern von Token tatsächlich viel sicherer, aber es besteht immer noch das Problem, dass die Benutzeridentität gestohlen wird. Dieses Problem ist tatsächlich schwer perfekt zu lösen. Wir können dies nur tun, wenn die Benutzeridentität auftritt. Wenn solche Dinge gestohlen werden, wird der Verlust minimiert.

Schauen wir uns daher eine andere Lösung an, nämlich die zweite Prüfung.

Die zweite Überprüfung ist etwas komplizierter durchzuführen. Lassen Sie mich zunächst über die Ideen sprechen.

Um den Benutzern die Verwendung zu erleichtern, haben wir die automatische Anmeldefunktion aktiviert. Die automatische Anmeldefunktion birgt jedoch Sicherheitsrisiken. Eine Möglichkeit, dies zu vermeiden, besteht darin, dass der Benutzer, wenn er die automatische Anmeldefunktion verwendet, nur einige herkömmliche unempfindliche Vorgänge ausführen kann. Zum Beispiel Daten durchsuchen und anzeigen, aber er darf keine Änderungs- oder Löschvorgänge ausführen. Wenn der Benutzer auf die Schaltfläche zum Ändern oder Löschen klickt, können wir zur Anmeldeseite zurückkehren, den Benutzer das Kennwort erneut eingeben lassen, um seine Identität zu bestätigen, und ihm dann erlauben, vertrauliche Vorgänge auszuführen.

Diese Funktion verfügt über einen bequemeren Filter, der in Shiro konfiguriert werden kann, und Spring Security ist natürlich derselbe. Beispielsweise biete ich jetzt drei Zugriffsschnittstellen an:

(1) Die erste /helloSchnittstelle nach der Zertifizierung, solange Sie entweder über die automatische Authentifizierung oder die Anmeldeauthentifizierung darauf zugreifen können. Solange Sie über die Zertifizierung verfügen, können Sie über die Authentifizierung mit Benutzername und Kennwort zugreifen.
(2) Auf die zweite /adminSchnittstelle kann nach einer Authentifizierung mit Benutzername und Kennwort zugegriffen werden. Wenn der Benutzer automatisch von einem zertifizierten Benutzer protokolliert wird, müssen Sie Ihren Benutzernamen und Ihr Kennwort erneut eingeben, um auf die Schnittstelle zugreifen zu können.
(3) Die dritte /remembermeSchnittstelle, auf die zugegriffen werden soll, muss über die automatische Anmeldeauthentifizierung erfolgen. Wenn der Benutzer eine Benutzernamen- / Kennwortauthentifizierung ist, können Sie nicht auf die Schnittstelle zugreifen.

@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) /remembermeSchnittstelle ist eine Notwendigkeit rememberMefür den Zugriff.
(2) /adminerforderlich ist fullyAuthenticated, im fullyAuthenticatedGegensatz zu authenticated, fullyAuthenticatedkeine automatische Login - Formular enthalten, und authenticatedumfasst die automatische Login - Formular.
(3) Die letzte verbleibende Schnittstelle (/ hallo) authenticatedkann darauf zugreifen.

OK, starten Sie den Test nach Abschluss der Konfiguration neu

Fügen Sie hier eine Bildbeschreibung ein

Ich denke du magst

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