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