Spring Security | JWTトークンの検証を行う
2022.08.18今回はログイン時に取得したJWTトークンを利用したログインを行います。
発行されたトークンを保存するのにローカルストレージを利用します。
Filterの作成
OncePerRequestFilterを利用してリクエストの発生に対して、1回実行される処理を定義できますここでリクエストのヘッダーからTokenを取得して、適切なTokenかどうかを判断して、認証済としてあげます
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
public class LoginFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// headersのkeyを指定して値(トークン)を取得する
String value = request.getHeader("X-AUTH-TOKEN");
if(value == null || !value.startsWith("Bearer ")){
filterChain.doFilter(request,response);
return;
}
String token = value.substring(7);
// tokenの検証と認証を行う
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256("secret")).build().verify(token);
// usernameの取得
String username = decodedJWT.getClaim("username").toString();
// ログイン状態の設定
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username,null,new ArrayList<>()));
filterChain.doFilter(request,response);
}
}
作成したFilterを追加します
@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());
http.addFilter(new JwtAuthenticationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class))));
// JwtAuthenticationFilterの後にフィルターを追加する
http.addFilterAfter(new LoginFilter(), JwtAuthenticationFilter.class);
return http.build();
}
またCORSの設定でレスポンスヘッダーを公開する設定を行います
@Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration cors = new CorsConfiguration();
cors.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
cors.setAllowedMethods(Arrays.asList("GET","POST"));
cors.setAllowedHeaders(Arrays.asList("*"));
cors.setAllowCredentials(true);
// X-AUTH-TOKENのレスポンスヘッダーを公開します → jsで取得できるようになる
cors.addExposedHeader("X-AUTH-TOKEN");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",cors);
return source;
}
ヘッダーにトークンを付与する
トークンを保持する必要がありますが、簡単に値を保存できるローカルストレージを使います
ログイン後に取得できるX-AUTH-TOKENの値をローカルストレージに保存します
// ログイン処理
function dologin(){
const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');
fetch('http://localhost:8080/api/login',{
method:'POST',
credentials: 'include',
headers: {
'X-XSRF-TOKEN' : csrfToken
},
body : JSON.stringify({
'username' : 'misaka',
'password': 'mikoto'
})
})
.then( res => {
// ローカルストレージに保存する
localStorage.setItem('jwt-token', 'Bearer ' + res.headers.get('X-AUTH-TOKEN'))
})
}
これでログインをするとローカルストレージにトークンが保存されているのがわかります
この値を利用してトークンをヘッダーに付与してリクエストします
function dopost(){
const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');
console.log(csrfToken);
fetch('http://localhost:8080/post',{
method:'POST',
credentials: 'include',
headers: {
'X-XSRF-TOKEN' : csrfToken,
// jwt-tokenの値をローカルストレージから取得して付与する
'X-AUTH-TOKEN' : localStorage.getItem('jwt-token')
},
})
.then(res => res.text())
.then(str => console.log(str))
}
これでjs側からヘッダーにX-AUTH-TOKENというkeyでトークンを付与してリクエストできるようになりました
正しくトークンの検証が行えればユーザー名の取得もできるようになっています
トークンの改変
試しにトークンの値を改変してリクエストを送ってみます
サーバー側の問題としてエラーになっています。SpringでもJWTトークンのデコードエラーになっています
次にnullでも認証済になってしまうので修正します
nullで送ってもPOSTが成功しているのでcookieにJSESSIONIDがあり、そもそも認証済として扱われているから
フィルターを修正します
public class LoginFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String value = request.getHeader("X-AUTH-TOKEN");
if(value == null || !value.startsWith("Bearer ")){
// 認証情報を除去する
SecurityContextHolder.getContext().setAuthentication(null);
filterChain.doFilter(request,response);
return;
}
String token = value.substring(7);
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256("secret")).build().verify(token);
String username = decodedJWT.getClaim("username").asString();
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username,null,new ArrayList<>()));
filterChain.doFilter(request,response);
}
}
認証情報にnullを設定します
これでトークンをnullにして送信すると一度ログインしても認証ユーザーとして扱われることはなくなりました
これでJWTトークンを利用したRESTAPIでのログイン処理ができるようになりました。
最後にソースコードはGithubにアップロードしております
https://github.com/jirentaicho/SpringSecurity5.7-jwt
前回までの記事