GAEでSpringMVCを動かしてみる4(続 Spring Security を試してみる)

前回のエントリー(GAEでSpringMVCを動かしてみる3(Spring Security を試してみる) | Walk on apps.)からの続きです。

SpringSecurityを利用して、ログイン・ログアウト機能を試しています。
前回は、「とりあえず、まずは動かしてみる、」ってことをやってみました。
今回は、パスワード暗号化や、ユーザごとのアクセス制御、HTTPS対応、とかを追加して「もうちょっと、きちんと動かしてみる」、つもりです。

前回のエントリーまでは、ソースコードをBlog上にまるごと貼り付けていたんですが、大変なのでGitHubで公開することにしました。
GitHubからDownload

Blogでは、ポイントに絞って書いていきます。


(6)もうちょっと、きちんと動かしてみる
次のようなことを、設定してみようと思います。
・パスワードの暗号化
・アクセス制御
・ログインユーザ名の取得
・オリジナルのLoginページの作成
・Logout時の設定
・通信の暗号化(HTTPS設定)
・日本語の文字化け対応

・パスワードの暗号化
StandardPasswordEncoderを利用して、パスワードを暗号化します。SpringSecurityのチュートリアルを参考にしました。「Using encoded passwords」のところに設定例が載っています。
Spring Security — Tutorial

マニュアルによると、StandardPasswordEncoderは、SHA-256でハッシュ化を行うようです。あと、saltとして、ランダムな8Byteデータを付与してくれる。
25. Spring Security Crypto Module

SpringSecurityのBean定義ファイルには、以下のように書きました。ユーザを2つ(admin, bob)定義しています。パスワードは、ユーザ名と同じにしています。

spring-security-dispatcher.xml への追記内容

    <beans:bean id="encoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>

    <authentication-manager>
        <authentication-provider>
            <password-encoder ref="encoder"/>
            <user-service>
                <user name="admin" password="d4c4f5f190abf98b0d5224b549a1f2645acf975c75ec5d2d51895b0a7ca35949fc33576cc8294769" authorities="supervisor, user" />
                <user name="bob" password="cb60b076bebc371a3e5f6097399a82eb65948513639f4b090e70e2d49b6808fa3b02aa9bcd25668f" authorities="user" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

ハッシュ値の計算方法は、StandardPasswordEncoderのencodeメソッドを使います。チュートリアルでは、scalaのインタプリタを使っていますが、僕は地道にJavaのコードを書きました。以下のようにして、ハッシュ値を計算できます。

File名:PasswordEncode.java

import org.springframework.security.crypto.password.StandardPasswordEncoder;

public class PasswordEncode {
        public static void main(String[] args) throws Exception {
            StandardPasswordEncoder encoder = new StandardPasswordEncoder();
            System.out.println(encoder.encode("admin"));
            System.out.println(encoder.encode("bob"));
        }
}

・アクセス制御
SpringSecurityのBean定義ファイルで、use-expressions=”true”の設定をして、アクセス制御の設定をします。
これも、SpringSecurityのチュートリアルを参考にしました。
Spring Security — Tutorial

以下のような権限を、設定してみることにしました。
・index.htmlと、ログイン・ログアウト画面には、全員に許可。
・コンテキストパス「/velocity/admin」はadminだけ許可。
・コンテキストパス「/velocity/user」は、admin, bobに許可。
・その他のパスは、アクセス禁止。

spring-security-dispatcher.xml への追記内容は、こんな感じ。

<http use-expressions="true">
    <intercept-url pattern="/index.html" access="permitAll" />
    <intercept-url pattern="/velocity/login" access="permitAll"/>
    <intercept-url pattern="/velocity/loginfailed" access="permitAll" />
    <intercept-url pattern="/velocity/logout" access="permitAll" />

    <intercept-url pattern="/velocity/admin/**" access="hasRole('supervisor')" />
    <intercept-url pattern="/velocity/user/**" access="isAuthenticated()" />
    <intercept-url pattern="/**" access="denyAll" />

   :    :
   (中略)

</http>

access=”permitAll”といった、権限の書き方一覧は、以下のマニュアルの「16.1.1 Common Built-In Expressions」「16.2 Web Security Expressions」に載っていました。
16. Expression-Based Access Control
上記マニュアルによると、permitAllといった書き方は、WebSecurityExpressionRootクラスで定義されているとのこと。一応JavaDocも見てみた。
WebSecurityExpressionRoot (Spring Security 3.2.0.M1 API)

・ログインユーザ名の取得
現在ログインしているユーザ名の取得を行い、ブラウザ画面に表示できるようにします。
Controllerのコードを修正します。こちらのサイトを参考にしました。
Get current logged in username in Spring Security

ControllerのメソッドにPrincipalインターフェースを追加して、それを利用してユーザ名を取得しています。
修正ファイル:VelocityController.java

    (中略)
   :    :

