閲覧許可がある画面のリンクのみ表示するには? | Java Springの逆引きメモ

Java Springの逆引きメモ

JavaのSpring frameworkのメモを書いていきます!
初心者の勉強ノートなので間違いがあるかもしれませんが、何かヒントになることがあれば幸いです。

前回 、ロールによってリンクの表示を制御する方法を見ました。

これはロールが増えたり変更されることが無ければ特に問題ないのですが、

増えることがあると、関係するすべてのJSPファイルを修正しなければならないので大変です。


ですので、ログインユーザがあるパスに対して認可OKかどうかを判定する機能がほしくなります。


その方法を見てみましょう!


ただし、最初に言っておくとSpringで用意しているtaglibには存在しないようです。

(自分が調べた限りでは)


ですので、機能を自作することになります。

しかし、さすがSpringラブラブ!

再利用しやすい形になっていて、割と簡単に実現できます。


では、早速見てみましょう!



【パスの認可をチェックするクラスのサンプル】

package utils;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.AccessDeniedException;
import org.springframework.security.intercept.web.FilterSecurityInterceptor;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
/**
 * 認可を模擬するメソッドを提供する。Spring Testのjarが必要。
 * @author admin
 */

public class MySpringSecurityUtils {
 /** 認可を行うフィルタのbean名。
 SpringSecurityのバージョンが上がったときは名称が変わるかも */
 private static String FILTER_NAME = "_filterSecurityInterceptor";
 
 private ServletContext context;
 private HttpServletRequest request;
 private FilterSecurityInterceptor filterSecurityInterceptor;
 
 /**
  * コンストラクタ
  * @param context [in]
  * @param req     [in]現在のセッションなどを保持したリクエスト
  */
 public MySpringSecurityUtils(ServletContext context, HttpServletRequest req){
  this.context = context;
  this.request = req;
 }
 
 
 /**
  * 指定のパスが閲覧許可されるか?
  * 設定したリクエストにあるセッションユーザ情報で認可チェックをする。
  * @param servletPath [in]パス。(例:"/topMenu.do")
  * @return 許可される場合true
  * @see #isAllowedUrl(String, ServletContext, HttpServletRequest, FilterSecurityInterceptor)
  */
 public boolean isAllowed(String servletPath){
  //認可を行うフィルタを取得する
  if(this.filterSecurityInterceptor == null){
   WebApplicationContext wac = getWebApplicationContext(context);
   this.filterSecurityInterceptor
           = (FilterSecurityInterceptor)wac.getBean(FILTER_NAME);
  }
  
  return isAllowedUrl(servletPath, this.context, 
                    this.request, this.filterSecurityInterceptor);
 }
  
 /**
  * 指定のパスが閲覧許可されるか?
  * @param servletPath
  * @param context
  * @param req
  * @return
  * @see #isAllowedUrl(String, ServletContext, HttpServletRequest, FilterSecurityInterceptor)
  */
 public static boolean isAllowedUrl(String servletPath, 
   ServletContext context, HttpServletRequest req){
  
  //ApplicationContextを取得
  WebApplicationContext wac = getWebApplicationContext(context);
  
  //認可を行うフィルタを取得する
  FilterSecurityInterceptor f
           = (FilterSecurityInterceptor)wac.getBean(FILTER_NAME);
  
  //
  return isAllowedUrl(servletPath, context, req, f);
 }
 
 
 /**
  * 指定のパスの認可をチェックする(実処理)
  * 設定したリクエストにあるセッションユーザ情報で認可チェックをする。
  * @param servletPath
  * @param context
  * @param req
  * @return 認可OKのときtrue。
  * @exception RuntimeException 予期せぬエラー
  */
 protected static boolean isAllowedUrl(String servletPath, 
  ServletContext context, HttpServletRequest req, 
  FilterSecurityInterceptor f){
  
  try{
   //何もしないFiterChainを生成する(無名クラス)
   FilterChain fc = new FilterChain(){
    @Override
    public void doFilter(ServletRequest arg0, ServletResponse arg1)
      throws IOException, ServletException {  
    }
   };
   //ダミーのRequestクラスにダミーのデータを設定する
   MockHttpServletRequest mockReq = new MockHttpServletRequest();
   mockReq.setSession(req.getSession());
   mockReq.setMethod(req.getMethod());
   mockReq.setServletPath(servletPath);
   
   //認可のフィルタ実行
   f.doFilter(mockReq, new MockHttpServletResponse(), fc);
   
  }catch(AccessDeniedException e){
   return false;
  }catch(Exception e){
   throw new RuntimeException("認可模擬チェック中に予期せぬエラー。", e);
  }
  
  return true;
 }
 
 
 static protected WebApplicationContext
                  getWebApplicationContext(ServletContext context){
  WebApplicationContext wac = WebApplicationContextUtils
        .getRequiredWebApplicationContext(context);
  return wac;
 }
 
}




