S-JIS[2017-09-20] 変更履歴

Spring Boot Rest Basic認証

Spring BootのRest APIのBasic認証のサンプル。


概要

Rest API用のBasic認証も、ウェブアプリケーションのBasic認証と同じ。


Basic認証とForm認証を混在させる例1

ウェブアプリケーションをForm認証、Rest APIをBasic認証にする例。(Spring Boot 1.5.6、Spring Security 4.2.3)

「/api/」で始まるURI(Rest API)をBasic認証とし、それ以外(ウェブ)をForm認証にする。
この為には、Basic認証用のConfigクラスとForm認証用のConfigクラスを別々に用意する。
(→ひとつのクラス内に両方書く例
なお、同じ(優先度の)Configクラスが2つあるとSpring Boot起動時にエラーになるので、優先順位を付ける必要がある。

src/main/java/com/example/demo/auth/both/WebSecurityBasicConfig.java:

package com.example.demo.auth.both;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.password.PasswordEncoder;
/**
 * Basic認証
 */
@EnableWebSecurity
@Order(Ordered.HIGHEST_PRECEDENCE)
public class WebSecurityBasicConfig extends WebSecurityConfigurerAdapter {

@OrderアノテーションでConfigの優先度を付ける。
もうひとつのWebSecurityFormConfigより優先度が高ければいいので、ここではHIGHEST_PRECEDENCEにしている。

	@Autowired
	private UserDetailsService userDetailsService;

	@Autowired
	private PasswordEncoder passwordEncoder;
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.antMatcher("/api/**"); // コンフィグを適用するURI

		http.authorizeRequests().anyRequest().authenticated(); // 適用された全リクエストに対して認証を要求
		http.httpBasic(); // Basic認証
		http.csrf().disable();
	}

configureメソッド内の冒頭にあるhttp.antMatcherメソッド(実体はhttp.requestMatcher(new AntPathRequestMatcher("/api/**"))と同じ)が重要。
これにより、ブラウザーからのリクエストのURIが「/api/」で始まるときだけ当コンフィグが使われる。
(この状態なら、authorizeRequests().anyRequest().authenticated()で全リクエストに対しての設定と宣言しても、事前に「/api/」で絞られているので実質的には「/api/」だけが対象となる)

なお、http.antMatcher()を使わずhttp.authorizeRequests().antMatcher("/api/**").authenticated()とした場合、全リクエストに対して当コンフィグが使われ、「/api/」だけ認証対象となる。
この為、「/api/」以外のURIでも当コンフィグが使われ、もうひとつのWebSecurityFormConfig(で作られるフィルター)は使われない。

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
	}
}

WebSecurityBasicConfigに@Orderアノテーションを付けていないと、Spring Boot起動時に以下のような例外が発生する。

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Injection of autowired dependencies failed; nested exception is java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on com.example.demo.auth.both.WebSecurityBasicConfig$$EnhancerBySpringCGLIB$$a3f8eeb6@69bbb02a, so it cannot be used on com.example.demo.auth.both.WebSecurityFormConfig$$EnhancerBySpringCGLIB$$6e25cc92@73a173d2 too.
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:372) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:761) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867) ~[spring-context-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543) ~[spring-context-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) ~[spring-boot-1.5.6.RELEASE.jar:1.5.6.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693) [spring-boot-1.5.6.RELEASE.jar:1.5.6.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360) [spring-boot-1.5.6.RELEASE.jar:1.5.6.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:303) [spring-boot-1.5.6.RELEASE.jar:1.5.6.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118) [spring-boot-1.5.6.RELEASE.jar:1.5.6.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107) [spring-boot-1.5.6.RELEASE.jar:1.5.6.RELEASE]
	at com.example.demo.DemoApplication.main(DemoApplication.java:10) [bin/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_112]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_112]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_112]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_112]
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-1.5.6.RELEASE.jar:1.5.6.RELEASE]
Caused by: java.lang.IllegalStateException: @Order on WebSecurityConfigurers must be unique. Order of 100 was already used on com.example.demo.auth.both.WebSecurityBasicConfig$$EnhancerBySpringCGLIB$$a3f8eeb6@69bbb02a, so it cannot be used on com.example.demo.auth.both.WebSecurityFormConfig$$EnhancerBySpringCGLIB$$6e25cc92@73a173d2 too.
	at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.setFilterChainProxySecurityConfigurer(WebSecurityConfiguration.java:148) ~[spring-security-config-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_112]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_112]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_112]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_112]
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:701) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366) ~[spring-beans-4.3.10.RELEASE.jar:4.3.10.RELEASE]
	... 22 common frames omitted

Form認証の方はウェブ用のForm認証と全く同じ。

