SpringでInterceptorを使う

Springでinterceptorについて勉強しましたので記事を書いてみます。

#Interceptorの概要
SpringにおけるInterceptorクラスは、例えば「コントローラが呼ばれる前に何か共通の処理を行うクラスを実装したい」といった際に使うクラスです。
例えばリクエストをマッピングする前にアクセスしてきたユーザを認証する処理を行いたいときなどに使います。

#実装クラス
実装したクラスをざっと説明します。
####Interceptorクラス

```java:TestInterceptor.java
import java.lang.reflect.Method;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.example.annotation.NonAuth;
import com.example.models.TestUser;
import com.example.service.UserPermissionService;

public class TestInterceptor extends HandlerInterceptorAdapter{

	@Autowired
	UserPermissionService service;

    @Override
    public boolean preHandle(
                    HttpServletRequest request,
                    HttpServletResponse response,
                    Object handler) throws Exception {

    	// リクエストがマッピングできていない場合はfalse
    	if( handler instanceof HandlerMethod ) {

    		// @NonAuthが付与されているかどうかをチェックする
    		HandlerMethod hm = (HandlerMethod) handler;
            Method method = hm.getMethod();
            NonAuth annotation = AnnotationUtils.findAnnotation(method, NonAuth.class);
            if (annotation != null) {
                return true;
            }

            // ユーザ認証
	    	HttpSession session = request.getSession(false);
	    	try {
	    		TestUser user = (TestUser)session.getAttribute("user");
	    		if(!service.checkPermission(user)) {
	    			response.sendRedirect("/error");
	    			return false;
	    		}
	    	}catch(NullPointerException e){
	    		response.sendRedirect("/error");
	    		return false;
	    	}
	        return true;
    	}
    	response.sendRedirect("/error");
    	return false;
    }
}

```
・InterceptorクラスではHandlerInterceptorAdaptorを継承しなければなりません。
・preHandle()はコントローラの処理が呼ばれる前に呼ばれるメソッド。他にもコントローラの処理が終わった後に呼ばれるpostHandle()や一連のリクエスト処理が終わった後に呼ばれるafterCompletion()などをOverrideすることができます。
・そもそもNonAuthアノテーションが付与されているメソッドにリクエストをしている場合はtrueを返します(後述)
・上記クラスではUserPermission.checkPermission()でユーザ認証を行い、チェックに弾かれた場合はエラー画面にリダイレクトします(後述)
・そもそも渡されているHandlerがHandlerMethodのインスタンスでない場合はエラー画面にリダイレクトします(後述)

####Configurationクラス

```java:BeanConfiguration.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;

import com.example.interceptor.TestInterceptor;
import com.example.service.TestService;

@Configuration
public class BeanConfiguration {

	@Bean
	public HandlerInterceptor testInterceptor() throws Exception{
		return new TestInterceptor();
	}

}
```
・Interceptorクラスをbean定義します。

####WebMvcConfigクラス

```java:WebMvcConfig.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

	@Autowired
    HandlerInterceptor testInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(testInterceptor);
    }
}
```
・InterceptorはWebMvcConfigurerを継承したクラスでaddInterceptors()によってSpringに認識させてあげる必要があります。
・InterceptorクラスはAutowiredで呼び出します。

####Controllerクラス
```java:TestInterceptorController.java
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.example.annotation.NonAuth;
import com.example.models.TestUser;

@Controller
@RequestMapping("/interceptor")
public class TestInterceptorController {

	@RequestMapping(value="/index",method = RequestMethod.GET)
	@NonAuth
	public String index(Model model, HttpServletRequest request) {

		TestUser user = new TestUser();
		user.setName("Tanaka");

		HttpSession session = request.getSession(false);
		if (session == null){
			session = request.getSession(true);
			session.setAttribute("user", user);
		}else {
			session.setAttribute("user", user);
		}

		return "test/index";
	}

	@RequestMapping(value="/test", method=RequestMethod.GET)
	public String test(Model model) {
		model.addAttribute("msg", "HelloWorld!");
		return "test/test";
	}

}
```
・まずindexにアクセスするとセッションにユーザが登録されます。
・indexではNonAuthアノテーションがついているので問答無用で認証を通過します。
・testにアクセスする場合は認証をクリアしなければなりません。

