Force.comの少し偏った教科書 : 3.Sitesにセッションを実装する

Java,Salesforce,プログラミング

こんにちわ、ソリューション開発部の倉橋浩一です。

今回の偏りポイント

通常 Salesforce の Visualforce / Apex でページを作っていく場合、フレームワークが自動的に面倒を見てくれますので、セッションを意識することはありません。

しかし、このブログでお送りしているように Sites でページを作る場合にはセッションがサポートされません。つまり自分で実装する必要があります。

今回は、そのための方法をご紹介します。なお、前回も書きましたが、このブログで紹介している方法は実際の実装とは異ってかなり簡略化されており、セキュリティホールとなってしまう可能性があります。このブログでの内容はあくまでも参考にとどめ、Salesforce ではなく一般のWebページと同等の知識と技術をもって自己責任の下にページを実装してください。

また、今回はオブジェクトの定義、ページの定義、クラスの定義…を一気に進めます。盛りだくさんなので、どうぞお楽しみに。

なお、通常の Visualforce / Apex の場合、比較的親切なエラーメッセージが出ますが、Sites でエラーが出た場合は何故か「認証画面」が出てきます。エラーメッセージのまったく無い状態で開発するなんてZ80アセンブラ以来だぜ……などと文句を言いながら作業することになります。一応ログは見られるのですが実行ログの中にエラーメッセージが紛れ込む形なので、わりと大変です。その覚悟のある方だけお試しください。え、「あたし、バグ出さないから平気♡」ですか…うらやましいです。

前回の記事はこちら。

考え方

ログインに成功したらセッションIDを生成し、クッキーとしてブラウザに渡すとともにサーバ側でもオブジェクト上にIDを保存します。ログイン以外のページでは apex:outputPanel で全体をラップし、クッキーが有効なセッションIDを保持している時だけページを生成します。

必要なオブジェクトを定義

ログインするユーザのIDなどを記録するオブジェクトと、セッション情報を記録するオブジェクトを用意します。オブジェクトの登録は画面上端右寄りにある「設定」リンクをクリックして Force.com ホーム画面へ入ります。

必要なオブジェクトを定義

左のメニューバーから ビルド>作成>オブジェクト をクリックしてオブジェクト一覧画面を表示し、新規カスタムオブジェクトボタンをクリックします。

新規カスタムオブジェクト

以下の通りに項目を入力し「保存&新規」をクリックします。

表示ラベルユーザ
オブジェクト名User
レコード名氏名
データ型テキスト

もう一つのオブジェクトを登録します。

表示ラベルセッション
オブジェクト名Session
レコード名連番
データ型自動採番
表示形式{0000000000}
開始番号1

「保存」をクリックします。以下の画面に変わります。

保存した結果の画面

ここで注意事項です。オブジェクトとして User と Session を登録しましたが、Salesforce にログインするときのアカウント情報を保持しているオブジェクトも実は User という名前です。Salesforce はこういう名前の衝突を避けるために、カスタムオブジェクトについては強制的に末尾に __c を添付します。つまり、標準オブジェクトでは User、さきほど定義したカスタムオブジェクトは User__c となります。このルールはオブジェクトだけでなく項目についても共通しています。カスタムオブジェクトに追加した項目はもちろん、標準オブジェクトに追加した項目もすべて __c がつきます。いずれのオブジェクトでも Id(プライマリキー), Name などの標準項目については __c は付きません。

では次にこれらカスタムオブジェクトに項目を追加して行きましょう。さきほどの続きでカスタムオブジェクト-セッションの画面になっていると思いますので、「カスタム項目& リレーション」の「新規」ボタンをクリックしてください。「ステップ1.データ型の選択」に変わります。「テキスト」を選択して「次へ」ボタンをクリックします。

「ステップ2.詳細を入力」で以下の項目を入力し、「次へ」ボタンをクリックします。

項目の表示ラベルセッションID
文字数255
項目名sessionId

「ステップ 3. 項目レベルセキュリティの設定」では、ユーザのプロファイルごとにアクセス権限を設定します。各ユーザはプロファイルによって機能やデータへのアクセス権限を定義します。この項目はユーザが勝手にいじって欲しくないので、すべて「参照のみ」をチェックします。「次へ」ボタンをクリックします。

