Edited at

WebAPI(RestController)ベースのSpringBootアプリにHTTPセッションログイン

More than 1 year has passed since last update.


ポイントまとめ


  • ログイン/ログアウトのエンドポイントはSpringSecurityの設定で自動的に作成される(自分でRestController、RequestMappingする必要はない)

  • 必要になるまでCSRFのFilterは無効化しておくと吉

  • Webページからのユーザ/パスワード送信は”Formデータ”としてPOST送信する


コード


バージョン



  • SpringBoot: 1.5.7.RELEASE


  • SpringSecurity: 4.2.3.RELEASE

Webページ側もねんのため。



  • Angular: 4.4.6


SpringSecurityの依存追加

これだけでOKです。

  compile('org.springframework.boot:spring-boot-starter-security')

参考までに、実装時点でのbuild.gradleこちら


SpringSecurityの設定


import org.enlightenseries.DomainDictionary.presentation.config.security.AuthenticationFailureHandler;
import org.enlightenseries.DomainDictionary.presentation.config.security.AuthenticationLogoutSuccessHandler;
import org.enlightenseries.DomainDictionary.presentation.config.security.AuthenticationSuccessHandler;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

import javax.annotation.PostConstruct;

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

private final AuthenticationManagerBuilder authenticationManagerBuilder;

private final UserDetailsService userDetailsService;

public WebSecurityConfiguration(
// Point.1
AuthenticationManagerBuilder _authenticationManagerBuilder,
// Point.2
UserDetailsService _userDetailsService
) {
this.authenticationManagerBuilder = _authenticationManagerBuilder;
this.userDetailsService = _userDetailsService;
}

@PostConstruct
public void init() {
try {
// Point.3
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/**.{js,html}")
.antMatchers("/h2-console/**");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// Point.4
http.csrf()
.disable();

// Point.5
http.formLogin()
.loginProcessingUrl("/api/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
.permitAll();

// Point.6
http.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(authenticationLogoutSuccessHandler())
.permitAll();

http.authorizeRequests()
.antMatchers(HttpMethod.GET).permitAll()
.antMatchers("/api/**").authenticated();
}

@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler();
}

@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new AuthenticationFailureHandler();
}

@Bean
public AuthenticationLogoutSuccessHandler authenticationLogoutSuccessHandler() {
return new AuthenticationLogoutSuccessHandler();
}
}


Point.1

AuthenticationManagerBuilderのBeanは、特にどこに記述しなくても、SpringSecurityが勝手に用意してくれてます。Injectして必要な設定(Point.3参照)を施すだけでOKです。


Point.2

UserDetailsServiceはSpringのUserDetailsServiceインタフェースを実装したカスタムクラスです。

ログインのエンドポイントに送られたユーザ名から、DBなど任意の方法で保存した既存ユーザを取得する処理を記述します。

ここではDBアクセスせずにダミーのユーザを返します。

@Component

public class MyUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!username.equals("admin")) {
return null;
}

List<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList("ADMIN");

// ユーザ名"admin"、パスワード"password" でログインできるダミーのユーザを返します。
return new org.springframework.security.core.userdetails.User(
"admin",
//パスワードの文字をエンコードした文字を用意しておきます(本来エンコード済み文字がDB等に保存される)
new BCryptPasswordEncoder().encode("password"),
grantedAuthorities
);
}

}


Point.3

AuthenticationManagerBuilderに、ユーザ取得処理としてカスタムしたUserDetailsServiceと、パスワードの暗号化に使用するエンコーダ(ここではBCryptPasswordEncoderを使用しています)を設定します。

これで「Webページから送信されたユーザをDBから探し、パスワードを照合する」という処理をSpringSecurtyに任せることができます。


Point.4

次の処理でCSRFのFilterを無効化しておきます。

http.csrf().disable();

CSRFのFilterはデフォルトでは有効になっているため、ログイン/ログアウトのためのPOST送信にCSRFトークンのチェックが入ります。

ブラウザに余計なCookieが残っていると、次のような403エラーが発生してログイン/ログアウト処理がうまく検証できません。

Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.


Point.5

ログインのエントリポイント、検証時の振る舞いを定義します。


  • ここでは、ログイン時は(localhost:8080)/api/loginに向けてPOST送信するように構成します。

    http.formLogin()

.loginProcessingUrl("/api/login")



  • usernameParameterpasswordParameterで、送信するフォームパラメータの名前を指定します。

      .usernameParameter("username")

.passwordParameter("password")


  • デフォルトではログイン処理のレスポンスに、ステータス302が返されてしまいます。ここでは成功時は200だけ、失敗時は401だけを返すようにカスタマイズを行います。

      .successHandler(authenticationSuccessHandler())

.failureHandler(authenticationFailureHandler())

  @Bean

public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler();
}

@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new AuthenticationFailureHandler();
}


import org.springframework.security.core.Authentication;

import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
}

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed.");
}
}



Point.6

ログアウトのエントリポイントは(localhost:8080)/api/logoutを指定します。

ここにむけてPOST送信を行えばOKです。

また、ログアウト処理のレスポンスも302にならないように構成します。

      .logoutSuccessHandler(authenticationLogoutSuccessHandler())

  @Bean

public AuthenticationLogoutSuccessHandler authenticationLogoutSuccessHandler() {
return new AuthenticationLogoutSuccessHandler();
}


import org.springframework.security.core.Authentication;

import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class AuthenticationLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
}


SpringBootアプリ側は、これでOKです。自前のRequestMapping等を作成する必要はありません。


Webページからのリクエスト

注意するのは1つだけ、FormデータとしてPOST送信する、ということだけです。

今回はAngularで作成していましたので、そのコードを記載します。

  username: string;

password: string;

login() {
const data = 'username=' + encodeURIComponent(this.username) +
'&password=' + encodeURIComponent(this.password);
const headers = new HttpHeaders ({
'Content-Type': 'application/x-www-form-urlencoded'
});

this.http.post('/api/login', data, { headers: headers })
.subscribe(...);
}

      <input type="text" name="username" [(ngModel)]="username" required>

<input type="password" name="password" [(ngModel)]="password" required>


動作確認

ログインに成功すると、ステータス200とともに、リクエストヘッダにSet-Cookie:JSESSIONID=ながーいランダム文字列が返ってきます。

これでセッション認証OKです。

ログアウト時にSet-Cookie:JSESSIONID=(Cookieのクリア)は返ってきません。サーバ側のメモリ上だけでクリアされるようです。


参考