Spring BootのRest APIのBasic認証のサンプル。
Rest API用のBasic認証も、ウェブアプリケーションのBasic認証と同じ。
ウェブアプリケーションを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起動時にエラーになるので、優先順位を付ける必要がある。
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認証と全く同じ。
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();
}
}
上記のコードでは、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"}
ウェブアプリケーションをForm認証、Rest APIをBasic認証にする例。(Spring Boot 1.5.6、Spring Security 4.2.3)
上記の例ではBasic認証用のConfigとForm認証用のConfigを別々のクラスにしたが、ひとつのクラスで記述することも出来る。
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();
}
}
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でそのクッキーファイルを読み込む。