已经学习了: SpringBoot 配合 SpringSecurity 实现自动登录功能
知道了 Spring Boot 自动登录存在的一些安全风险,在实际应用中,我们肯定要把这些安全风险降到最低,今天就来和大家聊一聊如何降低安全风险的问题。
(1)持久化令牌方案
(2)二次校验
一、持久化令牌
1. 原理
要理解持久化令牌,一定要先搞明白自动登录的基本玩法: SpringBoot 配合 SpringSecurity 实现自动登录功能
持久化令牌就是在基本的自动登录功能基础上,又增加了新的校验参数,来提高系统的安全性,这一些都是由开发者在后台完成的,对于用户来说,登录体验和普通的自动登录体验是一样的。
在持久化令牌中,新增了两个经过 MD5 散列函数计算的校验参数,一个是 series
,另一个是 token
。其中,series
只有当用户在使用用户名/密码登录时,才会生成或者更新,而 token
只要有新的会话,就会重新生成,这样就可以避免一个用户同时在多端登录,就像手机 QQ ,一个手机上登录了,就会踢掉另外一个手机的登录,这样用户就会很容易发现账户是否泄漏(之前看到松哥交流群里有小伙伴在讨论如何禁止多端登录,其实就可以借鉴这里的思路)。
持久化令牌的具体处理类在 PersistentTokenBasedRememberMeServices
中,上篇文章我们讲到的自动化登录具体的处理类是在 TokenBasedRememberMeServices
中,它们有一个共同的父类:
而用来保存令牌的处理类则是 PersistentRememberMeToken
,该类的定义也很简洁命令:
public class PersistentRememberMeToken {
private final String username;
private final String series;
private final String tokenValue;
private final Date date;
//省略 getter
}
这里的 Date 表示上一次使用自动登录的时间。
2. 代码演示
次に、永続的なトークンの具体的な使用方法をコードで示します。
まず、トークン情報を記録するテーブルが必要です。このテーブルは完全にカスタマイズすることも、システムが提供するデフォルトのJDBCを使用して操作することもできます。デフォルトのJDBCを使用する場合、つまりJdbcTokenRepositoryImpl
、このクラスの定義を分析できます。
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 = ?";
}
このSQL定義に従って、テーブルの構造を分析できます。SQLスクリプトは次のとおりです。
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;
まず、このテーブルをデータベースに準備します。スクリプトをコピーして直接実行します。
データベースに接続する必要があるため、次のようにjdbcおよびmysqlの依存関係も準備する必要があります。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
次に、application.propertiesを変更して、データベース接続情報を構成します。
spring.datasource.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
次に、SecurityConfigを次のように変更します。
@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();
}
JdbcTokenRepositoryImplインスタンスを提供し、DataSourceデータソースを構成し、最後に、tokenRepositoryを介してJdbcTokenRepositoryImplインスタンスを構成に含めます。
OK、これをすべて実行したら、テストできます。
3.テスト
最初に/ helloインターフェースに移動します。その後、自動的にログインページにジャンプし、ログイン操作を実行します。「remember me」オプションを必ず確認してください。ログインが成功したら、サーバーを再起動して、ブラウザーを閉じます。もう一度開いてから、/ helloインターフェイスにアクセスし、引き続きアクセスできることを確認します。これは、永続的なトークン構成が有効になったことを示しています。
remember-meトークンを表示:
トークンが解析された後の形式は次のとおりです。
@Test
void contextLoads() {
String s = new String(
Base64.getDecoder().decode("UE8yVWZveUxyQWxJZUJqSnNTT0I2USUzRCUzRDpQdGdHV1R5SHNWUXprdEoxNzBUNWdnJTNEJTNE"));
System.out.println("s = " + s);
}
PO2UfoyLrAlIeBjJsSOB6Q%3D%3D:PtgGWTyHsVQzktJ170T5gg%3D%3D
これらのうち、%3D
=を意味するため、上記の文字は実際には次のように変換できます。
PO2UfoyLrAlIeBjJsSOB6Q==:PtgGWTyHsVQzktJ170T5gg==
この時点でデータベースを見ると、前のテーブルにレコードが生成されていることがわかりました。
データベース内のレコードは、解析後に見たremember-meトークンと一致しています。
4.ソースコード分析
ここでのソースコード分析は基本的に前の記事のプロセスと同じですが、実装クラスが変更されました。つまり、トークンの生成/トークンの解析の実装が変更されたので、ここでは主に違い、プロセスの問題、すべてを示します前の記事を参照できます。
今回の主な実装クラスは、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)ログインに成功したら、まずユーザー名であるユーザー名を取得します。
(2)次に、構成例のPersistentRememberMeTokengenerateSeriesData
とgenerateTokenData
メソッドを使用して取得series
しtoken
、実際には、特定の呼び出し生成プロセスSecureRandom
が、以前に使用した、Math.random
またはjava.util.Random
そのような疑似乱数とは異なる、再Base64エンコード用の乱数を生成します。SecureRandomが使用されます。これは、暗号化の乱数生成規則に似ており、その出力結果は予測がより難しく、ログインなどのシナリオでの使用に適しています。
(3)tokenRepository
インスタンスcreateNewToken
メソッドを呼び出し、tokenRepository
実際には構成を開始するJdbcTokenRepositoryImpl
ため、このコード行は実際PersistentRememberMeToken
にはデータベースに格納されます。
(4)最後にaddCookie
、ご覧のとおり、シリーズとトークンが追加されました。
これは、トークン生成のプロセスとトークン検証のプロセスです。このクラスでも、メソッドは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)最初に、フロントエンドから送信されたCookieからシリーズとトークンを解析します。
(2)シリーズに従って、データベースからPersistentRememberMeTokenインスタンスをクエリします。
(3)検出されたトークンがフロントエンドから送信されたトークンと異なる場合は、アカウントが盗まれる可能性があります(他の誰かがあなたのトークンでログインすると、トークンが変更されます)。このとき、関連するトークンはユーザー名に従って削除されます。これは、ユーザー名とパスワードを再入力してログインし、新しい自動ログイン権限を取得することと同じです。
(4)次に、トークンの有効期限が切れていないかどうかを確認します。
(5)新しいPersistentRememberMeTokenオブジェクトを作成し、データベース内のトークンを更新します(これは記事の冒頭で述べたとおり、新しいセッションは新しいトークンに対応します)。
(6)Cookieに新しいトークンを再度追加して戻ります。
(7)ユーザー名に基づいてユーザー情報を照会し、ログインプロセスを実行します。
第二に、第二のチェック
前回の記事と比較して、トークンを永続化する方法は実際にははるかに安全ですが、ユーザーIDが盗まれるという問題が依然としてあります。この問題を完全に解決することは実際には困難です。ユーザーIDが発生したときにのみ、私たちにできることはあります。そのようなものが盗まれても、損失は最小限に抑えられます。
したがって、2番目のチェックである別のソリューションを見てみましょう。
2番目のチェックは実装が少し複雑なので、最初にアイデアについて説明します。
ユーザーが使いやすくするために、自動ログイン機能を有効にしていますが、自動ログイン機能はセキュリティ上のリスクをもたらします。自動ログイン機能を使用する場合、ユーザーが従来の操作に依存しない操作しか実行できないようにすることが1つの方法です。たとえば、データの閲覧と表示は可能ですが、変更や削除の操作は許可されていません。ユーザーが変更または削除のボタンをクリックすると、ログインページに戻り、パスワードを再入力して身元を確認し、機密性の高い操作を実行できるようにします。
この関数には、Shiroで構成するためのより便利なフィルターがあり、Spring Securityはもちろん同じです。たとえば、次の3つのアクセスインターフェイスを提供します。
(1)
/hello
認証後、自動認証またはログイン認証を介してアクセスできる限り、認証後、ユーザー名とパスワード認証でアクセスできる最初のインターフェース。
(2)2番目の/admin
インターフェースは、ユーザー名とパスワードの認証後にアクセスできます。ユーザーが認証済みによって自動的にログに記録されている場合、インターフェースにアクセスするにはユーザー名とパスワードを再入力する必要があります。
(3)/rememberme
アクセスする3番目のインターフェースは、自動ログイン認証を経由する必要があります。ユーザーがユーザー名/パスワード認証の場合、インターフェースにアクセスできません。
@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
インターフェースはrememberMe
アクセスする必要があります。
(2)/admin
は必須でありfullyAuthenticated
、fullyAuthenticated
とは異なりauthenticated
、fullyAuthenticated
自動ログインフォームを含まず、自動ログインフォームを含みauthenticated
ます。
(3)最後に残ったインターフェース(/ hello)がauthenticated
アクセスできます。
OK、構成が完了したら、テストを再開します