spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
ちょっと背景を説明しておきますが、 jwt.ioのご厚意による素晴らしい紹介文があります。見てみましょう:
JSON Web Token (JWT) は、関係者間で情報を JSON オブジェクトとして安全に送信するためのコンパクトで自己完結型の方法を定義するオープン スタンダード (RFC 7519) です。この情報はデジタル署名されているため、検証および信頼できます。 JWT は、シークレット (HMAC アルゴリズムを使用) または RSA を使用した公開/秘密キーのペアを使用して署名できます。
この定義のいくつかの概念をさらに説明しましょう。
コンパクト: JWT はサイズが小さいため、URL、POST パラメーター、または HTTP ヘッダー内を介して送信できます。さらに、サイズが小さいということは、送信が高速であることを意味します。
自己完結型: ペイロードにはユーザーに関する必要な情報がすべて含まれており、データベースに複数回クエリを実行する必要がなくなります。
JSON Web トークンが役立ついくつかのシナリオを次に示します。
認証: これは、JWT を使用する最も一般的なシナリオです。ユーザーがログインすると、後続の各リクエストには JWT が含まれるため、ユーザーはそのトークンで許可されているルート、サービス、リソースにアクセスできるようになります。シングル サインオンは、オーバーヘッドが小さく、さまざまなドメイン間で簡単に使用できるため、現在では JWT が広く使用されている機能です。
情報交換: JSON Web トークンは、当事者間で情報を安全に送信するための優れた方法です。 JWT は公開鍵と秘密鍵のペアなどを使用して署名できるため、送信者が本人であることを確認できます。さらに、署名はヘッダーとペイロードを使用して計算されるため、コンテンツが改ざんされていないことを検証することもできます。
JSON Web トークンは、ドット(.)で区切られた 3 つの部分で構成されます。
したがって、JWT は通常次のようになります。
xxxxx
。 yyyyy
。 zzzzz
さまざまな部分を分解してみましょう。
ヘッダ
通常、ヘッダーは 2 つの部分で構成されます。1 つはトークンのタイプ (JWT)、もう 1 つは使用されているハッシュ アルゴリズム (HMAC SHA256 や RSA など) です。
例えば:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
次に、この JSON は Base64Url でエンコードされて、JWT の最初の部分を形成します。
ペイロード
トークンの 2 番目の部分はペイロードであり、クレームが含まれています。クレームは、エンティティ (通常はユーザー) と追加のメタデータに関するステートメントです。クレームには、予約クレーム、パブリック クレーム、プライベート クレームの 3 種類があります。
JWT はコンパクトであることを目的としているため、クレーム名の長さは 3 文字のみであることに注意してください。
パブリック クレーム: これらは、JWT を使用するユーザーが自由に定義できます。ただし、衝突を回避するには、IANA JSON Web Token Registry で定義するか、衝突耐性のある名前空間を含む URI として定義する必要があります。
プライベート クレーム: これらは、その使用に同意した当事者間で情報を共有するために作成されたカスタム クレームです。
ペイロードの例は次のとおりです。
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
次に、ペイロードは Base64Url でエンコードされて、JSON Web トークンの 2 番目の部分を形成します。
サイン
署名部分を作成するには、エンコードされたヘッダー、エンコードされたペイロード、シークレット、ヘッダーで指定されたアルゴリズムを取得し、署名する必要があります。
たとえば、HMAC SHA256 アルゴリズムを使用する場合、署名は次の方法で作成されます。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
署名は、JWT の送信者が本人であることを確認し、メッセージが途中で変更されていないことを確認するために使用されます。すべてをまとめる
出力は、ドットで区切られた 3 つの Base64 文字列であり、HTML および HTTP 環境で簡単に渡すことができ、SAML などの XML ベースの標準と比較するとコンパクトです。
以下は、前のヘッダーとペイロードがエンコードされ、シークレットで署名された JWT を示しています。エンコードされた JWT
認証では、ユーザーが資格情報を使用してログインに成功すると、JSON Web トークンが返され、これをローカル (通常はローカル ストレージに保存されますが、Cookie も使用できます) に保存する必要があります。サーバーにアクセスして Cookie を返します。
ユーザーが保護されたルートまたはリソースにアクセスしたいときは常に、ユーザー エージェントは、通常は Bearer スキーマを使用して Authorization ヘッダーで JWT を送信する必要があります。ヘッダーの内容は次のようになります。
Authorization: Bearer <token>
ユーザーの状態はサーバーのメモリに保存されないため、これはステートレスな認証メカニズムです。サーバーの保護されたルートは、Authorization ヘッダー内の有効な JWT をチェックし、それが存在する場合、ユーザーは保護されたリソースへのアクセスを許可されます。 JWT は自己完結型であるため、必要な情報がすべてそこにあり、データベースに複数回クエリを実行する必要性が軽減されます。
これにより、ステートレスなデータ API に完全に依存し、ダウンストリーム サービスにリクエストを行うこともできます。どのドメインが API を提供しているかは問題ではないため、Cross-Origin Resource Sharing (CORS) は Cookie を使用しないため問題になりません。
次の図は、このプロセスを示しています。
トークン ベースの認証スキーマは、セッション/Cookie と比較して重要な利点を提供するため、最近非常に人気があります。
このアプローチでは、いくつかのトレードオフを行う必要があります。
JWT 認証フローは非常にシンプルです
承認クレームがアクセス トークンに含まれることに注意することが重要です。これがなぜ重要なのでしょうか?さて、アクセス トークンの有効期間中に承認クレーム (データベース内のユーザー権限など) が変更されたとします。これらの変更は、新しいアクセス トークンが発行されるまで有効になりません。アクセス トークンの有効期間は短いため、ほとんどの場合、これは大きな問題ではありません。それ以外の場合は、不透明なトークン パターンを使用します。
可能な限り Spring セキュリティのデフォルト動作を再利用しながら、Java と Spring を使用して JWT トークンベースの認証を実装する方法を見てみましょう。 Spring Security フレームワークには、セッション Cookie、HTTP Basic、HTTP Digest などの承認メカニズムをすでに処理するプラグイン クラスが付属しています。それにもかかわらず、JWT のネイティブ サポートが不足しているため、機能させるには手を汚す必要があります。
このデモは現在、 test_dbという H2 データベースを使用しているため、多くの構成を必要とせずにすぐにすぐに実行できます。別のデータベースに接続する場合は、リソース ディレクトリ内のapplication.yml
ファイルで接続を指定する必要があります。 hibernate.hbm2ddl.auto=create-drop
、デプロイするたびに削除してクリーンなデータベースを作成することに注意してください (これを実際のプロジェクトで使用している場合は、変更することをお勧めします)。以下はプロジェクトの例です。独自の MySQL データベースを使用するために、 url
とdialect
プロパティのコメントを簡単に交換できることを確認してください。
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtTokenFilter
JwtTokenFilter
フィルターは、サインイン トークン エンドポイント ( /users/signin
) とシングアップ エンドポイント ( /users/signup
) を除く各 API ( /**
) に適用されます。
このフィルターには次の役割があります。
JwtTokenProvider
に委任します。それ以外の場合は、認証例外をスローします。認証が成功した場合には、 chain.doFilter(request, response)
が呼び出されることを確認してください。最後のフィルターFilterSecurityInterceptor#doFilter が、リクエストされた API リソースを処理するコントローラー内のメソッドを実際に呼び出す役割を担うため、リクエストの処理を次のフィルターに進める必要があります。
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
JwtTokenFilterConfigurer
JwtTokenFilter
スプリング ブート セキュリティのDefaultSecurityFilterChain
に追加します。
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
Jwtトークンプロバイダー
JwtTokenProvider
には次の役割があります。
私のユーザーの詳細
独自のカスタムloadUserbyUsername関数を定義するためにUserDetailsService
を実装します。 UserDetailsService
インターフェイスは、ユーザー関連のデータを取得するために使用されます。これには、ユーザー名に基づいてユーザー エンティティを検索する、 loadUserByUsernameという名前のメソッドが 1 つあり、ユーザーを検索するプロセスをカスタマイズするためにオーバーライドできます。
これは、認証中にユーザーに関する詳細をロードするためにDaoAuthenticationProvider
によって使用されます。
Webセキュリティ構成
WebSecurityConfig
クラスはWebSecurityConfigurerAdapter
を拡張して、カスタム セキュリティ構成を提供します。
このクラスでは、次の Bean が構成され、インスタンス化されます。
JwtTokenFilter
PasswordEncoder
また、 WebSecurityConfig#configure(HttpSecurity http)
メソッド内で、保護された/保護されていない API エンドポイントを定義するパターンを構成します。 Cookie を使用していないため、CSRF 保護が無効になっていることに注意してください。
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
Java 8 と Maven がインストールされていることを確認してください
このリポジトリをフォークしてクローンを作成します
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
に移動し、すべてが正しく動作していることを確認します。 application.yml
ファイルでデフォルトのポートを変更できます。 server :
port : 8080
/users/me
に GET リクエストを送信して、認証されていないことを確認します。有効な JWT トークンがまだ設定されていないため、 Access Denied
メッセージを含む403
の応答を受け取るはずです。 $ curl -X GET http://localhost:8080/users/me
/users/signin
に POST リクエストを送信します。 $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
に再度実行します。 $ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}