【JSPのサンプル】

<%@ page language="java" pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"% >
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags " %>
<%@ page import="utils.MySpringSecurityUtils" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%
MySpringSecurityUtils display

= new MySpringSecurityUtils(session.getServletContext(), request);
%>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" href="css/default.css" type="text/css" />

<title>トップページ</title>
</head>
<body>
<a href="logout">ログアウト</a>
<br />


<br />
トップページです!
<br />
:
<br />
<c:if test="<%=display.isAllowed("/test.do")%>">
<a href="test.do">テスト(自作認可チェックによるリンク表示)</a><br />
</c:if>




【説明】

上記でやっていることは、認可をチェックするクラス(MySpringSecurityUtils)を作成し、

JSP上でnewして使用しているだけです。

使い方はそれ程むずかしくないですよね。

 <c:if test="<%=display.isAllowed("/test.do")%>">

c:ifタグで認可が通るかどうかをチェックして、表示・非表示を制御します!




では、ここからは実装を見ていきましょう。


メインは、以下のメソッドです。

isAllowedUrl(String servletPath,
ServletContext context, HttpServletRequest req, FilterSecurityInterceptor f);


<全体的な動作>

 全体としては、以下のことをしています。

 ①Springの設定ファイル(ApplicationContext)から、認可処理をしているフィルタを取り出します。

  (上記プログラム上ではwac.getBean(FILTER_NAME)の部分)

  取り出しているクラスは、SpringSecurityの設定ファイルのタグでいうと、

  sec:intercept-urlタグにあたるフィルタで、FilterSecurityInterceptorクラスにあたります。

  tomcatのフィルタインターフェースを継承しています。


 ②ダミーのリクエストを作成し、本物のリクエストのセッションを設定します。

  (上記プログラム上ではMockHttpServletRequestにあたります)

  セッション内にログイン情報が入っているからです。


 ③②のダミーリクエストを引数にして、FilterSecurityInterceptor.doFilter()を呼び出します。

  (上記プログラム上ではf.doFilter(mockReq, new MockHttpServletResponse(), fc);

  このクラスの実装では、認可に失敗するとAccessDeniedException例外が発生します。



SpringSecurityが用意しているクラスをそのまま使用しているのが分かるでしょうか。

Springは再利用しやすく、なかなかつくりが良いですねー。



<フィルターについて>

SpringSecurityでは、ログイン機能、認証機能、認可機能、ログアウト機能、etc、のように、機能を分解して、その1つ1つをフィルターとして実装しています。

そしてそれらのフィルターをチェーンして、順番に実行していきます。

今、これらの機能のうちで流用したい機能は認可機能です。

そこでそれを取り出せばいいのですが、取り出すにはどうしたらよいのでしょう??


実はこれら1つ1つはSpringのApplicationContextに設定されていて、bean名も固定で決まっているのです。

FilterSecurityInterceptorは、"_filterSecurityInterceptor"というbean名です。

この名称で取り出しています。



【補足】

SpringSecurityには他にもフィルターがあり、拡張することもできます。

また、自作したフィルターを既存のフィルタと入れ替えたり、指定の場所に入れ込んだりできます。

かなり拡張性が高く設計されています!

こうした機能拡張や設定はまた別の記事で見ていこうと思います。



参考:

・ロールによって画面のリンクの表示/非表示を制御するには?