プログラミングホリック

開発に関係することとか気まぐれで書いていきます

Spring Security ログイン中に認証情報を更新させる

今回はSpring Securityに関連することを書いていきます。 文章下手ですみません。見て不足や分からないこと、間違っていることがあればコメントなどもらえれば修正・追記します(多分)

1. やること

ログイン中のユーザーの権限情報を途中で書き換える

2. なぜやるのか

3. 実装

YandroidFilter.java(名前は適当です)

 ~ import省略~

@Component
public class YandroidFilter implements Filter {

 ~ doFilter以外は今回は省略~

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

         Authentication authentication= SecurityContextHolder.getContext().getAuthentication();
         String userName = ((UserDetails) authentication.getPrincipal()).getUsername();
         UserDetails user = UserDetailsService.loadUserByUsername(userName);
         Authentication updatedAuthentication = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
         SecurityContextHolder.getContext().setAuthentication(updatedAuthentication);
    }

 ~ doFilter以外は今回は省略~

}

4. 解説①

ざっくり説明すると、このFilterにくるとセッション中のユーザー情報を元に改めてユーザー情報を取得し、新しく認証情報を作成し詰めなおしています。

仮にログイン中にページAへのアクセス権限が追加されたとします。 しかし、ログイン時点ではページAへのアクセス権限を持っていなかったため、セッション中の情報ではアクセス権限がありません。 このタイミングでFilterを通すリクエストがきた場合、セッション中に持つ情報にはページAへのアクセス権限がないのですが、このFilterを通すことでセッション中の認証情報が更新され、Filterの処理完了後のタイミングではページAへのアクセス権限をセッション中に持っています。

しかし、うまくセッション中の認証情報の更新ができないパターンが存在します。 それはログイン時点では権限を持っていなかったページへのアクセスを試みた(アクセスを試みたタイミングではDB上には権限を持っている)場合です。

4. 解説②

ログイン時点では権限を持っていなかったページへのアクセスを試みた(アクセスを試みたタイミングではDB上には権限を持っている)場合は権限がない感じのエラーが出ると思います。 これはFilterの呼ばれる優先順位の問題です。このFilterが呼ばれるより先に認可処理が呼ばれているため認証情報の更新にたどり着く前にエラーとなるのです。

解決方法として、HttpSecurity のaddFilterBeforeなどで認可処理より認証情報を更新するFilterの呼ばれる優先順位を上げることです。私の場合はFilterSecurityInterceptorより優先度が上になるようにしました。

参考: http://terasolunaorg.github.io/guideline/5.5.1.RELEASE/ja/Security/Authorization.html#filtersecurityinterceptor

6. おわりに

ユーザーの権限がころころ変わるようなサービスじゃなければあまりこの処理は要らないかもですが、ユーザーがログインしている最中にアクセス権限が変化する場合などは参考にしてもらえると嬉しいです。

インテグレーションテストを書いていてハマったこと

概要

SpringBootでJUnitを使い、インテーグレーションテストを書いていた時にハマったこととその解決方法を書きます。

状況

1.Controllerへリクエストを投げると各テーブルに正しくデータが登録されることを確認したいテストを作成していた。

2.テスト実施でDBに不要なレコードが追加されないようにテストクラスに@Transactionalをつけている

3.事前に特定の情報が各テーブルに登録されていないと実施できないテスト(これもロールバックできるように2と同じトランザクション内で行う)

起こったこと

あれ?なんかエンティティが思うように取得できない?なぜにnull?テストじゃなければ動いているのになー

以下のようなエンティティを使用していました。

@Entity
public class A {
@Id
private long id;
private String name;
@OneToMany
private Set<B> b;
}
@Entity
public class B {
@Id
private long id;
private String name;
}

このとき、通常であればAに紐づくBのデータが無い場合、AのrepositoryからEntityを取得するとbはsizeが0の結果が取得できるはずです。(私はそう思っています) しかし、今回のパターンのように1つのトランザクション内で登録して取得しようとする際には注意が必要なようです。

私が失敗してしまった点

まず事前に必要なデータを入れるために以下のようなシンプル方法でデータを入れ、取り出しました。

