WebSecurityConfigurerAdapterが非推奨になってた

2022.06.02

SpringBoot 2.7.0でプロジェクトを作ってSpringSecurityを使おうと、いつものようにWebSecurityConfigurerAdapterを継承しようとしたら非推奨という警告が出てました。

画像

この際、SpringSecurityを初歩から学びなおすのと同時に新しい書き方で、インメモリ認証とJDBC認証を書いてみます。

HttpSecurityとインメモリ認証

新しい書き方で書くと以下のようになります

package com.volkruss.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {

    // ログイン後は/homeに遷移させる
    @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers(header -> {
            header.frameOptions().disable();
        });
        http.authorizeHttpRequests(authorize -> {
            authorize.antMatchers("/h2-console/**").permitAll()
                    .anyRequest().authenticated();
        });
        http.formLogin(form -> {
            form.defaultSuccessUrl("/home");
        });
        return http.build();
    }

    // misaka/mikotoでログインする
    @Bean
    public InMemoryUserDetailsManager userDetailsService(){
        UserDetails user = User.withUsername("misaka")
                .password(
                        PasswordEncoderFactories
                                .createDelegatingPasswordEncoder()
                                .encode("mikoto"))
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}
  • 今まではWebSecurityConfigurerAdapterを継承して、configureをメソッドをオーバーライドすることでHttpSecurityなどを設定していました
  • これからはコンポーネントとして設定するようにします

HttpSecurityの設定

  • BeanとしてSecurityFilterChainを登録するようにします
  • HttpSecurityについては今まで通り設定できますが、最終的にはSecurityFilterChainを返すため、http.build()を返します。

インメモリ認証の設定

  • 今まではAuthenticationManagerBuilder#inMemoryAuthenticationで取得できるInMemoryUserDetailsManagerConfigurerに対してUserDetailsを設定していました
  • これからは、InMemoryUserDetailsManagerにUserDetailsを渡してInMemoryUserDetailsManagerを返すようにします(Beanとして登録します)

SecurityFilterChain

  • FilterChainProxyがリストとして持っているFilterの実装クラスです。
  • FilterChainProxyのdoFilterから取得されて利用されます

doFilterからgetFiltersメソッドまで呼ばれる

画像

getFiltersメソッド

private List<Filter> getFilters(HttpServletRequest request) {
	int count = 0;
	for (SecurityFilterChain chain : this.filterChains) {
		if (logger.isTraceEnabled()) {
			logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, ++count,
					this.filterChains.size()));
		}
		if (chain.matches(request)) {
			return chain.getFilters();
		}
	}
	return null;
}
  • FilterChainProxyはspringSecurityFilterChainというBean名でDIコンテナに登録されています
  • DelegatingFilterProxyからFilterChainProxyは利用されます
    • この辺はweb.xmlで書くと明示する必要があります

これで画面からmisaka/mikotoでログインできるようになります

画像

JDBC認証

必要となる以下のテーブルを作成します。デフォルトで利用する場合は、テーブル名も必要なカラム名も指定されています。

  • users
  • authorities

コードを書きます

package com.volkruss.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import javax.sql.DataSource;

@Configuration
public class SecurityConfiguration {

    @Autowired
    private DataSource dataSource;

    // ログイン後は/homeに遷移させる
    @Bean
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers(header -> {
            header.frameOptions().disable();
        });
        http.authorizeHttpRequests(authorize -> {
            authorize.antMatchers("/h2-console/**").permitAll()
                    .anyRequest().authenticated();
        });
        http.formLogin(form -> {
            form.defaultSuccessUrl("/home");
        });
        return http.build();
    }

    @Bean
    public UserDetailsManager userDetailsService(){
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(this.dataSource);
        // データベースにshokuhou/misakiというレコードが追加されます
        UserDetails user = User.withUsername("shokuhou")
                .password(
                PasswordEncoderFactories
                        .createDelegatingPasswordEncoder()
                        .encode("misaki"))
                .roles("USER")
                .build();
        users.createUser(user);
        return users;
    }
}

  • DataSourceをAutowiredしているのでapplication.yamlにデータベースの接続情報を記載しておきます
  • JdbcUserDetailsManagerというのはJdbcDaoImplを継承した拡張クラスです
    • またUserDetailsManagerの実装クラスです
  • UserDetailsをcreateUserメソッドに渡すと
    • そのUserDetailsがusersテーブルにインサートされる
    • usernameに紐づく認可レコードがauthoritiesテーブルにインサートされる
  • createUserで使われるSQLはJdbcUserDetailsManagerクラスのcreateUserSqlに登録されています

