[PicketLink] Ajax対応サンプル

これまでの記事で、PicketLinkで提供されているサンプルを紹介してきましたが、今回はオリジナルのサンプルを作成してみました。
今回のサンプルでは、PrimeFacesを使用しています。
そして、セッションがタイムアウトした場合に、Ajaxのフォームが無反応にならないような対策も実装しました。
コードは、GitHubに置きました。
https://github.com/subsonicsystems/picketlink-ajax-example

実行環境
PicketLink 2.7.0.Final
PrimeFaces 5.2
WildFly 8.2.0.Final

warファイルの内容は次の通りです。
picketlink-ajax-example.war
picketlink-ajax-example-war

/WEB-INF/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
        bean-discovery-mode="all">
</beans>

CDIのバージョンは1.1です。
beans.xmlは、空のXMLです。bean-discovery-modeをannotatedにすると認証が動作しなくなるので注意が必要です。

/WEB-INF/faces-config.xml

<?xml version="1.0"?>
<faces-config version="2.2"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
    <application>
        <locale-config>
            <default-locale>en</default-locale>
            <supported-locale>ja</supported-locale>
        </locale-config>
        <resource-bundle>
            <base-name>resources.message</base-name>
            <var>msg</var>
        </resource-bundle>
    </application>
</faces-config>

JSFのバージョンは、2.2です。
<locale-config>で、JSFのローカライズ設定しています。ここでは、英語と日本語を設定して、英語をデフォルトにしています。
<resource-bundle>は、リソースファイルの設定を行います。ここでは、<base-name>でresources.messageを設定しているので、/WEB-INF/classes/resources/message_xx.propertiesファイルを参照します。xxは、<locale-config>で指定したenとjaとなります。また、拡張子は設定ファイルに登場していませんが、.propertiesとなります。
<var>はEL式で参照する際の変数名を定義します。ここでは、msgとしているので、例えば、xhtmlファイルの中で、#{msg[‘index.title’]}という書き方でリソースを呼び出せます。

/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
    <context-param>
        <param-name>javax.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONE</param-name>
        <param-value>true</param-value>
    </context-param>
    <context-param>
        <param-name>primefaces.THEME</param-name>
        <param-value>bootstrap</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>60</session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

サーブレットのバージョンは、3.1です。
1つ目の<context-param>のjavax.faces.DATETIMECONVERTER_DEFAULT_TIMEZONE_IS_SYSTEM_TIMEZONEは、JSFで時刻を取得する際のタイムゾーンを設定しています。trueの場合、システムのタイムゾーンを返します。falseの場合、UTCを返します。
2つ目の<context-param>のprimefaces.THEMEは、PrimeFacesのテーマ(デザイン)を設定します。bootstrapというのは、テーマ名のひとつです。

SecurityInitializer.java

@Singleton
@Startup
public class SecurityInitializer {

	/**
	 * The PartitionManager.
	 */
	@Inject
	private PartitionManager partitionManager;

	/**
	 * Creates User.
	 */
	@PostConstruct
	public void create() {
		IdentityManager identityManager = this.partitionManager
				.createIdentityManager();

		User user = new User("jane");

		user.setEmail("jane@doe.com");
		user.setFirstName("Jane");
		user.setLastName("Doe");

		identityManager.add(user);
		identityManager.updateCredential(user, new Password("abcd1234"));
	}
	
}

このクラスは、アプリケーションの起動時にユーザ登録を行っています。@Startupにより、アプリケーションの起動時にこのクラスが呼ばれます。
ここでは、ユーザ情報をハードコーディングしていますが、実際の開発では、データベースを使用することになると思います。

HttpSecurityConfiguration.java

public class HttpSecurityConfiguration {

	/**
	 * Configures Http Security.
	 * 
	 * @param event the SecurityConfigurationEvent.
	 */
	public void onInit(@Observes SecurityConfigurationEvent event) {
		SecurityConfigurationBuilder builder = event.getBuilder();

		builder.http()
			.forPath("/protected/*")
				.authenticateWith()
					.scheme(FormWithAjaxAuthenticationScheme.class);
	}

}

このクラスは、認証に関する設定を行っています。onInitメソッドの引数を、@Observes SecurityConfigurationEventとしています。デプロイ時にSecurityConfigurationEvent が発行され、このメソッドが実行されます。
ここでは、/protected以下のディレクトリにアクセス制限をかけています。
schemeメソッドは、独自の認証スキームを設定します。ここでは、FormWithAjaxAuthenticationSchemeクラスという独自のクラスを設定しています。
PicketLink標準のスキームの場合、セッションタイムアウト後にAjaxフォームを操作すると、無反応(に見える)状態になるため、独自のスキームを作成しています。