A a = new A();
a.setId(1L);
a.setName("test");
ARepository.save(a);

A result = ARepository.findById(1L);
result.getB() ← null

解決策

EntityManagerというものを使えばよいのです。

@PersistenceContext
EntityManager entityManager;

さっきのに一行追加

A a = new A();
a.setId(1L);
a.setName("test");
ARepository.save(a);

entityManager.refresh(a); ← これを追加

A result = ARepository.findById(1L);
result.getB() ← size0でちゃんと取得できる!

なぜこれで解決できたかというとEntityのライフサイクルが関係しているみたいなのですが、 大体は理解しているつもりなのですが、完全には理解しきってないので もし気になる方が複数人いらっしゃったら改めて深く勉強して今度まとめるかもしれないです。

JavaとJavaScriptの連携 (昔書いた記事をお引越ししました)

この記事は2013年11月12日に書いたものを引っ越したものです。

内容的には古いものです。

 

今日は卒論早く切り上げて帰ってきたので、

この前やったJavaとwebview内に書いたJavaScriptの連携について書いていこうと思います。

 

まず、簡単にまとめると

Java側からJavaScriptの関数を呼び出すには webView.loadUrl("javascript:関数名") を利用する。

JavaScript側からJavaの関数を呼び出すには JavascriptInterface を利用する。

・値の受け渡しはJSONを利用する。

 

これだけです。

 

 

そして以下はその具体的な書き方を書いていきます。

 

Java側からJavaScriptの関数を呼び出す 

これは簡単です。 

Java

webView.loadUrl("javascript:sample(関数名)");

と書くと、JavaScript側に書いてあるsampleという関数が呼び出されます。

 

 

JavaScript側からJavaの関数を呼び出す

 

Java

 

まず使用するクラスの?インスタンスを生成します。

Sample sample = new Sample();

 

そして生成したイスタンスをJavaScript上で使えるようにする。

webview.addJavascriptInterface(sample,"JSsample(JavaScript側で呼び出す時のオブジェクト名)");

 

あとはこれをJavaScript側で呼び出すだけです。

 

JavaScript

JSsample.(呼び出したいメソッド名);

 

これでJavaScript側からJavaの関数を呼び出すことができるようになります。

 

※ただし、このaddJavaScriptInterfaceを使用するのはセキュリティ上あまりよろしくないようなので、

使用する際は気を付けて使用するべきかもです。

あと、この addJavaScriptInterfaceを使用するとエミュレータが落ちますww

使用するエミュレータのバージョンにもよるらしいですけど、

とりあえず自分は2.3.3のエミュレータでやってたら落ちたので、

自分はこれを書いた以降はずっと実機でテストしてますw

 

 

値の受け渡し(JavaJavaScript

 

Java

 

JSONObject オブジェクト名 = new JSONObject;

オブジェクト名.put("変数名",値);

オブジェクト名.put("変数名",値);

オブジェクト名.put("変数名",値);

 

このような感じでJavaScript側へ渡したい値を変数名とともにオブジェクトに入れていきます。

そしてこのオブジェクトをreturnなどでJavaScriptへ渡します。

 

例1

 

public JSONObject sample(){

 

JSONObject obj = new JSONObject;

obj.put("sample1","a");

obj.put("sample2","b");

return obj;

}

と書いてJavaScript側からこのsample()を呼び出してもらえばいいのです。

もしくは、

 

例2

public void sample(){

 

JSONObject obj = new JSONObject;

obj.put("sample1","a");

obj.put("sample2","b");

webView.loadUrl("javascript:jsonsample("+ obj +")");

}

としてJSONObjectであるobjを引数としてJavaScriptの関数を呼び出せばよいのです。

 

JavaScript

 

Javaから渡されたJSONObjectのオブジェクト名がobjだとします。

そしてobjにはsample1とsample2があるとします。

この場合

 

JSON.parse(obj); 

変数名 = JSONObject.sample1;

変数名 = JSONObject.sample2

 

と書けばいいのです。

 

 

うん、初めてこういうことやったから調べてたときは難しいって思ったけど、

やってみると意外と簡単にできた♪

 

公開処刑(中間発表)まであと2週間ww