1. JWT はステートレスな Web サービスを実装します
1. ステートフルとは
ステートフル サービス、つまり、サーバーは、クライアントの ID を識別し、ユーザーの ID に基づいてリクエストを処理するために、各セッションのクライアント情報を記録する必要があります。典型的な設計は、Tomcat のセッションなどです。
たとえば、ログイン: ユーザーがログインした後、ログイン情報をサーバー セッションに保存し、対応するセッションを記録するための Cookie 値をユーザーに与えます。次のリクエストで、ユーザーが Cookie 値を持ってくると、対応するセッションを識別してユーザーの情報を見つけることができます。
デメリットは何ですか?
- サーバーは大量のデータを保存するため、サーバーへの負荷が増大します。
- サーバーはユーザーのステータスを保存し、水平方向に拡張することはできません。
- クライアントのリクエストはサーバーに依存し、複数のリクエストは同じサーバーにアクセスする必要があります。
2. ステートレスとは何ですか?
サーバーはクライアントのステータス情報、つまり次の情報を記録する必要はありません。
- サーバーはクライアントのリクエスタ情報を保存しません
- クライアントからの各リクエストには、クライアントの ID を識別できる自己記述情報が必要です。
メリットは何ですか?
- クライアントリクエストはサーバー情報に依存せず、複数のリクエストが同じサービスにアクセスする必要はありません。
- サーバーのクラスターとステータスはクライアントに対して透過的です
- サーバーは任意に移行および拡張可能
- サーバーストレージの負荷を軽減
3. 無国籍を実現する方法
ステートレス ログイン プロセス:
- クライアントが初めてサービスを要求するとき、サーバーはユーザーの情報を認証します(ログイン)
- 認証に合格すると、ユーザー情報が暗号化されてトークンが形成され、ログイン資格情報としてクライアントに返されます。
- 後続のリクエストごとに、クライアントは認証トークンを保持します。
- サービスはトークンを復号化し、それが有効かどうかを判断します。
フローチャート:
クライアントはログインを要求し、ログイン後に資格情報が発行されます。
ログインプロセス全体の中で最も重要なポイントは何ですか?
トークンのセキュリティ
トークンはクライアントを識別する唯一の識別子であり、暗号化が不十分で偽造されたら終わりです。
安全で信頼できる暗号化方式は何ですか?
JWT + RSA 非対称暗号化を使用します。
4. JWT の概要
JWTの正式名称はJson Web Tokenで、ステートレスかつ分散型のWebアプリケーション認可を実現できるJSON形式の軽量認可・本人認証仕様です、公式サイト:https://jwt.io
JWT にはデータの 3 つの部分が含まれています。
-
ヘッダー: ヘッダー。通常、ヘッダーには 2 つの情報部分があります。
- 宣言タイプ。これは JWT の自己記述情報です
ヘッダーをbase64エンコードして、データベースの最初の部分をbase64でエンコードおよびデコードします。
-
ペイロード: ペイロードは有効なデータであり、通常は次の情報が含まれます。
- ユーザー ID 情報 (ここでは Base64 エンコードが使用されているため、デコードは元に戻すことができるため、機密情報は保存しないでください)
- 登録ステートメント: トークンの発行時刻、有効期限、発行者など。コンテンツのこの部分は ID カードの情報に似ています。
この部分も、データの 2 番目の部分を取得するために、base64 を使用してエンコードされます。
-
署名: 署名はデータ全体の認証情報です。一般に、最初の 2 つのステップのデータと秘密 (漏洩しないように、定期的に変更するのが最善です) に基づいて、暗号化アルゴリズム (不可逆的) によって署名が生成されます。データ全体の完全性と信頼性を検証するために使用されます。
生成されるデータ形式:
3 つのセグメントに分割されていることがわかります。各セグメントは上記のデータの一部です。
5. JWT対話プロセス
ステップ翻訳:
- 1. ユーザーログイン
- 2.サービス認証、通過後にjwtが生成される
- 3. 生成された jwt をブラウザに返します
- 4. ユーザーはリクエストするたびに jwt を実行します
- 5. サーバーは公開鍵を使用して JWT 署名を解釈し、署名が有効であると判断した後、ペイロードからユーザー情報を取得します。
- 6. リクエストを処理し、レスポンス結果を返す
2. nimbus-jose-jwt ライブラリ
1. 依存関係を入力します
nimbus-jose-jwt、jose4j、および java-jwt は、Java で JWT を操作するためのいくつかの一般的なライブラリです。
nimbus-jose-jwt 公式ウェブサイト: https://connect2id.com/products/nimbus-jose-jwt
必要な座標
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.11.1</version>
</dependency>
2.コアAPI
2.1. 暗号化プロセス
-
nimbus-jose-jwt では、JWT の先頭を表すために Header クラスが使用されますが、Header クラスは抽象クラスであるため、そのサブクラスJWSHeaderを使用します。
ヘッダー オブジェクトを作成します。
@Test public void createToken(){ //创建头部对象 JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) // 加密算法 .type(JOSEObjectType.JWT) // 静态常量 .build(); System.out.println(jwsHeader); }
.toBase64URL()
次のメソッドを通じて、Base64 形式のヘッダー情報 (JWT 内の実際のヘッダー情報でもあります) を取得できます。 -
Payloadクラスを使用してJWT のペイロード部分を表現します。
ロードセクションオブジェクトを作成します。
@Test public void createToken(){ //创建头部对象 JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) // 加密算法 .type(JOSEObjectType.JWT) // 静态常量 .build(); System.out.println(jwsHeader); //创建载荷 Payload payload = new Payload("hello world"); System.out.println(payload); }
.toBase64URL()
次のメソッドを通じて、Base64 形式のペイロード情報を取得できます (これは、JWT 内の実際のペイロード情報でもあります) 。 -
署名部分
署名部分には特別なクラス表現はなく、自分で作成するのではなく、によって
头部 + 荷载部 + 加密算法
計算されます。nimbus-jose-jwt は、署名プロセスに参加するための署名者JWSSigner を特別に提供します。キーは署名者の作成時に指定されます。
JWSSigner jwsSigner = new MACSigner("Key"); //MACSigner() でキーを指定する必要があります
最终,整个 JWT 由一个 **JWSObject** 对象表示:
```java
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 进行签名(根据前两部分生成第三部分)
jwsObject.sign(jwsSigner);
最終的に必要なのはオブジェクトではなく JWT 文字列です。ここで、.serialize()
JWT を表す JWSObject オブジェクトのメソッドを呼び出すことができます。
String token = jwsObject.serialize();
完全な例:
@Test
public void createToken() throws JOSEException {
//创建头部对象
JWSHeader jwsHeader =
new JWSHeader.Builder(JWSAlgorithm.HS256) // 加密算法
.type(JOSEObjectType.JWT) // 静态常量
.build();
//创建载荷
Payload payload = new Payload("hello world");
//创建签名器
JWSSigner jwsSigner = new MACSigner("woniu");//woniu为密钥
//创建签名
JWSObject jwsObject = new JWSObject(jwsHeader, payload);// 头部+载荷
jwsObject.sign(jwsSigner);//再+签名部分
//生成token字符串
String token = jwsObject.serialize();
System.out.println(token);
}
例外が発生した場合: com.nimbusds.jose.KeyLengthException: 秘密の長さは少なくとも 256 ビットである必要があります。キーの長さが十分でないためです。キーの長さを増やしてください。
2.2. 復号化
逆復号化および検証プロセス用のコア API は 2 つだけです。JWSObject の静的メソッドの解析メソッドと、その JWSVerifier オブジェクトの検証です。
JWSObject オブジェクトの有効性を直接検証したい場合は、JWSVerifierオブジェクトを作成する必要があります。
//创建验证器
JWSVerifier jwsVerifier = new MACVerifier("密钥");//密钥要和加密时的相同
次に、 jwsObject オブジェクトのverifyメソッドを直接呼び出します。
if (!jwsObject.verify(jwsVerifier)) {
throw new RuntimeException("token 签名不合法!");
}
3. トークンの更新
実際の開発では、トークンが常に有効であるとは限りません。たとえば、30 分以内に何も操作しないと認証が切れて、再度ログインする必要があります。アクセスを要求していた場合、トークンは最後まで有効です。次の訪問までに 30 秒以上の時間があり、時間が 30 分を超えると、認定は期限切れになります。
springsecurity は JWT を統合します。
@Component
public class JWTfilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired(required = false)
private SecurityLoginService securityLoginService;
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain)
throws ServletException, IOException {
//功能点1:在请求头拿到jwt
String jwt = httpServletRequest.getHeader("jwt");
if (jwt == null) {
//放给security 其他过滤器,该方法不做处理
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
// 功能点2:jwt不合法
if (!JWTUtil.decode(jwt)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//功能点3 获取jwt的用户信息
Map payLoad = JWTUtil.getPayload(jwt);
String username = (String) payLoad.get("username");
//拿到redis的jwt
String redisJWT = redisTemplate.opsForValue().get("jwt:" + username);
//判断redis是否有该jwt
if (redisJWT == null) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
if (!jwt.equals(redisJWT)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//给redis 的jwt续期
redisTemplate.opsForValue().set("jwt:" + username, jwt, 30,
TimeUnit.MINUTES);
//获取用户名,密码,权限
UserDetails userDetails = securityLoginService.loadUserByUsername(username);
// 获取用户信息 生成security容器凭证
UsernamePasswordAuthenticationToken upa =
new UsernamePasswordAuthenticationToken(userDetails.getUsername()
, userDetails.getPassword(), userDetails.getAuthorities());
//放入凭证
SecurityContextHolder.getContext().setAuthentication(upa);
// 本方法共功能执行完了,交给下一个过滤器
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
//前后端项目中要禁用掉session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
在securityConfig 类注入
http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);