「ステップ 4. ページレイアウトへの追加」で、自動生成されるメンテナンス画面上にこの項目を掲載するか否かを決めます。チェックしたままとし、「保存&新規」をクリックします。

同様にして、以下の項目を追加します。

データ型日付/時間
項目の表示ラベル有効時間
項目名available
セキュリティレベル全部参照のみ
セッションレイアウトチェック

「保存&新規」ボタンをクリックします。

データ型テキスト
項目の表示ラベルユーザID
文字数255
項目名userId
セキュリティレベル全部参照のみ
セッションレイアウトチェック

「保存」ボタンをクリックします。これで「セッション」オブジェクトの定義は終わりです。

同様にして「ユーザオブジェクト」の定義をします。画面左のメニューバーから ビルド>作成>オブジェクト をクリックします。カスタムオブジェクト一覧画面で「ユーザ」をクリックします。「カスタム項目 & リレーション」の「新規」ボタンをクリックします。あとはさきほどと同じように、

データ型メール
項目の表示ラベルid
項目名id
必須項目チェック
ユニークチェック
セキュリティレベルそのまま
レイアウトそのまま
データ型テキスト(暗号化)
項目の表示ラベルパスワード
文字数127
項目名password
必須項目チェック
マスク種別 すべての文字をマスク
マスク文字 *
セキュリティレベル そのまま
レイアウト そのまま

を登録します。

なお、蛇足ですが、ここではパスワードは Salesforce の「テキスト(暗号化)」属性を用いて保存していますが、Salesforce の「テキスト(暗号化)」属性の項目は Salesforce の UI からは見ることができないので一見安全に見えます。しかし、SOQL で fetch すると平文の文字列として取得できてしまうので、安全ではありません。安全なWebサイトを作る場合には、パスワードのハッシュをオブジェクト上に保存し、ログイン時に入力したパスワードのハッシュと比較する、などの対策を講じてください。

メンテナンスしやすいように、ユーザオブジェクトを画面上のツールバー(左端に「ホーム」とある行)に追加しましょう。

Force.com ホーム画面の左メニューバーから ビルド>作成>タブ をクリックします。「カスタムオブジェクトタブ」の「新規」ボタンをクリックします。「ステップ 1. 詳細を入力」でオブジェクトに「ユーザ」を選び、タブスタイルの虫眼鏡アイコンをクリックして適当なアイコンを選びます。「次へ」ボタンをクリックします。

「ステップ 2. プロファイルに追加」でプロファイルに対してタグの使用許可を与えます。なおオブジェクトの使用許可とは別に設定できますが、オブジェクトの使用が許可されていなければタブだけ許可しても無意味です。ここでは「デフォルトで表示」のプルダウンを「タブを隠す」に変更してから、「プロファイルごとに異なるタブ表示を適用する」を選び、システム管理者だけ「デフォルトで表示」に変更します。ユーザのIDやパスワードはシステム管理者だけがアクセスできる、という寸法です。「次へ」ボタンをクリックします。

「ステップ 3. カスタムアプリケーションに追加」で、どのアプリケーションにこのタブを追加するかを指定します。ここでいう「アプリケーション」とは Salesforce 特有の用語で、タブのセットをアプリケーションと呼んでいます。画面の上の右端にプルダウンメニューがありますが、ここでタブのセットを切り替えできるようになっており、そのひとかたまりのタブがアプリケーションと呼ばれているわけです。ここではいじらずにそのまま「保存」ボタンをクリックしましょう。

画面上のツールバーに「ユーザ」が追加された

画面上のツールバーに「ユーザ」が追加されたと思います。クリックします。

ユーザの一覧が表示されます。もちろんまだデータはありませんので、「新規」ボタンをクリックし、

氏名<ご自分の氏名>
id適当なメールアドレス
パスワード任意のパスワード

を入力してください。なお、各入力欄が赤い縁取りになっているのは、この項目が必須入力項目であることを示しています。「保存」ボタンを押して保存してください。詳細表示画面になります。再度上の「ユーザ」をクリックするとユーザ一覧に今登録したデータが表示されます。

必要なコントローラークラスを定義

Visualforce で MVC の View を定義したらその実際の処理をコントローラとして定義します。コントローラークラスは、Apexで記述します。Apex は Java によく似たスクリプト言語です。JavaScript からはむしろ遠く、Java 経験のある方ならそれほど苦労しないで書けると思います。

