[解決済み] JAX-RSとJerseyでRESTトークンベースの認証を実装する方法
質問
Jerseyでトークンベースの認証を有効にする方法を探しています。私は、特定のフレームワークを使用しないようにしています。それは可能ですか?
私の計画は、ユーザーが私のウェブサービスにサインアップし、私のウェブサービスがトークンを生成してクライアントに送信し、クライアントがそれを保持することです。そして、クライアントは、各リクエストについて、ユーザー名とパスワードの代わりにトークンを送信します。
各リクエストにカスタムフィルタを使用しようと考えていて
@PreAuthorize("hasRole('ROLE')")
しかし、これではトークンが有効かどうかをチェックするために、データベースへのリクエストが大量に発生してしまうと思っただけです。
または、フィルタを作成せず、各リクエストでパラメータトークンを置く?そうすれば、各APIはまずトークンをチェックし、その後リソースを取得するために何かを実行することになります。
解決方法は?
トークンベースの認証の仕組み
トークンベース認証では、クライアントは ハードクレデンシャル (と呼ばれるデータ(ユーザー名やパスワードなど)と交換します。 トークン . 各リクエストに対して、クライアントはハードクレデンシャルを送信する代わりに、トークンをサーバーに送信し、認証と認可を実行します。
トークンに基づく認証方式は、一言で言えば、次のような手順で行われます。
- クライアントがサーバーに認証情報(ユーザー名とパスワード)を送信する。
- サーバーは資格情報を認証し、それが有効であれば、ユーザー用のトークンを生成する。
- サーバーは、先に生成されたトークンをユーザー識別子と有効期限とともに、何らかのストレージに保存する。
- サーバーは、生成したトークンをクライアントに送信する。
- クライアントはリクエストごとにトークンをサーバーに送信します。
-
サーバーは、各リクエストで、受信したリクエストからトークンを抽出します。トークンを使って、サーバーはユーザーの詳細を調べ、認証を実行する。
- トークンが有効であれば、サーバーはリクエストを受け付けます。
- トークンが無効な場合、サーバーはリクエストを拒否します。
- 認証が行われると、サーバーは認可を実行する。
- サーバーはトークンをリフレッシュするためのエンドポイントを提供することができます。
JAX-RS 2.0(Jersey、RESTEasy、Apache CXF)でできること
このソリューションでは、JAX-RS 2.0 API のみを使用します。 ベンダ固有のソリューションを回避 . したがって、次のような JAX-RS 2.0 の実装で動作するはずです。 ジャージー , RESTEasy および アパッチCXF .
トークン・ベースの認証を使用する場合、サーブレットコンテナが提供する標準的な Java EE ウェブアプリケーション・セキュリティ機構に依存しないこと、およびアプリケーションの
web.xml
記述子を使用します。これはカスタム認証です。
ユーザー名とパスワードでユーザーを認証し、トークンを発行する
JAX-RSリソースメソッドを作成し、認証情報(ユーザー名とパスワード)を受け取って検証し、ユーザーのためにトークンを発行します。
@Path("/authentication")
public class AuthenticationEndpoint {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateUser(@FormParam("username") String username,
@FormParam("password") String password) {
try {
// Authenticate the user using the credentials provided
authenticate(username, password);
// Issue a token for the user
String token = issueToken(username);
// Return the token on the response
return Response.ok(token).build();
} catch (Exception e) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
private void authenticate(String username, String password) throws Exception {
// Authenticate against a database, LDAP, file or whatever
// Throw an Exception if the credentials are invalid
}
private String issueToken(String username) {
// Issue a token (can be a random String persisted to a database or a JWT token)
// The issued token must be associated to a user
// Return the issued token
}
}
資格情報を検証する際に例外が発生した場合は、ステータス
403
(Forbidden)が返されます。
認証情報の検証に成功した場合、レスポンスにステータス
200
(OK)が返され、発行されたトークンがレスポンス・ペイロードとしてクライアントに送信されます。クライアントは、リクエストのたびにトークンをサーバーに送信する必要があります。
を消費する場合
application/x-www-form-urlencoded
の場合、クライアントはリクエストペイロードに以下の形式でクレデンシャルを送信する必要があります。
username=admin&password=123456
フォームパラメータの代わりに、ユーザー名とパスワードをクラスにラップすることが可能です。
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
そして、それをJSONとして消費する。
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response authenticateUser(Credentials credentials) {
String username = credentials.getUsername();
String password = credentials.getPassword();
// Authenticate the user, issue a token and return a response
}
この方法を用いると、クライアントはリクエストのペイロードに以下の形式でクレデンシャルを送信する必要があります。
{
"username": "admin",
"password": "123456"
}
リクエストからトークンを抽出し、バリデートする
クライアントは、トークンを標準的な HTTP
Authorization
ヘッダを生成します。例えば
Authorization: Bearer <token-goes-here>
標準的なHTTPヘッダーの名前が残念なのは、そのヘッダーに 認証 情報であって オーソライゼーション . しかし、これはサーバーに認証情報を送信するための標準的なHTTPヘッダーです。
JAX-RSでは
@NameBinding
これは、フィルタやインターセプターをリソースクラスやメソッドにバインドするためのアノテーションを作成するために使用するメタアノテーションです。を定義します。
@Secured
アノテーションは、以下のようになります。
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }
上記で定義した名前バインディングのアノテーションを使用して、フィルタクラスを装飾します。
ContainerRequestFilter
これにより、リソースメソッドで処理される前に、リクエストをインターセプトすることができます。そのため
ContainerRequestContext
は、HTTPリクエストヘッダにアクセスし、トークンを抽出するために使用することができます。
@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String REALM = "example";
private static final String AUTHENTICATION_SCHEME = "Bearer";
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Validate the Authorization header
if (!isTokenBasedAuthentication(authorizationHeader)) {
abortWithUnauthorized(requestContext);
return;
}
// Extract the token from the Authorization header
String token = authorizationHeader
.substring(AUTHENTICATION_SCHEME.length()).trim();
try {
// Validate the token
validateToken(token);
} catch (Exception e) {
abortWithUnauthorized(requestContext);
}
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
// The authentication scheme comparison must be case-insensitive
return authorizationHeader != null && authorizationHeader.toLowerCase()
.startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
}
private void abortWithUnauthorized(ContainerRequestContext requestContext) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE,
AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
.build());
}
private void validateToken(String token) throws Exception {
// Check if the token was issued by the server and if it's not expired
// Throw an Exception if the token is invalid
}
}
トークンの検証中に何らかの問題が発生した場合、ステータスのあるレスポンス
401
(Unauthorized)が返されます。そうでなければ、リクエストはリソースメソッドに進みます。
RESTエンドポイントの安全性確保
リソースメソッドやリソースクラスに認証フィルタをバインドするには、リソースメソッドやリソースクラスに
@Secured
アノテーションを作成します。アノテーションされたメソッドやクラスに対して、フィルタリングが実行されます。つまり、そのようなエンドポイントでは
のみ
有効なトークンでリクエストを実行した場合に到達します。
認証が不要なメソッドやクラスは、アノテーションを付けないようにします。
@Path("/example")
public class ExampleResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myUnsecuredMethod(@PathParam("id") Long id) {
// This method is not annotated with @Secured
// The authentication filter won't be executed before invoking this method
...
}
@DELETE
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response mySecuredMethod(@PathParam("id") Long id) {
// This method is annotated with @Secured
// The authentication filter will be executed before invoking this method
// The HTTP request must be performed with a valid token
...
}
}
上に示した例では、フィルターが実行されます。
のみ
に対して
mySecuredMethod(Long)
メソッドでアノテーションされているからです。
@Secured
.
現在のユーザーを特定する
REST APIへのリクエストを実行したユーザーを知る必要がある場合が多々あります。そのためには、以下のような方法があります。
現在のリクエストのセキュリティコンテキストをオーバーライドする
の中で
ContainerRequestFilter.filter(ContainerRequestContext)
メソッドで、新しい
SecurityContext
のインスタンスを現在のリクエストに設定することができます。そして
SecurityContext.getUserPrincipal()
を返します。
Principal
のインスタンスを作成します。
final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return () -> username;
}
@Override
public boolean isUserInRole(String role) {
return true;
}
@Override
public boolean isSecure() {
return currentSecurityContext.isSecure();
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
トークンを使ってユーザー識別子 (ユーザー名) を調べ、それを
Principal
の名前です。
を注入します。
SecurityContext
を任意のJAX-RSリソースクラスで使用することができます。
@Context
SecurityContext securityContext;
JAX-RSのリソースメソッドでも同様のことが可能です。
@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id,
@Context SecurityContext securityContext) {
...
}
そして
Principal
:
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
CDI (Context and Dependency Injection) の使用法
もし、何らかの理由で
SecurityContext
を使えば、CDI (Context and Dependency Injection) が使えるので、イベントやプロデューサーなどの便利な機能が利用できます。
CDI修飾子を作成します。
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
あなたの
AuthenticationFilter
上記で作成した
Event
でアノテーションされた
@AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
認証に成功したら、ユーザー名をパラメータとしてイベントを発生させます(トークンはユーザーに対して発行され、トークンはユーザー識別子を調べるために使用されることを覚えておいてください)。
userAuthenticatedEvent.fire(username);
あなたのアプリケーションには、ユーザーを表すクラスが存在する可能性が高いです。このクラスを
User
.
認証イベントを処理する CDI Bean を作成し、その中で
User
インスタンスを作成し、そのインスタンスを
authenticatedUser
プロデューサー・フィールド
@RequestScoped
public class AuthenticatedUserProducer {
@Produces
@RequestScoped
@AuthenticatedUser
private User authenticatedUser;
public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
this.authenticatedUser = findUser(username);
}
private User findUser(String username) {
// Hit the the database or a service to find a user by its username and return it
// Return the User instance
}
}
は
authenticatedUser
フィールドは
User
このインスタンスは、JAX-RS サービスや CDI Bean、サーブレット、EJB などのコンテナ管理 Bean に注入することができます。以下のコード片を使用して
User
インスタンス (実際には CDI プロキシ) を作成します。
@Inject
@AuthenticatedUser
User authenticatedUser;
なお、CDI
@Produces
アノテーションは
異なる
JAX-RSから
@Produces
アノテーションを使用します。
-
CDIです。
javax.enterprise.inject.Produces
-
JAX-RSです。
javax.ws.rs.Produces
必ずCDIを使用する
@Produces
アノテーションを
AuthenticatedUserProducer
ビーンになります。
でアノテーションされたビーンがキーとなります。
@RequestScoped
これにより、フィルタとビーン間でデータを共有することができます。イベントを使いたくない場合は、フィルターを変更して認証済みユーザーをリクエストスコープのBeanに格納し、JAX-RSリソースクラスから読み取ることができます。
をオーバーライドするアプローチと比較すると
SecurityContext
のように、CDI のアプローチでは、JAX-RS のリソースやプロバイダ以外の Bean から認証ユーザを取得することができます。
ロールベースの認可をサポートする
私の他の 回答 は、ロールベースの認証をサポートする方法の詳細について説明します。
トークンの発行
トークンは、以下のようなものがあります。
- 不透明である。 値そのもの以外の詳細を明らかにしない(ランダムな文字列のような)。
- 自己完結している。 トークン自体の詳細が含まれている(JWTのような)。
詳細は下記をご覧ください。
ランダムな文字列をトークンとする
トークンは、ランダムな文字列を生成し、ユーザー識別子と有効期限とともにデータベースに保存することで発行することができる。Javaでランダムな文字列を生成する方法の良い例を以下に示します。 こちら . も使える。
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (JSONウェブトークン)
JWT(JSON Web Token)は、2者間で安全にクレームを表現するための標準的な手法で、以下のように定義されています。 RFC 7519 .
これは自己完結型のトークンであり、これによって詳細を クレーム . これらのクレームは、以下のようにエンコードされたJSONであるトークン・ペイロードに格納されます。 ベース64 . に登録されたクレームを以下に示します。 RFC 7519 とその意味(詳しくはRFCの全文をお読みください)。
-
iss
: トークンを発行したプリンシパル。 -
sub
: JWTのサブジェクトであるプリンシパル。 -
exp
: トークンの有効期限です。 -
nbf
: トークンの処理受付開始時刻。 -
iat
: トークンが発行された時刻。 -
jti
: トークンの一意な識別子。
トークンには、パスワードなどの機密データを保存しないように注意してください。
ペイロードはクライアントが読むことができ、トークンの完全性はサーバー上で署名を検証することで簡単に確認することができます。署名は、トークンが改ざんされるのを防ぐためのものです。
JWTトークンを追跡する必要がなければ、JWTトークンを永続化する必要はないでしょう。しかし、トークンを永続化することで、トークンのアクセスを無効化したり取り消したりできる可能性があります。JWT トークンの追跡を維持するには、トークン全体をサーバーに永続化する代わりに、トークン識別子 (
jti
クレーム) に加えて、トークンを発行したユーザや有効期限などの詳細が表示されます。
トークンを永続化する際には、データベースが際限なく大きくなるのを防ぐために、古いトークンを削除することを常に考慮してください。
JWTの使用
JWTトークンの発行や検証を行うためのJavaライブラリは、以下のようなものがあります。
JWTを使用するための他の素晴らしいリソースを見つけるには、以下を参照してください。 http://jwt.io .
JWTでトークン失効を処理する
トークンを失効させたい場合は、トークンを追跡する必要があります。トークン全体をサーバサイドに保存する必要はなく、トークン識別子 (これは一意でなければなりません) と必要なメタデータのみを保存すればよいのです。トークン識別子には、次のようなものを使用します。 UUID .
は
jti
のクレームは、トークンの識別子を保存するために使用されるべきです。トークンを検証する際には、トークンが失効していないことを
jti
は、サーバー側で持っているトークン識別子に対してクレームしてください。
セキュリティのため、ユーザーがパスワードを変更したら、そのユーザーのすべてのトークンを失効させる。
追加情報
- どのタイプの認証を使用するかは問題ではありません。 常に を防ぐために、HTTPS接続の先頭で実行します。 マンインザミドルアタック .
- をご覧ください。 この質問 トークンの詳細については、情報セキュリティのページをご覧ください。
- この記事で トークン・ベース認証について、いくつかの有用な情報を得ることができます。
関連
-
java.util.NoSuchElementException 原因解析と解決方法
-
springboot project MIMEタイプ text/htmlで転送された静的ファイルを読み込む。
-
java Mail send email smtp is not authenticated by TLS encryption solution.
-
eclipseにプロジェクトをインポートした後、Editorにmain typeが含まれない問題
-
Spring boot runs with Error creating bean with name 'entityManagerFactory' defined in class path resource
-
エラーの解決方法 jarfile XXX.jarにアクセスできません。
-
Google Chromeのエラー「Not allowed to load local resource」の解決策について
-
[解決済み] RESTを理解する。動詞、エラーコード、認証
-
[解決済み] JAX-RS / ジャージーエラー処理をカスタマイズする方法は?
-
[解決済み] ASP.NET Coreのトークンベース認証
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
スタイルが読み込まれず、ブラウザコンソールでエラーが報告される。リソースはスタイルシートとして解釈されますが、MIMEタイプtext/htmlで転送されます。
-
Java のエラーです。未解決のコンパイル問題 解決方法
-
Java Exceptionが発生しました エラー解決
-
JQuery DataTable 详解
-
Java コンパイルエラー - スレッド "main" で例外 java.lang.Error: 未解決のコンパイル問題です。
-
javaコンパイル時のエラー:不正な文字 '\ufeff' に対する解決策です。
-
linux run jarfile Invalid or corrupt jarfile error.
-
起動時にEclipseエラーが発生しました。起動中に内部エラーが発生しました。java.lang.NullPoin: "Javaツーリングの初期化 "中に内部エラーが発生しました。
-
maven プラグイン エラー プラグインの実行は、ライフサイクル構成ソリューションの対象外です。
-
[解決済み] 英数字のランダムな文字列を生成する方法