Spring Security | JWT認証 – トークンの取得を行う

2022.08.18

以前は古いSpring Securityのバージョンで書いたので、今回は5.7のバージョンで実装を行っていきます。

とりあえず前回までのプログラムを利用します。

手順

以下の手順で実装していきます。細かいですが、UsernamePasswordAuthenticationFilterを継承以外は軽微なモノです。

  • java-jwtの依存追加
  • UsernamePasswordAuthenticationFilterを継承
  • Formクラスの作成
  • InMemoryUserDetailsManagerでusernameとpasswordを設定
  • AuthenticationManagerをBean化
  • 作成したFilterを設定

java-jwtの依存追加

java-jwtのページ参考に依存関係を追加します。今回はgradleなのでgradleを参考に入れています。全ての依存はこんな感じになっていますが、h2とjdbcは使う予定はありませんので無くてもOKです。

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'com.auth0:java-jwt:4.0.0'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

UsernamePasswordAuthenticationFilterを継承したクラスの作成

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager){
        this.authenticationManager = authenticationManager;
        // ログインパスの指定
        setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/login","POST"));
        // ログインパラメータの設定
        setUsernameParameter("username");
        setPasswordParameter("password");
        // ログイン成功時にtokenを発行してレスポンスにセットする
        this.setAuthenticationSuccessHandler((req,res,ex) -> {
            // JWTトークンの作成
            String token = JWT.create()
                    .withIssuer("com.volkruss.toaru")
                    .withClaim("username",ex.getName())
                    .sign(Algorithm.HMAC256("secret"));
            // HeaderにX-AUTH-TOKENというKEYで生成したトークンを付与する
            res.setHeader("X-AUTH-TOKEN",token);
            res.setStatus(200);
        });

        //ログイン失敗時
        this.setAuthenticationFailureHandler((req,res,ex) -> {
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        });

    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            ServletInputStream servletInputStream = request.getInputStream();
            // あとで作成するLoginFormクラスを、リクエストのパラメータとマッピングして作成する
            LoginForm form = new ObjectMapper().readValue(request.getInputStream(),LoginForm.class);
            // 作成したLoginFormクラスの内容でログインの実行をする
            return this.authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(form.username,form.password,new ArrayList<>())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  • コメントに記載の通りです

Formクラスの作成

ログインフォームとマッピングできるFormクラスを作成します。今回は以下のパラメータで作成します

  • username
  • password
public class LoginForm {
    public String username;
    public String password;
}
  • インナークラスで作成するとエラーになります

InMemoryUserDetailsManagerでusernameとpasswordを設定

認証ユーザー情報です。ここはDB認証などのやり方もありますが、今回は簡単にインメモリ認証で済ましてしまいます。

@Configuration
public class SecurityConfig {

...省略

    @Bean
    public InMemoryUserDetailsManager userDetailsManager(){
        UserDetails user = User.withUsername("misaka")
                .password(
                        PasswordEncoderFactories
                                .createDelegatingPasswordEncoder()
                                .encode("mikoto"))
                                .roles("USER")
                                .build();
        return new InMemoryUserDetailsManager(user);
    }
}

AuthenticationManagerをBean化

@Configuration
public class SecurityConfig {

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

...省略

  • 今までは継承していたWebSecurityConfigurerAdapter#authenticationManagerメソッドで取得できましたが、Bean化して取得するようにします
    • ここは参考サイトを参照しました

作成したFilterを設定

@Configuration
public class SecurityConfig {

...省略

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        // ログインが必須なページを修正
        http.authorizeRequests(auth -> {
            auth.antMatchers("/get").permitAll();
            auth.antMatchers("/api/login").permitAll();
            auth.antMatchers("/post").authenticated();
        });
        http.cors().configurationSource(corsConfigurationSource());
        // 作成したFilterを設定
        http.addFilter(new JwtAuthenticationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class))));
        return http.build();
    }
    
...省略

}
  • ログインページはログイン無しでアクセスするためにpermitAllとしています
  • 作成したJwtAuthenticationFilterクラスを生成してaddFilterします
    • AuthenticationManagerについては参考サイトを参照しました

確認

postmanで確認してみます。

まずは間違ったユーザー情報でログインに失敗する様子です

画像

画像

  • 401エラーになっています
  • Headersにトークンの設定がされていません

次に正しいユーザー情報を送信します

画像

  • ステータスも200OKになっています
  • X-AUTH-TOKENにトークンが設定されています

これでログインしてトークンを取得することができるようになりました次回はJavaScriptからログインを試してみます。

参考

https://stackoverflow.com/questions/71281032/spring-security-exposing-authenticationmanager-without-websecurityconfigureradap