AngularJS+JavaEEでセッション管理

AngularJS + JavaEE7 でログイン・ログアウトと、認証の処理について実装してみたときのメモ。

動作確認は GlassFish 4.0 上で実施。

[GitHub サンプル](https://github.com/samples-of-opengl-8080/angular-auth-sample)

#認証処理を入れた場所
次の２カ所で、ログインしているかどうかのチェックを入れている。

1. REST のリクエストがあったとき
2. ページ遷移のとき

##REST のリクエストがあったときのチェック
###Filter でチェック
Filter を作成して、そこでチェックを行っている。

```java:SessionFilter.java
package sample.angular.filter;

import java.io.IOException;

import javax.inject.Inject;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import sample.angular.rest.session.UserSession;

@WebFilter("/rest/*")
public class SessionFilter implements Filter {
    
    @Inject
    private UserSession userSession;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse res = (HttpServletResponse)response;
        
        if (this.needsSessionCheck(req) && !this.userSession.isLogged()) {
            res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
        } else {
            chain.doFilter(request, response);
        }
    }
    
    private boolean needsSessionCheck(HttpServletRequest req) {
        return !req.getRequestURI().endsWith("/session");
    }

    @Override public void destroy() {/*no use*/}
    @Override public void init(FilterConfig filterConfig) throws ServletException {/*no use*/}
}
```

認証チェックが必要なリクエスト（`/rest/session` 以外へのリクエスト）の場合、 `UserSession` の状態をチェックして認証済みか確認している。
認証済み出ない場合は `401` のエラーをクライアントに返している。

###UserSession で認証情報を保持する
`UserSession` は CDI の `SessionScoped` で管理しているインスタンスで、内部にログイン済みかどうかのフラグを持っている。

```java:UserSession.java
package sample.angular.rest.session;

import java.io.Serializable;

import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
import javax.servlet.http.HttpSession;

@SessionScoped
public class UserSession implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @Inject
    private HttpSession session;
    
    private boolean isLogged;
    
    public void start() {
        this.isLogged = true;
    }
    
    public boolean isLogged() {
        return this.isLogged;
    }
    
    public void end() {
        this.isLogged = false;
        this.session.invalidate();
    }
}
```

ログイン時に `start()` メソッドを実行し、ログアウトするときに `end()` メソッドを実行してセッションを破棄する。

###サーバーから 401 が返されたときは、クライアントでページを強制遷移させる
```js:interceptors.js
angular
.module('mine')
.factory('SessionInterceptor', function($q, $location) {
    return {
        responseError: function(response) {
            // サーバーからのレスポンスが 401 ならトップページに強制遷移する
            if (response.status === 401) {
                $location.path('/');
            }
            
            return $q.reject(response);
        }
    };
});
```

`$http` の処理にインターセプターをかませて、サーバーからのレスポンスコードが `401` の場合に、強制的にログイン画面に遷移するようにしている。


##ページ遷移のときのチェック
ページが切り換わるときのイベントをフックして、都度サーバーに認証情報の問い合わせを行うようにしている。

```js:app.js
angular
.module('mine', ['ngRoute'])
.config(function($httpProvider) {
    $httpProvider.interceptors.push('SessionInterceptor');
})
.config(function($routeProvider) {
    $routeProvider
        .when('/', {
            templateUrl: 'app/view/login.html',
            controller: 'LoginController',
            needsAuth: false
        })
        .when('/sample', {
            templateUrl: 'app/view/sample.html',
            controller: 'SampleController',
            needsAuth: true
        });
})
.run(function($rootScope, sessionService, $location) {
    // ページが切り換わるごとに、認証が必要なページの場合はサーバーにセッションの問い合わせを行う
    $rootScope.$on('$routeChangeSuccess', function(event, current, next) {
        if (current.needsAuth) {
            sessionService.inquire();
        }
    });
});
```

`$routeScope.$on("$routeChangeSuccess", callback)` でページ遷移のイベントをフックできる。

このタイミングで認証が必要なページかどうかを確認して、必要であればサーバーに確認のリクエストを送信する。

401 が返された場合は、先ほどのインターセプターによってログイン画面に強制遷移させられる。

認証が必要かどうかを判断するための値（`needsAuth`）は、 `when()` メソッドでルートを設定するときの引数で指定している。