@Controller
@RequestMapping("/velocity")
public class VelocityController {

    //DI via Spring
    String message;

    public void setMessage(String message) {
        this.message = message;
    }

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model, Principal principal) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        String name = principal.getName(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }

   :    :
   (中略)

参考にしたサイトによると、ほかにも2つやり方があるらしく、ちょっと書き方変えるだけなので、参考に載せておきます。

まず、「Authentication.getName()」を使うパターン。ControllerのメソッドにPrincipalインターフェースを追加しなくても動きます。

   :  (中略)  :
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
   :  (中略)  :

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String name = auth.getName(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }
   :    :
   (中略)

次に「User.getUsername()」を使うパターン。こちらも、ControllerのメソッドにPrincipalインターフェースを追加しなくても動きます。

   :  (中略)  :

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
   :  (中略)  :

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String name = user.getUsername(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }
   :    :
   (中略)

・オリジナルのLoginページの作成
こちらのサイトを参考にしました。
Spring Security form login using database
このサイトからダウンロードしたサンプルに、ログイン画面のサンプルがあります。

まず、SpringSecurityのBean定義ファイルに、form-login設定をします。

login-pageでは、ログイン画面を指定します。authentication-failure-urlでは、認証エラー時に表示する画面を指定します。
default-target-urlは、リダイレクト先が指定されていないときに、ログイン後に表示する画面を指定します。

認証が必要な画面/velocity/userにアクセスするとログイン画面/velocity/loginが表示されます。ログイン完了すると /velocity/userにリダイレクトされます。ですが、ログイン画面/velocity/loginに直接アクセスする場合もあります。このとき は、ログイン完了後にリダイレクトする宛先がありません。こういったときに、default-target-urlで指定された画面が表示されます。

spring-security-dispatcher.xml への追記内容

<http use-expressions="true">
   :  (中略)  :
    <form-login login-page="/velocity/login" default-target-url="/index.html"
        authentication-failure-url="/velocity/loginfailed" />
   :  (中略)  :
</http>

次に、Controllerのメソッドにログイン画面への遷移を追加します。
ログイン画面/velocity/loginと、認証エラー画面/velocity/loginfailedは、同じHTMLテンプレートを用いています。認証エラー時は、error変数にtrueを設定し、テンプレートに返しています。

修正ファイル:VelocityController.java

:  (中略)  :
@Controller
@RequestMapping("/velocity")
public class VelocityController {

:  (中略)  :
    @RequestMapping(value="/login", method = RequestMethod.GET)
    public String login(ModelMap model) {

        return "login";
    }

    @RequestMapping(value="/loginfailed", method = RequestMethod.GET)
    public String loginerror(ModelMap model) {

        model.addAttribute("error", "true");
        return "login";
    }
:  (中略)  :

}

次にログイン画面のテンプレートを作成します。
認証エラー時に、SPRING_SECURITY_LAST_EXCEPTION.messageにエラー内容が入るみたいなので、認証エラー時はそれを表示するようにしています。

作成したFile:login.vm

<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Login Page</title>

<style>
.errorblock {
    color: #ff0000;
    background-color: #ffEEEE;
    border: 3px solid #ff0000;
    padding: 8px;
    margin: 16px;
}
</style>

</head>

<body onload='document.f.j_username.focus();'>
    <h3>Login with Username and Password ログインテスト</h3>

    #if(${error}=="true")
        <div>
            Your login attempt was not successful, try again.<br /> Caused :
            $SPRING_SECURITY_LAST_EXCEPTION.message
        </div>
    #end

    <form name='f' action="j_spring_security_check"
        method='POST'>

        <table>
            <tr>
                <td>User:</td>
                <td><input type='text' name='j_username' value=''>
                </td>
            </tr>
            <tr>
                <td>Password:</td>
                <td><input type='password' name='j_password' />
                </td>
            </tr>
            <tr>
                <td colspan='2'><input name="submit" type="submit"
                    value="submit" />
                </td>
            </tr>
            <tr>
                <td colspan='2'><input name="reset" type="reset" />
                </td>
            </tr>
        </table>

    </form>
</body>
</html>

「認証エラー時に、SPRING_SECURITY_LAST_EXCEPTION.messageにエラー内容が入る」と書きましたが、その値がApacheVelocity側に引き渡されるように、VelocityViewResolverクラスへの設定追加が必要です。

Bean定義ファイルspring-security-dispatcher.xml の設定修正し、exposeSessionAttributesをtrueに設定します。

   :  (中略)  :
    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType" value="text/html;charset=UTF-8" />
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
        <property name="exposeSessionAttributes" value="true"/>
    </bean>
   :  (中略)  :

・Logout時の設定

まず、SpringSecurityのBean定義ファイルに、logout設定をします。

logout-success-urlで、ログアウト画面を指定します。delete-cookiesでログアウト時にJSESSIONIDをcookieから削除するようにしています。

spring-security-dispatcher.xml への追記内容