大まかな実装クラスは以上になります。
#補足
####特定のメソッドはpreHandle()内で必ずtrueを返却したい場合
・前述しましたが、例えばアノテーションを使用する方法があります。
・アノテーションは自分で定義します。
・InterceptorクラスのpreHandle()に入るとで定義したアノテーションが付与されたメソッドであるかどうかを調べています。
・付与されている場合は問答無用でtrueを返却するようにしています。
・以下のサイトを参考にさせていただきました。
[https://qiita.com/dmnlk/items/cce551ce18973f013b36](https://qiita.com/dmnlk/items/cce551ce18973f013b36)

####認証に失敗した際にエラー画面に遷移させたい場合
・Springでエラーハンドリングをしたい場合は、ErrorController(Springで用意されているクラス)を継承したクラスを定義します。


```java:ErrorController
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.annotation.NonAuth;

@Controller
public class ErrController implements ErrorController {

    private static final String PATH = "/error";

    @RequestMapping("/404")
    @NonAuth
    String notFoundError() {
        return "test/error";
    }

    @RequestMapping(PATH)
    @NonAuth
    String home() {
        return "test/error";
    }

    @Override
    @NonAuth
    public String getErrorPath() {
        return PATH;
    }

}
```

・定義しているメソッドでは認証処理を行わないため、NonAuthアノテーションを付与します。
・Interceptorクラスの中でエラー画面をdispatchした後にreturn falseは必ずしないといけないようです。なぜなら、return falseを書かないと、エラー画面への遷移が行われたとしても、handlermethod(マッピングされたcontrollerのメソッド)は起動されてしまうからです。以下のサイトを参考にしました。
[https://stackoverflow.com/questions/44740332/redirect-using-spring-boot-interceptor](https://stackoverflow.com/questions/44740332/redirect-using-spring-boot-interceptor)
上記のサイト中に書かれていることをgoogle翻訳してみます。
>Returns: true if the execution chain should proceed with the next interceptor or the handler itself. Else, DispatcherServlet assumes that this interceptor has already dealt with the response itself.
実行チェーンが次のインターセプタまたはハンドラ自体に進む必要がある場合はtrueです。そうでない場合、DispatcherServletはこのインターセプターが既に応答自体を処理したと見なします。

「そうでない場合...」のところで言っていることがよくわかりませんが、多分DispatcherServletはpreHandleでreturn falesされるとすでにresponseが返されたとみなすのだと思います(return falseしない場合はコントローラのメソッドが起動されてしまうわけですから、「そうでない場合」は「falseの場合」ということでしょうか？ )。何はともあれ、コントローラのメソッドを起動させたくない場合はreturn falseを書けということらしいです。

####preHandle()に渡ってくるHandlerはHandlerMethodとは限らない
・Interceptorクラスの中でhandlerがHandlerMethodかどうかを検証していますが、これはhandlerは必ずしもHandlerMethodではないからです。
・例えばマッピングを想定していないアドレスにリクエストを送ろうとすると、ResourceHttpRequestHandlerが渡ってきます。
・以下のサイトを参考にさせていただきました。
[https://qiita.com/tukiyo320/items/d51ea698c848414b5874](https://qiita.com/tukiyo320/items/d51ea698c848414b5874)


参考：
[springbootでインターセプターを設定する](https://blog.beaglesoft.net/entry/2015/11/18/205745)
[Interceptorでコントローラメソッドの前後に処理を入れる](http://teqspaces.com/Spring/8)
[SpringBootの特定のAnnotationが付与されたControllerのメソッドに対して事前処理を行う](https://qiita.com/dmnlk/items/cce551ce18973f013b36)
[Spring Boot 2.0 (Spring 5) の WebMvcConfigurer覚書](https://qiita.com/yukina-ge/items/98ba5393bcde4df599d5)
[Springbootでエラー画面を表示する](https://qiita.com/t-iguchi/items/45e4b3ed6181a046f462)
[SpringBoot(1.2くらい)を使っていてハマった事[随時更新]
](https://qiita.com/yakumo/items/026fc4274ac2692e4947)[Redirect using Spring boot interceptor](https://stackoverflow.com/questions/44740332/redirect-using-spring-boot-interceptor)
[preHandleに来るmethodがHandlerMethodとは限らない](https://qiita.com/tukiyo320/items/d51ea698c848414b5874)