src/main/java/com/example/demo/auth/both/WebSecurityFormConfig.java:

package com.example.demo.auth.both;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.util.matcher.AntPathRequestMatcher;
/**
 * フォーム認証
 */
@EnableWebSecurity
public class WebSecurityFormConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private UserDetailsService userDetailsService;
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests() //
			.antMatchers("/login*").permitAll() // ログイン画面
			.anyRequest().authenticated(); // その他の全リクエストに対して認証を要求
		http.formLogin() //
			.loginPage("/login").usernameParameter("user").passwordParameter("password") // ログイン画面
			.successForwardUrl("/") // ログイン成功時に表示するURL(これをセットしない場合、ユーザーがログイン前に要求したURLに跳ぶ)
			.failureForwardUrl("/login-fail") // ログイン失敗時に遷移するパス
			.permitAll();
		http.logout() //
			.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // logoutUrl()はPOSTに対応していない
			.logoutSuccessUrl("/login") // ログアウト成功時に表示するURL
			.deleteCookies("JSESSIONID").invalidateHttpSession(true).permitAll();
	}
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
	}

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

csrf.disable

上記のコードでは、Rest API用の定義でcsrf().disable()を指定している。

これを付けないと、POSTメソッドのAPIをクライアントから要求したときに以下のようなエラーが返る。

$ curl http://127.0.0.1:8080/api/example -X POST -u hishidama:hoge
{"timestamp":1504004963795,"status":403,"error":"Forbidden","message":"Could not verify the provided CSRF token because your session was not found.","path":"/api/example"}

Basic認証とForm認証を混在させる例2

ウェブアプリケーションをForm認証、Rest APIをBasic認証にする例。(Spring Boot 1.5.6、Spring Security 4.2.3)

上記の例ではBasic認証用のConfigとForm認証用のConfigを別々のクラスにしたが、ひとつのクラスで記述することも出来る。

src/main/java/com/example/demo/auth/WebSecurityMultiConfig.java:

package com.example.demo.auth.both;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.util.matcher.AntPathRequestMatcher;
/**
 * 認証
 */
@EnableWebSecurity
public class WebSecurityMultiConfig {

	@Autowired
	private UserDetailsService userDetailsService;
	/**
	 * Basic認証
	 */
	@Configuration
	@Order(Ordered.HIGHEST_PRECEDENCE)
//	@Order(1)
	public class WebSecurityBasicConfig extends WebSecurityConfigurerAdapter {

		// この中は最初の例のWebSecurityBasicConfigと同じ

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http.antMatcher("/api/**"); // コンフィグを適用するURI
			http.authorizeRequests().anyRequest().authenticated(); // 全リクエストに対して認証を要求
			http.httpBasic(); // Basic認証
			http.csrf().disable();
		}

		@Override
		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
		}
	}
	/**
	 * Form認証
	 */
	@Configuration
//	@Order(2)
	public class WebSecurityFormConfig extends WebSecurityConfigurerAdapter {

		// この中は最初の例のWebSecurityFormConfigと同じ

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http.authorizeRequests() //
				.antMatchers("/login*").permitAll() // ログイン画面
				.anyRequest().authenticated(); // その他の全リクエストに対して認証を要求
			http.formLogin() //
				.loginPage("/login").usernameParameter("user").passwordParameter("password") // ログイン画面
				.successForwardUrl("/") // ログイン成功時に表示するURL(これをセットしない場合、ユーザーがログイン前に要求したURLに跳ぶ)
				.failureForwardUrl("/login-fail") // ログイン失敗時に遷移するパス
				.permitAll();
			http.logout() //
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // logoutUrl()はPOSTに対応していない
				.logoutSuccessUrl("/login") // ログアウト成功時に表示するURL
				.deleteCookies("JSESSIONID").invalidateHttpSession(true).permitAll();
		}

		@Override
		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
		}
	}
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

クライアントにcurlを使う例

UNIXのcurlコマンドでは、-uでユーザーID(とパスワード)を指定して認証を行う事が出来る。

$ curl http://127.0.0.1:8080/api/example -X GET -u hishidama:hoge

ちなみに、-iでレスポンスの内容、-vでリクエストとレスポンスの内容も表示される。


毎回ユーザーIDとパスワードを指定するのではなく、初回のみ指定し、クッキーにセッションを保持しておくことも出来る。

$ curl http://127.0.0.1:8080/api/example -u hishidama:hoge -c cookie.txt
$ curl http://127.0.0.1:8080/api/example -b cookie.txt

-cでクッキーをファイルに保存し、-bでそのクッキーファイルを読み込む。


Spring Bootへ戻る / Spring Frameworkへ戻る / 技術メモへ戻る
メールの送信先:ひしだま