【SpringBoot】SpringSecurity JWT認証（ステートレス）

## 前提条件
以下URLのプログラムを元にJwt認証用（ステートレス）に変更します。

https://qiita.com/nmaolks/items/92edd19b3b606fad82f9

## クラスの説明
以下の6つのクラスを改修、追加します。


### ◇設定ファイル
　<b>〇WebSecurityConfigクラス（設定）</b>

### ◇認証（トークン取得）
　<b>〇WebUsernamePasswordAuthenticationFilterクラス（認証）</b>
　<b>〇WebAuthenticationProviderクラス（検証）</b>
　<b>〇SuccessHandlerクラス（認証検証成功時）</b>
　<b>〇FailureHandlerクラス（認証検証失敗時）</b>

### ◇認可（トークン認可）
　<b>〇JwtAuthorizationFilterクラス（認可）</b>

## プログラムの流れ
<b>◇JWTトークン取得する場合</b>
ユーザがトークン取得APIを実行時
↓
認証（トークン取得）が動く

<b>◇その他APIにアクセスする場合</b>
ユーザがトークン取得API以外のその他API実行時
↓
認可（トークン認可）が動く


## 設定ファイル
```java:WebSecurityConfig
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	/**
	 * HTTP Security設定
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {

		http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();
		http.addFilter(webUsernamePasswordAuthenticationFilter(authenticationManagerBean())).addFilter(jwtAuthorizationFilter(authenticationManagerBean()))
		.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

	}
	
	/**
	 * 独自認証を設定
	 */
	@Configuration
	protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter {

		@Autowired
		private WebAuthenticationProvider webAuthenticationProvider;

		@Override
		public void configure(AuthenticationManagerBuilder auth) throws Exception {
			auth.authenticationProvider(webAuthenticationProvider);
		}
	}
	
	/**
	 * 認証フィルター
	 * @param authenticationManager
	 * @return
	 */
	public WebUsernamePasswordAuthenticationFilter webUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
		// 独自フィルター作成
		WebUsernamePasswordAuthenticationFilter filter = new WebUsernamePasswordAuthenticationFilter();

		filter.setAuthenticationManager(authenticationManager);
		filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/v1/api/auth", "GET")); // ログイン時URL
		filter.setAuthenticationSuccessHandler(successHandler()); // 成功時URL
		filter.setAuthenticationFailureHandler(failureHandler()); // 失敗時URL
		//filter.setUsernameParameter("username"); // ユーザ パラメータ名
		//filter.setPasswordParameter("password"); // パスワード パラメータ名
		return filter;
	}
	
	/**
	 * 認証成功時処理
	 * @return
	 */
	@Bean
	public SuccessHandler successHandler() {
		return new SuccessHandler();
	}
	
	/**
	 * 認証失敗時処理
	 * @return
	 */
	@Bean
	public FailureHandler failureHandler() {
		return new FailureHandler();
	}
	
	/**
	 * 認可フィルター
	 * @param authenticationManager
	 * @return
	 */
	public JwtAuthorizationFilter jwtAuthorizationFilter(AuthenticationManager authenticationManager) {
		JwtAuthorizationFilter filter = new JwtAuthorizationFilter(authenticationManager);
		return filter;
	}
}

```
独自の認証、認可フィルター作成し、認証成功、失敗の`Handler`を設定している。


```java:WebUsernamePasswordAuthenticationFilter
public class WebUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	/**
	 * 独自フィルター
	 */
	@Override
	public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
			throws AuthenticationException {
		
		String authorization = req.getHeader("Authorization");
		String userAndPass = new String(Base64.decodeBase64(authorization.substring("Basic".length())));
		String user = userAndPass.substring(0,userAndPass.indexOf(":"));
		String password = userAndPass.substring((userAndPass.indexOf(":")+1));
		
		if(user == null) {
			user = "";
		}
		
		if(password == null) {
			password = "";
		}
	
        // トークンの作成
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user, password);
        
        setDetails(req, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
	}
}
```
認証のフィルタークラスではヘッダーからBasic認証でデータを取得するよう作成している。

