トークンベースの認証の仕組み
トークンベースの認証では、クライアントはトークンと呼ばれるデータの一部のハード認証情報(ユーザー名やパスワードなど)を交換します。リクエストごとに、クライアントはハード認証情報を送信する代わりに、トークンをサーバーに送信して、認証と承認を実行します。
簡単に言うと、トークンに基づく認証スキームは次の手順に従います。
- クライアントは資格情報(ユーザー名とパスワード)をサーバーに送信します。
- サーバーは資格情報を認証し、資格情報が有効な場合は、ユーザーのトークンを生成します。
- サーバーは、以前に生成されたトークンをユーザーIDと有効期限と共に何らかのストレージに保存します。
- サーバーは、生成されたトークンをクライアントに送信します。
- クライアントは各リクエストでトークンをサーバーに送信します。
- サーバーは、各要求で、着信要求からトークンを抽出します。トークンを使用して、サーバーはユーザーの詳細を検索して認証を実行します。
- トークンが有効な場合、サーバーは要求を受け入れます。
- トークンが無効な場合、サーバーは要求を拒否します。
- 認証が実行されると、サーバーは承認を実行します。
- サーバーは、トークンを更新するエンドポイントを提供できます。
注:サーバーが署名済みトークン(ステートレス認証を実行できるJWTなど)を発行した場合、ステップ3は不要です。
JAX-RS 2.0(Jersey、RESTEasy、Apache CXF)でできること
このソリューションはJAX-RS 2.0 APIのみを使用し、ベンダー固有のソリューションを回避します。したがって、Jersey、RESTEasy、Apache CXFなどのJAX-RS 2.0実装で動作するはずです。
トークンベースの認証を使用している場合は、サーブレットコンテナによって提供され、アプリケーションのweb.xml
記述子を介して構成可能な標準のJava EE Webアプリケーションセキュリティメカニズムに依存していないことに言及することは価値があります。これはカスタム認証です。
ユーザー名とパスワードを使用してユーザーを認証し、トークンを発行する
資格情報(ユーザー名とパスワード)を受け取って検証し、ユーザーにトークンを発行する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
(禁止)の応答が返されます。
資格情報が正常に検証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"
}
リクエストからトークンを抽出して検証する
クライアントはAuthorization
、リクエストの標準HTTP ヘッダーでトークンを送信する必要があります。例えば:
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
(未承認)の応答が返されます。それ以外の場合、リクエストはリソースメソッドに進みます。
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
の名前になるユーザー識別子(username)を検索します。
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;
認証が成功した場合、パラメーターとしてユーザー名を渡してイベントを発生させます(ユーザーに対してトークンが発行され、トークンはユーザーIDの検索に使用されます)。
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
。
必ずBeanでCDI @Produces
アノテーションを使用してくださいAuthenticatedUserProducer
。
ここで重要なのは、アノテーションが付けられたBeanです。これにより@RequestScoped
、フィルターとBeanの間でデータを共有できます。イベントを使用したくない場合は、フィルターを変更して、認証されたユーザーをリクエストスコープのBeanに格納し、JAX-RSリソースクラスから読み取ることができます。
をオーバーライドするアプローチと比較してSecurityContext
、CDIアプローチでは、JAX-RSリソースおよびプロバイダー以外のBeanから認証済みユーザーを取得できます。
役割ベースの許可のサポート
ロールベースの承認をサポートする方法の詳細については、他の回答を参照してください。
トークンの発行
トークンには次のものがあります。
- 不透明:値自体(ランダムな文字列など)以外の詳細を明らかにしない
- 自己完結型:トークン自体に関する詳細(JWTなど)が含まれています。
以下の詳細を参照してください。
トークンとしてのランダムな文字列
トークンは、ランダムな文字列を生成し、ユーザー識別子と有効期限と共にデータベースに永続化することで発行できます。Javaでランダムな文字列を生成する方法の良い例は、こちらにあります。あなたも使うことができます:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT(JSON Webトークン)
JWT(JSON Web Token)は、2つのパーティ間でクレームを安全に表すための標準的な方法であり、RFC 7519で定義されています。
これは自己完結型のトークンであり、クレームに詳細を格納できます。これらのクレームは、Base64としてエンコードされたJSONであるトークンペイロードに格納されます。RFC 7519に登録されているいくつかのクレームとその意味(詳細については、完全なRFCをお読みください):
iss
:トークンを発行したプリンシパル。
sub
:JWTの主体であるプリンシパル。
exp
:トークンの有効期限。
nbf
:トークンが処理のために受け入れられ始める時間。
iat
:トークンが発行された時刻。
jti
:トークンの一意の識別子。
パスワードなどの機密データをトークンに保存してはいけないことに注意してください。
ペイロードはクライアントで読み取ることができ、トークンの整合性はサーバーで署名を確認することで簡単に確認できます。署名は、トークンが改ざんされないようにするものです。
追跡する必要がない場合は、JWTトークンを永続化する必要はありません。とはいえ、トークンを永続化することにより、トークンへのアクセスを無効にして取り消す可能性があります。サーバー上でトークン全体を永続化する代わりに、JWTトークンを追跡するために、トークン識別子(jti
クレーム)を、トークンを発行したユーザー、有効期限などの他の詳細とともに永続化できます。
トークンを永続化する場合は、データベースが無制限に大きくなるのを防ぐために、常に古いトークンを削除することを検討してください。
JWTの使用
次のようなJWTトークンを発行および検証するJavaライブラリがいくつかあります。
JWTを操作するためのその他の優れたリソースを見つけるには、http://jwt.ioを参照してください。
JWTによるトークン失効の処理
トークンを取り消す場合は、トークンを追跡する必要があります。トークン全体をサーバー側に保存する必要はありません。トークン識別子(一意である必要があります)と、必要に応じて一部のメタデータのみを保存します。トークン識別子にはUUIDを使用できます。
jti
前記トークンにトークン識別子を格納するために使用されるべきです。トークンを検証するときjti
は、サーバー側にあるトークン識別子に対してクレームの値をチェックして、トークンが取り消されていないことを確認してください。
セキュリティ上の理由から、ユーザーがパスワードを変更したときに、ユーザーのすべてのトークンを取り消します。
追加情報
- どのタイプの認証を使用するかは関係ありません。中間者攻撃を防ぐために、常に HTTPS接続の上で実行してください。
- 見てみましょう、この質問のトークンの詳細については、情報セキュリティのを。
- この記事では、トークンベースの認証に関する役立つ情報を紹介します。
The server stores the previously generated token in some storage along with the user identifier and an expiration date. The server sends the generated token to the client.
このRESTfulはどうですか?