Interceptorでユーザーログインやセッションの管理をする
今日は日本語で、Interceptorを使ってどうやってログインやセッションを管理するかを記録。
問題点:
1. Interceptorを使う際のinjectionエラー
2. そのinterceptorのTestを書くときのエラー
3. 多数のinterceptorを使う際の順番問題
まずは全体的のclassを紹介:
SessionInterceptor
public class SessionInterceptor extends HandlerInterceptorAdapter {
@Autowired
SessionHandler sessionHandler;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(true);
sessionHandler.createSession(session);
return true;
}
}
SessionInterceptorConfig
@Configuration
public class SessionInterceptorConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(sessionInterceptor()).addPathPatterns("/**");
}
}
わかりやすいように書いてるので、コードの中身はほぼ抜いた。
簡単に説明すると、これはInterceptorを使用する際の基本で、InterceptorはConfigファイルでこのように設定してあげないと使えない。
ここで一つ目のエラーが出る。
問題点1
エラーは何処かと言うと、すごく面白くて、
sessionHandler.createSession(session);
にヌルポエラーが出る。
What the hell???
って思う。
解決策:どうしてこのようなエラーが出るかというと、@AutowiredをされたsessionHandlerを使うためにはInterceptorをそもそもInjectしないとダメ、これ重要。
なので、上のSessionInterceptorConfigをこうする↓
@Configuration
@ComponentScan
public class SessionInterceptorConfig extends WebMvcConfigurerAdapter {
@Bean
public SessionInterceptor sessionInterceptor() {
return new SessionInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(sessionInterceptor())
.addPathPatterns("/**");
}
}
ちなみに:これはできない↓
SessionInterceptorConfig
@Configuration
@ComponentScan
public class SessionInterceptorConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SessionInterceptor())
.addPathPatterns("/**");
}
}
SessionInterceptor
@Component
public class SessionInterceptor extends HandlerInterceptorAdapter {
@Autowired
SessionHandler sessionHandler;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
sessionHandler.createSession(session);
return true;
}
}
@Componentと@Beanを根本的に違うみたい。
基本Configでinjectする際は@Beanを使おう。
ということで、このSessionInterceptorの実装が終わったのでこれのunitテストを書こう!
これがTest、名前はSessionInterceptorTest
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class SessionInterceptorTest {
private MockHttpServletRequest mockHttpServletRequest;
private MockHttpServletResponse mockHttpServletResponse;
private Object handler;
private SessionInterceptor sessionInterceptor;
@Before
public void setup() {
mockHttpServletRequest = new MockHttpServletRequest();
mockHttpServletResponse = new MockHttpServletResponse();
handler = new Object();
sessionInterceptor = new SessionInterceptor();
}
@Test
public void preHandleTest() throws Exception {
//make sure this mockRequest doesn't have session yet
HttpSession httpSession = mockHttpServletRequest.getSession(false);
assertNull(httpSession);
//test preHandle method
sessionInterceptor.preHandle(mockHttpServletRequest, mockHttpServletResponse, handler);
}
}
で、これを実行するとやはりヌルポが出る、しかもこの
sessionInterceptor.preHandle(mockHttpServletRequest, mockHttpServletResponse, handler);
のところで。
基本、上の実装をしたので少ない経験から言うと、これは多分injectionの問題が大きい。なるほど、この行のsessionInterceptorは@Autowiredじゃない!なので以下が正しいコード↓
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class SessionInterceptorTest {
private MockHttpServletRequest mockHttpServletRequest;
private MockHttpServletResponse mockHttpServletResponse;
private Object handler;
@Autowired
SessionInterceptor sessionInterceptor;
@Before
public void setup() {
mockHttpServletRequest = new MockHttpServletRequest();
mockHttpServletResponse = new MockHttpServletResponse();
handler = new Object();
}
@Test
public void preHandleTest() throws Exception {
//make sure this mockRequest doesn't have session yet
HttpSession httpSession = mockHttpServletRequest.getSession(false);
assertNull(httpSession);
//test preHandle method
sessionInterceptor.preHandle(mockHttpServletRequest, mockHttpServletResponse, handler);
}
}
これで正常に動いた。
見やすくしてるためコードの中身は抜いてるが、基本このSessionInterceptorでは初回アクセスのsessionをDB入れるようにしている。そして二回目からのアクセスはすでにセッションが作られているのでここはスキップされる。
これであと一つ、ユーザーがログインしているかのinterceptorを作りたい。
目的としては、もしユーザーがログインしている場合はそのままcontrollerにアクセスするようにして、してない場合はinterceptorから直接loginのcontrollerにjumpさせる。こうすることで全てのControllerクラスはログインの判断をする必要がなくなり、ユーザーがすでに正常にログインされているとの状態での処理だけが必要となる。
PS:関係ない話だが、ここはひとつのinterceptorにしても良かった。でも今回は出来るだけ分けてやりたいので二つ作った。
なので、このLoginInterceptorを作ってみた。
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Value("${endpoint.URL}")
private String hostURL;
@Autowired
SessionHandler sessionHandler;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
//when to check if user is login, check the DB if session attribute exists by session name
if (sessionHandler.fetchUserLoginSessionAttribute(session.getId()) == null) {
response.sendRedirect(hostURL + "/userLogin");
return false;
}
return true;
}
}
しかしここで第三の問題
このLoginInterceptorは
HttpSession session = request.getSession(false);
の次に直接sessionがnullじゃない前提で行動しているので、SessionInterceptorは必ずこのLoginInterceptorの前に実行されなければならない。
そこで調べたところ、以下の二つを参考に@Orderを使うことにした。
Spring MVC(+Spring Boot)上でのリクエスト共通処理の実装方法を理解する - Qiita
How to define the execution order of interceptor in Spring Boot application? - Stack Overflow
そしてこれがLoginSessionConfig
@ComponentScan
@Configuration
@Order(Ordered.LOWEST_PRECEDENCE)
public class LoginInterceptorConfig extends WebMvcConfigurerAdapter {
@Bean
public LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor())
.addPathPatterns("/login/**");
}
}
もちろんSessionInterceptorCOnfigにも@Orderをつけた
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SessionInterceptorConfig extends WebMvcConfigurerAdapter {
解決!