```java:WebAuthenticationProvider
@Configuration
@EnableWebSecurity
public class WebAuthenticationProvider implements AuthenticationProvider {

	/**
	 * 認証処理
	 */
	@Override
	public Authentication authenticate(Authentication auth) throws AuthenticationException {
		String user = auth.getPrincipal().toString();
		String password = auth.getCredentials().toString();
		
		if (ObjectUtils.isEmpty(user)) {
			throw new AuthenticationCredentialsNotFoundException("ユーザー名もしくはパスワードに誤りがあります。");
		}

		if (ObjectUtils.isEmpty(password)) {
			throw new AuthenticationCredentialsNotFoundException("ユーザー名もしくはパスワードに誤りがあります。");
		}
		
		if (!user.equals("user")) {
			throw new AuthenticationCredentialsNotFoundException("ユーザー名もしくはパスワードに誤りがあります。");
		}

		if (!password.equals("pass")) {
			throw new AuthenticationCredentialsNotFoundException("ユーザー名もしくはパスワードに誤りがあります。");
		}
		
        Collection<GrantedAuthority> authorityList = new ArrayList<>();
        authorityList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

		// トークンを返却
		return new UsernamePasswordAuthenticationToken(user, password,authorityList);
	}

	@Override
	public boolean supports(Class<?> token) {
		return UsernamePasswordAuthenticationToken.class.isAssignableFrom(token);
	}

}
```
Filterでセットしたログイン情報をもとに認証。

```java:SuccessHandler 
public class SuccessHandler implements  AuthenticationSuccessHandler {
	
	/**
	 * 認証成功時
	 */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
    	String secretKey = "secretKey";
    	Date now = new Date();
        String user = (String)auth.getPrincipal();
        Calendar cal = Calendar.getInstance();
        cal.setTime(now);
        cal.add(Calendar.MINUTE, 60);
        Date expTime = cal.getTime();
        
        String token = JWT.create()
                .withSubject(user)
                .withIssuedAt((now))
                .withExpiresAt(expTime)
                .sign(Algorithm.HMAC256(secretKey.getBytes()));
        
        response.setStatus(HttpStatus.OK.value());
        response.addHeader("Authrization","Bearer "+ token);
    }
}
```
認証成功時にトークン作成し`response`にセットし返却。

```java:FailureHandler
public class FailureHandler implements AuthenticationFailureHandler {

    /**
     * 認証失敗時処理
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
    	response.setStatus(HttpStatus.UNAUTHORIZED.value());


    }
}
```
失敗時は401エラーで返却。

```java:JwtAuthorizationFilter
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

	/**
	 * コンストラクタ
	 * @param authenticationManager
	 */
    public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * Jwtの認可処理
     */
    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException {
        String token = req.getHeader("Authorization");
        
        if (token == null || !token.startsWith("Bearer ")) {
            chain.doFilter(req, res);
            return;
        }

        // 認証トークン作成
        UsernamePasswordAuthenticationToken authentication = getAuthentication(token);

        //　独自に認可
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        chain.doFilter(req, res);
    }

    /**
     * 認証token作成
     * @param token
     * @return
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String token) {

    	if (token != null) {
        	try {
	        	String secretKey = "secretKey";
	            String user = JWT.require(Algorithm.HMAC256(secretKey.getBytes()))
	                    .build()
	                    .verify(StringUtils.substringAfter(token, "Bearer "))
	                    .getSubject();

	            if (user != null) {
	                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
	            }
        	}catch(Exception e) {
        		logger.error("error",e);
        	}
            return null;
        }
        return null;
    }
}
```
受け取ったトークンが正しいかをチェック。

##
実装の仕方はいろんな方法があると思うので今後も調査していきます！