FormWithAjaxAuthenticationScheme.java
以下のコードは、部分的な抜粋です。

public class FormWithAjaxAuthenticationScheme implements
		HttpAuthenticationScheme<CustomAuthenticationConfiguration> {

ユーザ独自のスキームを作成する場合、HttpAuthenticationScheme<CustomAuthenticationConfiguration>を実装します。

/**
 * The timeout page URL.
 */
private static final String TIMEOUT_PAGE_URL = "/timeout.xhtml";

/*
 * (non-Javadoc)
 * 
 * @see
 * org.picketlink.http.authentication.HttpAuthenticationScheme#challengeClient
 * (javax.servlet.http.HttpServletRequest,
 * javax.servlet.http.HttpServletResponse)
 */
@Override
public void challengeClient(HttpServletRequest request,
		HttpServletResponse response) {

	if ("partial/ajax".equals(request.getHeader("Faces-Request"))) {
		sendXmlResponse(TIMEOUT_PAGE_URL, request, response);
		return;
	}

	forwardToPage(TIMEOUT_PAGE_URL, request, response);
}

/**
 * Sends XML response.
 * 
 * @param page the redirect page.
 * @param request the HttpServletRequest.
 * @param response the HttpServletResponse.
 */
private void sendXmlResponse(String page, HttpServletRequest request,
		HttpServletResponse response) {
	response.setContentType("text/xml");
	response.setCharacterEncoding("UTF-8");

	try {
		response.getWriter()
				.printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?><partial-response><redirect url=\"%s\"></redirect></partial-response>",
						request.getContextPath() + page).flush();
	} catch (IOException e) {
		e.printStackTrace();
	}

}

/**
 * Forwards to page.
 * 
 * @param page the redirect page.
 * @param request the HttpServletRequest.
 * @param response the HttpServletResponse.
 */
private void forwardToPage(String page, HttpServletRequest request,
		HttpServletResponse response) {

	try {
		response.sendRedirect(request.getContextPath() + page);
	} catch (IOException e) {
		e.printStackTrace();
	}

}

challengeClientメソッドは、認証されていないユーザとセッションタイムアウトしているユーザが、アクセス制限の掛かっているURLにアクセスした場合に、実行されます。
PicketLink標準のスキームでは、Ajax対応がされていないため、ブラウザに応答が帰ってきても処理を続行できず、ユーザから見るとボタンを押しても無反応に見えてしまいます。
そのため、上記のコードでは、Ajaxの場合はXMLを返し、Ajaxでない場合は、リダイレクト先を返すようにしています。
sendXmlResponseメソッドは、Ajaxの場合の応答XMLです。

/index.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://xmlns.jcp.org/jsf/core"
        xmlns:h="http://xmlns.jcp.org/jsf/html"
        xmlns:p="http://primefaces.org/ui">
    <f:view transient="true">
        <h:head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
            <title><h:outputText value="#{msg['index.title']}"/></title>
            <h:outputStylesheet library="css" name="default.css"/>
        </h:head>
        <h:body>
            <p:link outcome="/login" value="#{msg['index.loginPage']}"/>
        </h:body>
    </f:view>
</html>

picketlink-ajax-example-index
このアプリケーションの最初の画面です。<html>のxmlns:p=”http://primefaces.org/ui”で、PrimeFacesを使えるようにしています。
<f:view transient=”true”>は、JSFをステートレスモードにしています。ステートレスモードでない場合、セッションタイムアウト後に画面を操作すると、ViewExpiredExceptionが発生しますので、ステートレスモードでそれを防いでいます。
<h:outputText value=”#{msg[‘index.title’]}”/>のvalueの値は、メッセージリソースを呼んでいます。ブラウザの言語の優先順位の設定によって、message_en.propertiesかmessage_ja.propertiesのどちらかのindex.titleが呼ばれます。
<p:link>は、PrimeFacesのタグで、/login.xhtmlへのリンクを作成します。outcome属性は、.xhtmlを省いて指定します。