Force.com ホーム画面の左側メニューバーから、ビルド>開発>Apex クラスをクリックします。Apex クラス一覧が表示されます。「新規」ボタンを押し、以下のコードをコピペしてから「保存」ボタンをクリックしてください。SQL(Salesforce ではSOQL)が直接書かれていたり Java との相違点もありますが、なにをやっているかはだいたいおわかりいただけるかと思います。

もう一度「新規」ボタンを押し、以下の4本のコードをコピペしてから「保存」ボタンをクリックしてください。コードはログイン画面の LoginController、メイン画面の UserListController、共通クラスの CommonController、インタフェース CommonInterfaceです(貼ると自動的にクラス名が指定されます)。

public class LoginController  {
    public User__c inputUser {get; set;}    
    public User__c loginUser;

    public LoginController() {
        inputUser = new User__c();
    }
    
    public PageReference actionLogin() {
        User__c user = login(inputUser);
        
        if (user != null) {
            String sessionId = generateNewSessionId();
            writeSessionId(user, sessionId);
            writeSessionIdToCookie(sessionId);
            
            return Page.UserListPage;
        } else {
            removeSessionIdOfCookie();
            return null;
        }
    }

    public User__c login(User__c inUser) {
        List users = [Select Id, id__c, Name, password__c FROM User__c WHERE id__c = :inUser.id__c];
        if (users.size() == 1) {
            User__c aUser = users[0];
            if (aUser.password__c != null && aUser.password__c.equals(inUser.password__c)) {
                loginUser = aUser;
                return loginUser;
            }
        }
        
        ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'ログイン失敗'));
        return null;
    }
    
    private String generateNewSessionId() {
        DateTime dt = DateTime.now();       //  donot use such session id
        return EncodingUtil.convertToHex(Crypto.generateDigest('md5', Blob.valueOf(dt.formatLong())));
    }
    
    private void writeSessionId(User__c inUser, String inSessionID) {
        if (inSessionID != null && inUser != null) {
            Session__c session = null;
            List sessions = [SELECT ID FROM Session__c WHERE userId__c = :inUser.id__c];
            if (sessions.size() != 1) {
                if (sessions.size() > 0) {
                    delete sessions;
                }
                sessions = null;
            }
            if (sessions == null) {
                session = new Session__c(userId__c = inUser.id__c);
            } else {
                session = sessions[0];
            }
            session.sessionId__c = generateNewSessionId();
            session.available__c = DateTime.now().addSeconds(CommonController.releaseTime);
            upsert session;
        }
    }
    
    public void writeSessionIdToCookie(String inSessionId) {
        Cookie aCookie = new Cookie(CommonController.theCookieName, inSessionId, '/',  CommonController.releaseTime, true);
        ApexPages.currentPage().setCookies(new Cookie[] {aCookie});
    }
    
    public void removeSessionIdOfCookie() {
        Cookie aCookie = new Cookie(CommonController.theCookieName, 'deleted', '/',  -1, true);
        ApexPages.currentPage().setCookies(new Cookie[] {aCookie});
    }
}
public class UserListController extends CommonController {
    public List<User__c> arrayUser {get; set;}
    