実際にインサートされたレコード

画像

※misakaは予め入れておいたレコードです

これでshokuhou/misakiでログインできます

既存レコードでログイン

既存レコードというのは、ここでいうmisakaです。ソースコードでshokuhouというユーザーをcreateしましたが、データベースに既に登録されているレコードでログインを試します。

この時、ログインさせる条件で大事なことが以下でした

  • パスワードがハッシュ化されていること
  • authoritiesテーブルにも紐づくレコードが登録されていること
@Bean
public UserDetailsManager userDetailsService(){
    // 既存User : misaka/mikoto
    JdbcUserDetailsManager users = new JdbcUserDetailsManager(this.dataSource);
    return users;
}
  • データソースを渡すだけです
    • 後はデータベースに紐づくレコードを取得してくれます

その他

もしもusersテーブルやauthoritiesテーブルでなく独自のテーブルを使いたい場合は以下のようにsetUsersByUsernameQueryメソッドなどを使用します

@Bean
public UserDetailsManager userDetailsService(){

   String USERQUERY =
           "select username,password,enabled from myusers where username = ?";
   String AuthoritiesQuery =
           "select username,role from myauthorities where username = ?";

    JdbcUserDetailsManager users = new JdbcUserDetailsManager(this.dataSource);

    //  独自のテーブルやカラム名を使いたい時
    users.setUsersByUsernameQuery(USERQUERY);
    users.setAuthoritiesByUsernameQuery(AuthoritiesQuery);

    return users;
}
  • 既存のusersテーブルからmyusersテーブルに変更してログイン可能になります
  • カラムも独自に設定したカラム名でログインができます
  • 同じようにログインできる条件
    • ハッシュ化されたパスワードであること
    • authorities(ここではmyauthorities)に紐づくレコードがあること

これで基本的な設定などはできるようになります。

今回のコードはGithubにもあげております

https://github.com/jirentaicho/laravel-new-SecurityConfiguration

使用したSQLは以下です(H2にて実行時に構築しています)

DROP TABLE IF EXISTS users;

CREATE TABLE "users" (
	"id" SERIAL NOT NULL,
	"username" varchar(64) NOT NULL PRIMARY KEY,
	"password" TEXT NOT NULL,
	"enabled" BOOLEAN NOT NULL
)
;

DROP TABLE IF EXISTS myusers;

CREATE TABLE "myusers" (
	"id" SERIAL NOT NULL,
	"username" varchar(64) NOT NULL PRIMARY KEY,
	"password" TEXT NOT NULL,
	"enabled" BOOLEAN NOT NULL
)
;

DROP TABLE IF EXISTS myauthorities;

CREATE TABLE "myauthorities" (
	"id" SERIAL NOT NULL,
	"username" varchar(64) NOT NULL PRIMARY KEY,
	"role" TEXT NOT NULL
)
;

DROP TABLE IF EXISTS authorities;

CREATE TABLE "authorities" (
	"id" SERIAL NOT NULL,
	"username" varchar(64) NOT NULL PRIMARY KEY,
	"authority" TEXT NOT NULL
)
;
-- 予め用意しているユーザーレコードを使う場合はハッシュ化されたパスワードを利用することが必須です(これはパスワードmikotoです)
-- またauthoritiesテーブルにもusernameに紐づくレコードが登録されていないとログインできません
INSERT INTO "users" ("username", "password","enabled") VALUES ('misaka', '{bcrypt}$2a$10$t.uzqGjte5WsosZO4wFV5OULSCqYJbn0qC5Lh0Uilj5hF.biixBoG', true);
insert into "authorities" ("username","authority") values ('misaka','level5');

-- 独自に定義したテーブルでJDBC認証を行う場合
INSERT INTO "myusers" ("username", "password","enabled") VALUES ('misaka', '{bcrypt}$2a$10$t.uzqGjte5WsosZO4wFV5OULSCqYJbn0qC5Lh0Uilj5hF.biixBoG',true);
insert into "myauthorities" ("username","role") values ('misaka','level5')

かなりぐちゃぐちゃになっているが奮闘記録もあります

https://zenn.dev/misaka/scraps/3ce785913f1bc3

参考

  • https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/web/FilterChainProxy.html
  • https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
  • https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/jdbc.html