/login.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://xmlns.jcp.org/jsf/core"
        xmlns:h="http://xmlns.jcp.org/jsf/html"
        xmlns:p="http://primefaces.org/ui">
    <f:view transient="true">
        <h:head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
            <title><h:outputText value="#{msg['login.title']}"/></title>
            <h:outputStylesheet library="css" name="default.css"/>
        </h:head>
        <h:body>
            <h:outputText value="#{msg['login.title']}"/>
            <div style="height: 10px;"></div>
            <h:form>
                <p:messages id="msg"/>
                <p:panelGrid columns="2" styleClass="ui-noborder">
                    <h:outputText value="#{msg['login.username']}"/>
                    <p:inputText value="#{loginCredentials.userId}"/>
                    <h:outputText value="#{msg['login.password']}"/>
                    <p:password value="#{loginCredentials.password}"/>
                </p:panelGrid>
                <div style="height: 10px;"></div>
                <p:commandButton value="#{msg['login.login']}" action="#{loginView.login()}" update="msg"/>
            </h:form>
            <p>
                <h:outputText value="#{msg['login.tip']}"/>
            </p>
        </h:body>
    </f:view>
</html>

picketlink-ajax-example-login
このxhtmlファイルは、ログイン画面です。
<p:inputText value=”#{loginCredentials.userId}”/>と<p:password value=”#{loginCredentials.password}”/>のloginCredentialsは、PicketLinkで定義されているオブジェクトです。
<p:commandButton value=”#{msg[‘login.login’]}” action=”#{loginView.login()}” update=”msg”/>のloginView.login()は、LoginView.javaのloginメソッドを実行します。
ログインに失敗した場合、update属性がmsgとなっているので、<p:messages id=”msg”/>が更新され、ログイン失敗のメッセージが表示されます。
picketlink-ajax-example-login-faided
成功した場合、ホーム画面/protected/home.xhtmlへリダイレクトします。

LoginView.java

@Named
@RequestScoped
public class LoginView {

	/**
	 * The Identity.
	 */
	@Inject
	private Identity identity;

	/**
	 * Processes login.
	 * 
	 * @return the outcome.
	 */
	public String login() {

		if (identity.isLoggedIn()) {
			return "/protected/home?faces-redirect=true";
		}

		AuthenticationResult result = identity.login();

		if (AuthenticationResult.FAILED.equals(result)) {
			FacesContext facesContext = FacesContext.getCurrentInstance();
			UIViewRoot uiViewRoot = facesContext.getViewRoot();
			ResourceBundle resourceBundle = ResourceBundle.getBundle(
					"resources.message", uiViewRoot.getLocale());
			String loginFailed = resourceBundle.getString("login.failed");
			facesContext.addMessage(null, new FacesMessage(loginFailed));
			return "";
		}

		return "/protected/home?faces-redirect=true";
	}

}

@Namedは、EL式でこのクラスを参照できるようにしています。クラス名の最初の大文字は、EL式で参照する際は、小文字にする規約のため、LoginView.javaは、loginViewとなります。
@RequestScopedは、CDIのアノテーションで、javax.enterprise.context.RequestScopedです。
Identityは、PicketLinkで定義されているユーザ情報に関するオブジェクトで、@Injectによって、注入されます。
loginメソッドは、/login.xhtmlの#{loginView.login()}から呼ばれます。
identity.isLoggedIn()で、すでにログイン済みかどうか検証し、ログイン済みであれば、ホーム画面/protected/home.xhtmlへリダイレクトします。.xhtmlが省かれていることに注意してください。また、?faces-redirect=trueを付加するとリダイレクトし、付加されない場合は、ブラウザ側のURLは変化しないで、JavaScriptによってページ内容が書き換えられます。
identity.login()で、認証を実行します。
認証に失敗した場合、認証失敗メッセージを/login.xhtmlに表示します。ResourceBundle.getBundle(
“resources.message”, uiViewRoot.getLocale())において、uiViewRoot.getLocale()は、ロケールを取得します。ロケールがenの場合、resourceBundleは、/WEB-INF/classes/resources/message_en.propertiesとなり、jaの場合、/WEB-INF/classes/resources/message_ja.propertiesとなります。
リソースファイルから、login.failedの値を取得して、FacesMessageに設定します。
ここで設定された文字列は、/login.xhtmlの<p:messages id=”msg”/>に出力されます。
そして、return “”となっているので、画面遷移せずにログイン失敗のメッセージが表示されます。
ログインに成功した場合、ホーム画面/protected/home.xhtmlへリダイレクトします。