<http use-expressions="true">
:  (中略)  :
    <logout logout-success-url="/velocity/logout" delete-cookies="JSESSIONID"/>

</http>

次に、Controllerのメソッドにログアウト画面への遷移を追加します。

修正ファイル:VelocityController.java

:  (中略)  :
@Controller
@RequestMapping("/velocity")
public class VelocityController {

    //DI via Spring
    String message;

    public void setMessage(String message) {
        this.message = message;
    }

:  (中略)  :

    @RequestMapping(value="/logout", method = RequestMethod.GET)
    public String logout(ModelMap model) {

        model.addAttribute("message", this.message);
        return "logout";
    }

}

次にログアウト画面のテンプレートを作成します。

作成したFile:logout.vm

<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Hello App Engine</title>
  </head>

  <body>

    message: ${message}<br/>
    <p>Logoutしました。</p>
  </body>
</html>

・通信の暗号化(HTTPS設定)
SpringSecurityで特定のURLに対してHTTPSを要求する設定ができます。ただし、HTTPS自体をSpringSecurityで制御するわけでないので、HTTPSの入り口は、APサーバ等で用意しておく必要があります。

そのため、まず、GoogleAppEngine側でHTTPSの入り口を用意する必要があります。GAEでHTTPSを使う場合、web.xmlにsecurity-constraint設定を追加すると利用できます。
配備記述子: web.xml – Google App Engine — Google Developers

注意点としては、Google App Engine SDKでは、開発環境ではHTTP接続、GAE上ではHTTPS接続になります。
SpringSecurityでHTTPSを要求する設定をした場合、開発環境でもHTTPSが使えるようにしておかないと、アクセスができません。そのため、僕の場合、HTTPS関連設定はGAE側にすべて任せることにします。

web.xmlに、security-constraint設定を追加します。

  :  (中略)  :
    <!-- GAE Setting -->
    <security-constraint>
        <web-resource-collection>
            <url-pattern>/velocity/login*</url-pattern>
            <url-pattern>/velocity/loginfailed*</url-pattern>
            <url-pattern>/velocity/logout*</url-pattern>
        </web-resource-collection>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
  :  (中略)  :

参考までに、SpringSecurityで設定する場合の内容を書いておきます。
マニュアルはこちら。3. Security Namespace Configuration

requires-channelでプロトコルを指定すると設定できます。
port-mappingは必要に応じて設定します。開発環境などで、通常のポートが使えないときに指定します。

  <http>
    <intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/>
    <intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/>
    ...

    <port-mappings>
      <port-mapping http="9080" https="9443"/>
    </port-mappings>
    ...
  </http>

・日本語の文字化け対応
HTMLテンプレートに日本語があると、ブラウザで表示させたときに、文字化けがおきます。
そのため、Bean定義ファイルと、web.xmlに設定追加しておく必要があります。
こちらのサイトを参考にしました Spring-MVCのビューにVelocityを使いたい – PukiWiki

今回はUTF-8に統一することにします。
まず、Bean定義ファイルspring-security-dispatcher.xmlに設定追加します。

VelocityConfigurerクラスのinput.encoding、output.encodingにUTF-8を設定。
→ たぶん、テンプレートファイル読込と、HTML出力の設定だと思う

VelocityViewResolverクラスの、contentTypeにUTF-8を設定。
→ ブラウザに返却するContent-Typeを設定してる。HTTPヘッダですかね。

 
   :  (中略)  :
    <bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/WEB-INF/view/"/>

        <property name="velocityProperties">
            <props>
            <prop key="input.encoding">UTF-8</prop>
            <prop key="output.encoding">UTF-8</prop>
            </props>
        </property>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType" value="text/html;charset=UTF-8" />
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
        <property name="exposeSessionAttributes" value="true"/>
    </bean>
   :  (中略)  :

次にweb.xmlに設定追加します。

CharacterEncodingFilterクラスで、encodingにUTF-8を設定します。
→HTMLフォームからの入力された文字を、UTF-8にしてサーバ側で処理することになります。

   :  (中略)  :
    <!-- UTF-8 Setting -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
   :  (中略)  :

上記設定には、forceEncodingというパラメータもあるみたいだけど、今回は設定しませんでした。

   :  (中略)  :
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
   :  (中略)  :

(7)アプリ起動
GAE上で動かしてみました。
コンテキストパス/velocity/user にhttpでアクセスすると、ログイン画面にリダイレクトされます。httpsになってます。
spring4_01

bobでログインすると、画面が返ってきました。httpにもどっています。
ユーザ名bobが、画面に表示されています。
spring4_02

bobユーザのままで、/velocity/admin にアクセスすると、アクセス拒否されます。
spring4_03

/velocity/user 画面でログアウトをクリックすると、ログアウトします。httpsになっています。
spring4_04

ログイン画面でパスワードを間違えると、エラーメッセージが表示されます。
spring4_05


Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s