    public UserListController() {
        arrayUser = [SELECT Id, Name, id__c FROM User__c ORDER BY id__c];
    }
}
public virtual class CommonController implements CommonInterface {
    public final static String   theCookieName = 'soldevcookie';
    public final static integer  releaseTime   = 1800;
    
    public boolean hasSession {get; set;}
    public User__c currentUser {get; set;}
    
    public CommonController() {
        checkUserByCookie();
    }
    
    public User__c checkUserByCookie() {
        hasSession = false;
        Cookie aCookie = ApexPages.currentPage().getCookies().get(theCookieName);
        currentUser = null;
        String currentSessionId = null;
        if (aCookie != null) {
            try {
                String cookieValue = aCookie.getValue();
                if (cookieValue != null && cookieValue != 'deleted') {
                    List<Session__c> sessions =  [SELECT Id, userId__c, sessionId__c, available__c FROM Session__c WHERE sessionId__c = :cookieValue];    
                    if (sessions.size() == 1) {
                        Session__c session = sessions[0];
                        if (session.available__c >= DateTime.now()) {
                            List<User__c> users = [SELECT Id, Name, id__c FROM User__c WHERE id__c = :session.userId__c];
                            if (users.size() == 1) {
                                currentUser = users[0];
                                hasSession = true;
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                ApexPages.addMessages(ex);
            }
        }
                
        return currentUser;
    }    
}
public virtual interface CommonInterface {

}

必要なページを用意

では View を定義します。Force.com ホーム画面の左側メニューバーから、ビルド>開発>Visualforce ページをクリックしてVisualforce 一覧ページを表示し、「新規」ボタンをクリックします。

  • 表示ラベル:ログイン
  • 名前:LoginPage
<apex:page showHeader="false" sidebar="false" controller="LoginController" cache="false">
    <div style="padding-top:100px; width:100%;">        
        <div style="margin:auto; width:300px;" >
            <apex:form >
                <apex:pageBlock >
                    <apex:pageBlockSection columns="1" title="そるでぶろぐデモ - Login">
                        <apex:pageBlockSectionItem >
                            Id:
                            <apex:inputText value="{!inputUser.id__c}" />
                        </apex:pageBlockSectionItem>
                        <apex:pageBlockSectionItem >
                            Password:
                            <apex:inputSecret value="{!inputUser.password__c}" />
                        </apex:pageBlockSectionItem>
                        <apex:pageBlockSectionItem >
                            <apex:messages />
                        </apex:pageBlockSectionItem>
                        <apex:pageBlockSectionItem >
                            <apex:inputHidden />
                            <apex:commandButton value="Login" action="{!actionLogin}" />
                        </apex:pageBlockSectionItem>
                    </apex:pageBlockSection>
                </apex:pageBlock>
            </apex:form>
        </div>
    </div>
</apex:page>

「保存」ボタンをクリックし、再度「新規」ボタンをクリックします。

  • 表示ラベル:ユーザ一覧
  • 名前:UserListPage
<apex:page showHeader="false" sidebar="false" controller="UserListController" cache="false">
    <apex:outputPanel layout="none" rendered="{!hasSession}">
        <apex:form>
            <apex:pageBlock>
                <apex:pageBlockTable value="{!arrayUser}" var="anItem">
                    <apex:column>
                        <apex:facet name="header">{!$ObjectType.User__c.Fields.id__c.Label}</apex:facet>
                        <apex:outputField value="{!anItem.id__c}"/>
                    </apex:column>
                    <apex:column>
                        <apex:facet name="header">{!$ObjectType.User__c.Fields.name.Label}</apex:facet>
                        <apex:outputField value="{!anItem.Name}"/>
                    </apex:column>
                </apex:pageBlockTable>
            </apex:pageBlock>
            
        </apex:form>
    </apex:outputPanel>
</apex:page>

Visualforce は HTML を拡張したマークアップ言語です。他のフレームワーク同様タグとプロパティで表示を定義していく方式ですので、HTML などに馴れた方であれば何をやっているかはだいたいおわかりになるかと思います。

Sitesの設定

以上で MVC がそろいました。Sites からアクセスするための設定を行います。例によって Force.com ホーム画面の左側メニューバーから、ビルド>開発>サイト をクリックします。すると Sites 一覧が表示されます。前回定義した「デモ」が表示されていると思います。「デモ」の右にある「編集」をクリックしてください。

前回のデモの定義がありますが、「有効なサイトのホームページ」を「ログイン」に変更します。「有効なサイトのホームページ」の左にある虫眼鏡アイコンをクリックし、「ログイン」と入力して「GO!」ボタンをクリックすると下の一覧にさきほど定義した「ログイン」が現れますので、これをクリックして選択します。「サイトの編集」に戻ったら、「保存」ボタンをクリックします。

続いて、アクセス権限の設定を行います。「公開アクセス設定」ボタンをクリックします。必要な設定は「有効な Apex クラス」「有効な Visualforce ページ」「カスタム項目レベルセキュリティ」「カスタムオブジェクト権限」です。いずれもページやクラス、データベース項目について、Sites からのアクセス権限を設定します。なお、クラスに with sharing オプションを付加すれば、クラス上ではすべてのオブジェクトに対してアクセスできるようになりますが、個人的には可能な限り設定ベースで対応していくべきと考えています。なお with sharing で fetch できても、画面への表示・入力に「apex:inputField」「apex:outputField」を用いる場合は「カスタム項目レベルセキュリティ」で表示が許可されていない場合には画面上には空白しか表示されません。

では「有効なApexクラス」を定義します。「有効なApexクラス」にマウスカーソルを合わせるとウィンドウがポップアップしてきますので、その中の「編集」ボタンをクリックし、LoginController と UserListController を「有効化された Apex クラス」に追加し、保存ボタンをクリックします。

Apex クラスアクセスを有効化

同様に「有効な Visualforce ページアクセス」にマウスオーバーしポップアップの「編集」ボタンをクリックし、LoginPage と UserListPageを「有効化された Visualforce ページ」に追加し、「保存」ボタンをクリックします。

Visualforce ページのアクセスを有効化

「カスタム項目レベルセキュリティ」の「セッション」の「参照」をクリックし、「編集」をクリックします。「セッションID」「ユーザID」「有効時間」の「編集アクセス権」を チェックします(自動的に参照アクセス権もチェックされます)。「保存」ボタンをクリックします。

「プロファイルに戻る」ボタンをクリックして、同様に「カスタム項目レベルセキュリティ」の「ユーザ」をの「参照」をクリックし、「編集」をクリックします。「パスワード」の「参照アクセス権」をクリックします。今回 Sites 上ではパスワードを変更することはないので read only にしましたが、もしパスワード変更が必要な場合には「編集アクセス権」をチェックします。「保存」ボタンをクリックし、「プロファイルに戻る」ボタンをクリックします。

続いて、「カスタムオブジェクト権限」ですが、これは「デモプロファイル」画面の上の方にある「編集」ボタンから変更します。「セッション」については「参照」「作成」「編集」「削除」をチェックし、ユーザの「参照」をチェックします。「保存」ボタンをクリックします。

ワークフロー設定

上記でオブジェクトを定義する際にセッションの「有効時間」を設けました。ログイン以降、セッションが有効な時間を設定して期限が切れたら表示しない、という処理を実装するつもりだったのですが、日時の処理がめんどくさいので Salesforce のワークフロー機能を使います。Salesforce でいうところのワークフロー機能とは、データを新規作成または更新する際に、値が一定の条件を満たす時にメールを送信する、他の項目の値を変更する、などの機能を自動実行するためのものです。例えば、終了期日が入力されたら、データの所有者に終了を通知するメールを送る、というような機能がコーディングなしで実現可能です。

例によって Force.com ホーム画面の左側メニューバーから、ビルド>作成>ワークフローと承認設定>ワークフロールール をクリックします。説明にさっと目を通したら「次へ」ボタンをクリックしてください。「新規ルール」ボタンをクリックします。

「ステップ 1: オブジェクトの選択」で処理の対象となるオブジェクトとして「セッション」を選び、「次へ」ボタンをクリックします。

さて、一通り準備は終わりました。

テスト

Salesforce にログインし、さきほど定義したユーザオブジェクトにユーザを何件か登録します。登録が終わったら一旦 Salesforce からログアウトします。

次にブラウザを二つ用意します。とりあえず Safari と Chrome を用いて、Safari でサイト一覧に記載されている URL にアクセスします。ログインページが表示されますので、さきほどユーザオブジェクトに登録した id と password でログインします。すると上で登録したユーザが一覧表示されます。この状態でブラウザの URL をコピーし、Chrome 上にコピペしてみます。ユーザ一覧が表示されずログインへのリンクだけが表示されれば、「ログインしないとデータを見ることができない」という最低限の機能が実装されていることがわかります。

ページが正常に表示されない場合には、Force.com ホーム画面の左側メニューバーから、ビルド>開発>サイト の「デモ」のリンクをクリックし、「公開アクセス設定」をクリックして各権限が正しく設定されているかをご確認ください。

最後に

繰り返しになりますが、今回のセッション実装はかなり簡易的なものです。安全な業務サイトを Sites 上に作るためには、一般的なWebサイトにおけるセキュリティについて知見を踏まえたより安全な実装が必要です。

また、これも繰り返しになりますが、Sites でのオブジェクトなどの利用については、形態によっては別途ライセンス料が必要となる場合があります。「こういうことをやりたいのですが」と Salesforce 営業担当者に相談してから、行ってください。

  • Zabbix Enterprise Appliance
  • 低コスト・短納期で提供するまるごとおまかせZabbix