/protected/home.xhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:f="http://xmlns.jcp.org/jsf/core"
        xmlns:h="http://xmlns.jcp.org/jsf/html"
        xmlns:p="http://primefaces.org/ui">
    <f:view transient="true">
        <h:head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
            <title><h:outputText value="#{msg['home.title']}"/></title>
            <h:outputStylesheet library="css" name="default.css"/>
        </h:head>
        <h:body>
            <p>
                <h:outputText value="#{msg['home.title']}"/>
            </p>
            <div style="height: 10px;"></div>
            <h:form>
                <h:outputText value="#{msg['home.currentDateTime']}"/><br/>
                <h:outputText id="currentDateTime" value="#{homeView.currentDateTime}">
                    <f:convertDateTime pattern="yyyy-MM-dd HH:mm:ss"/>
                </h:outputText>
                <div style="height: 10px;"></div>
                <p:commandButton value="#{msg['home.updateDateTime']}"
                        actionListener="#{homeView.updateDateTime()}" update="currentDateTime"/>
            </h:form>
                <div style="height: 30px;"></div>
            <h:form>
                <p:commandButton value="#{msg['home.logout']}" action="#{homeView.logout()}"/>
            </h:form>
        </h:body>
    </f:view>
</html>

picketlink-ajax-example-home
ログイン後のホーム画面です。ログインに成功した場合に表示されます。
ホーム画面では、現在の日時を表示します。また、[最新日時を表示する]ボタンを押すと、日時を更新します。<p:commandButton value=”#{msg[‘home.updateDateTime’]}”
actionListener=”#{homeView.updateDateTime()}” update=”currentDateTime”/>は、[最新日時を表示する]ボタンを作成します。homeView.updateDateTime()は、HomeView.javaのupdateDateTimeメソッドを呼びます。
このメソッドは、currentDateTimeフィールドを最新日時に更新します。そして、このタグは、update=”currentDateTime”となっているので、IDがcurrentDateTimeである<h:outputText id=”currentDateTime” value=”#{homeView.currentDateTime}”>の値を更新します。
このタグは、value=”#{homeView.currentDateTime}”となっているので、HomeView.javaのgetCurrentDateTimeを呼びます。getCurrentDateTimeは、単純なゲッターなので、updateDateTimeで更新された後のcurrentDateTimeフィールドの値を返します。
また、この一連の処理は、Ajaxで行われています。
<p:commandButton value=”#{msg[‘home.logout’]}” action=”#{homeView.logout()}”/>は、[ログアウト]ボタンを作成します。[ログアウト]ボタンを押すと、HomeView.javaのlogoutメソッドが実行されます。

HomeView.java

@Named
@RequestScoped
public class HomeView {

	/**
	 * The Identity.
	 */
	@Inject
	private Identity identity;

	/**
	 * The current Date and time.
	 */
	private Date currentDateTime = new Date();

	/**
	 * Updates date and time.
	 */
	public void updateDateTime() {
		currentDateTime = new Date();
	}

	/**
	 * Processes logout.
	 * 
	 * @return the outcome.
	 */
	public String logout() {
		identity.logout();
		return "/loggedOut?faces-redirect=true";
	}

	/**
	 * Gets current date and time.
	 *
	 * @return the current date and time.
	 */
	public Date getCurrentDateTime() {
		return currentDateTime;
	}

	/**
	 * Sets current date and time.
	 *
	 * @param currentDateTime the current date and time.
	 */
	public void setCurrentDateTime(Date currentDateTime) {
		this.currentDateTime = currentDateTime;
	}

}

/protected/home.xhtmlから呼び出されるバッキングビーンです。
PicketLinkで定義されているIdentityオブジェクトを@Injectで注入しています。
updateDateTimeメソッドは、現在日時を最新に更新します。
logoutメソッドは、identity.logout()を実行して、ログアウトを行います。その後、return “/loggedOut?faces-redirect=true”で/loggedOut.xhtmlへリダイレクトします。

/loggedOut.xhtml
ソースは省略します。
picketlink-ajax-example-logged-out
この画面は、ログアウトした後に表示されます。

/timeout.xhtml
ソースは省略します。
picketlink-ajax-example-timeout
この画面は、認証されていないユーザとセッションタイムアウトしたユーザがセキュリティーの掛かっているディレクトリ(今回の例では/protected以下のディレクトリ)にアクセスした場合に表示されます。
通常のフォーム操作はもちろん、Ajaxの場合も対応しています。