ほとんどのWebサイトでは、ユーザーがシステムにログインするためにユーザー名とパスワードを入力しようとすると、「ログインしたままにする」などのチェックボックスがあります。チェックボックスをオンにすると、同じWebブラウザからのすべてのセッションでログインしたままになります。 Java EEで同じを実装するにはどうすればよいですか?
JSFログインページでFORMベースのコンテナー管理認証を使用しています。
<security-constraint>
<display-name>Student</display-name>
<web-resource-collection>
<web-resource-name>CentralFeed</web-resource-name>
<description/>
<url-pattern>/CentralFeed.jsf</url-pattern>
</web-resource-collection>
<auth-constraint>
<description/>
<role-name>STUDENT</role-name>
<role-name>ADMINISTRATOR</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>jdbc-realm-scholar</realm-name>
<form-login-config>
<form-login-page>/index.jsf</form-login-page>
<form-error-page>/LoginError.jsf</form-error-page>
</form-login-config>
</login-config>
<security-role>
<description>Admin who has ultimate power over everything</description>
<role-name>ADMINISTRATOR</role-name>
</security-role>
<security-role>
<description>Participants of the social networking Bridgeye.com</description>
<role-name>STUDENT</role-name>
</security-role>
Java EE 8以降)を使用している場合は、カスタム @RememberMe
を HttpAuthenticationMechanism
とともに- RememberMeIdentityStore
。
@ApplicationScoped
@AutoApplySession
@RememberMe
public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {
@Inject
private IdentityStore identityStore;
@Override
public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext context) {
Credential credential = context.getAuthParameters().getCredential();
if (credential != null) {
return context.notifyContainerAboutLogin(identityStore.validate(credential));
}
else {
return context.doNothing();
}
}
}
public class CustomIdentityStore implements RememberMeIdentityStore {
@Inject
private UserService userService; // This is your own EJB.
@Inject
private LoginTokenService loginTokenService; // This is your own EJB.
@Override
public CredentialValidationResult validate(RememberMeCredential credential) {
Optional<User> user = userService.findByLoginToken(credential.getToken());
if (user.isPresent()) {
return new CredentialValidationResult(new CallerPrincipal(user.getEmail()));
}
else {
return CredentialValidationResult.INVALID_RESULT;
}
}
@Override
public String generateLoginToken(CallerPrincipal callerPrincipal, Set<String> groups) {
return loginTokenService.generateLoginToken(callerPrincipal.getName());
}
@Override
public void removeLoginToken(String token) {
loginTokenService.removeLoginToken(token);
}
}
実世界の例 は Java EEキックオフアプリケーション にあります。
Java EE 6または7を使用している場合は、固有のクライアントを追跡し、サーブレット3.0 APIを使用してプログラムによるログインを使用するために、長生きするCookieを育てます HttpServletRequest#login()
ユーザーがログインしていないが、Cookieが存在する場合。
これは、PKとしてJava.util.UUID
値を使用し、FKとして問題のユーザーのIDを持つ別のDBテーブルを作成する場合に最も簡単に実現できます。
次のログインフォームを想定します。
<form action="login" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="checkbox" name="remember" value="true" />
<input type="submit" />
</form>
そして、/login
にマッピングされるServlet
のdoPost()
メソッドに以下を追加します。
String username = request.getParameter("username");
String password = hash(request.getParameter("password"));
boolean remember = "true".equals(request.getParameter("remember"));
User user = userService.find(username, password);
if (user != null) {
request.login(user.getUsername(), user.getPassword()); // Password should already be the hashed variant.
request.getSession().setAttribute("user", user);
if (remember) {
String uuid = UUID.randomUUID().toString();
rememberMeService.save(uuid, user);
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE);
} else {
rememberMeService.delete(user);
removeCookie(response, COOKIE_NAME);
}
}
(COOKIE_NAME
は一意のCookie名、たとえば"remember"
で、COOKIE_AGE
は秒単位の年齢、たとえば2592000
は30日間)
制限されたページにマッピングされるFilter
のdoFilter()
メソッドは次のようになります。
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
User user = request.getSession().getAttribute("user");
if (user == null) {
String uuid = getCookieValue(request, COOKIE_NAME);
if (uuid != null) {
user = rememberMeService.find(uuid);
if (user != null) {
request.login(user.getUsername(), user.getPassword());
request.getSession().setAttribute("user", user); // Login.
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
} else {
removeCookie(response, COOKIE_NAME);
}
}
}
if (user == null) {
response.sendRedirect("login");
} else {
chain.doFilter(req, res);
}
これらのCookieヘルパーメソッドと組み合わせて(サーブレットAPIに不足しているのは残念です):
public static String getCookieValue(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
public static void removeCookie(HttpServletResponse response, String name) {
addCookie(response, name, null, 0);
}
UUID
はブルートフォースするのが非常に困難ですが、ユーザーに「remember」オプションをユーザーのIPアドレス(request.getRemoteAddr()
)にロックして、データベースも同様です。これにより、少し堅牢になります。また、データベースに「有効期限」を保存しておくと便利です。
また、ユーザーがパスワードを変更した場合は、UUID
値を置き換えることをお勧めします。
アップグレードしてください。
通常、これは次のように行われます。
ユーザーにログインするとき、特定の時間(通常は1〜2週間)後に期限が切れるCookieをクライアントに設定(およびCookie値をデータベースに保存)します。
新しいリクエストが来たら、特定のCookieが存在することを確認し、存在する場合はデータベースを調べて、特定のアカウントと一致するかどうかを確認します。一致する場合、そのアカウントに「緩やかに」ログインします。大まかに言うと、そのセッションに情報を読み取らせるだけで、情報は書き込まないということです。書き込みオプションを許可するには、パスワードを要求する必要があります。
これがすべてです。秘Theは、「ゆるい」ログインがクライアントに大きな害を及ぼさないようにすることです。これにより、remember me cookieを取得してログインしようとするユーザーからユーザーをある程度保護できます。
HttpServletRequest.login(username、password)を使用してユーザーに完全にログインすることはできません。データベースにユーザー名とプレーンテキストパスワードの両方を保持するべきではないためです。また、データベースに保存されているパスワードハッシュを使用してこのログインを実行することもできません。ただし、GlassfishサーバーAPIに基づくカスタムログインモジュール(Javaクラス)を使用して、Cookie/DBトークンでユーザーを識別し、パスワードを入力せずにログインする必要があります。
詳細については、次のリンクを参照してください。
http://www.lucubratory.eu/custom-jaas-realm-for-glassfish-3/
BalusCによる答え(Java EE 6/7)は有用なヒントを提供しますが、ログインフィルターをページにマッピングできないため、現代のコンテナーでは動作しません。標準的な方法で保護されています(コメントで確認)。
何らかの理由でSpring Securityを使用できない場合(互換性のない方法でServlet Securityを再実装する場合)、_<auth-method>FORM
_のままにしてすべてのロジックをアクティブなログインページに配置することをお勧めします。
コードは次のとおりです(完全なプロジェクトはこちら: https://github.com/basinilya/rememberme )
web.xml:
_ <form-login-config>
<form-login-page>/login.jsp</form-login-page>
<form-error-page>/login.jsp?error=1</form-error-page>
</form-login-config>
_
login.jsp:
_if ("1".equals(request.getParameter("error"))) {
request.setAttribute("login_error", true);
} else {
// The initial render of the login page
String uuid;
String username;
// Form fields have priority over the persistent cookie
username = request.getParameter("j_username");
if (!isBlank(username)) {
String password = request.getParameter("j_password");
// set the cookie even though login may fail
// Will delete it later
if ("on".equals(request.getParameter("remember_me"))) {
uuid = UUID.randomUUID().toString();
addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
Map.Entry<String,String> creds =
new AbstractMap.SimpleEntry<String,String>(username,password);
rememberMeServiceSave(request, uuid, creds);
}
if (jSecurityCheck(request, response, username, password)) {
return;
}
request.setAttribute("login_error", true);
}
uuid = getCookieValue(request, COOKIE_NAME);
if (uuid != null) {
Map.Entry<String,String> creds = rememberMeServiceFind(request, uuid);
if (creds != null) {
username = creds.getKey();
String password = creds.getValue();
if (jSecurityCheck(request, response, username, password)) {
return; // going to redirect here again if login error
}
request.setAttribute("login_error", true);
}
}
}
// login failed
removeCookie(response, COOKIE_NAME);
// continue rendering the login page...
_
ここにいくつかの説明があります:
request.login()
を呼び出す代わりに、HTTPリスナーへの新しいTCP接続を確立し、ログインフォームを_/j_security_check
_アドレスに送信します。これにより、コンテナーはリダイレクトできます最初に要求されたWebページに移動し、POSTデータ(存在する場合)。セッション属性または_RequestDispatcher.FORWARD_SERVLET_PATH
_からこの情報を取得しようとすると、コンテナ固有になります。
コンテナはフィルタに到達する前にログインページに転送/リダイレクトされるため、自動ログインにはサーブレットフィルタを使用しません。
動的ログインページは、以下を含むすべての仕事を行います
/j_security_check
_を呼び出す「Stay Logged In」機能を実装するために、送信されたログインフォームからの資格情報をサーブレットコンテキスト属性に保存します(今のところ)。上記のSOの回答とは異なり、パスワードはハッシュされません。特定のセットアップだけがそれを受け入れます(Gdbsfish with jdbcレルム)。永続的なCookieは資格情報に関連付けられています。
フローは次のとおりです。
<form-error-page>
_として提供されている場合は、フォームとエラーメッセージをレンダリングします/j_security_check
_を呼び出して結果にリダイレクトします(これもまた可能性があります)/j_security_check
_に進みます_/j_security_check
_のコードは、現在のJSESSIONID
Cookieと、実際のフォームからの、または永続的なCookieに関連付けられた資格情報を使用してPOST=リクエストを送信します。