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でそのクッキーファイルを読み込む。