Spring Security × JWTによるREST API。ロールベースアクセス制御。

# はじめに

アサインされたプロジェクのJWT×REST API認証方法が少し特殊で、お客さんからなんでspring-security使ってないのと聞かれ焦り...
一般的なspring security × JWT × REST APIとはどんなものか理解したく、デモアプリケーションを実装してみました。

### 環境
- Windows 10 Home
- Java 1.8.0_221
- Spring Boot 2.5.1
- Spring Security 5.5.0
- jjwt 0.11.2
- H2 1.4.200
- Spring DATA JPA 2.5.1


### 参考
- [Spring Security Reference](https://docs.spring.io/spring-security/site/docs/5.0.4.RELEASE/reference/htmlsingle/
)
- [Hello Spring Security with Boot](https://docs.spring.io/spring-security/site/docs/5.0.4.RELEASE/guides/html5//helloworld-boot.html)
- [Spring Security & JWT with Spring Boot 2.0で簡単なRest APIを実装する
](https://qiita.com/rubytomato@github/items/eb595303430b35f4773d#%E8%AA%8D%E8%A8%BC%E3%81%AE%E4%BB%95%E6%96%B9)
- [JWTによるREST APIのログインを実現する](https://retheviper.github.io/spring/2020/06/10/spring-rest-api-login-with-jwt/#AuthenticationSuccessHandler%E3%81%AE%E4%BD%9C%E6%88%90) 
- [JWTトークンについて]
(https://hiyosi.tumblr.com/post/70073770678/jwt)

### ソース
[https://github.com/Takeuchi713/spring-security-jwt-sample](https://github.com/Takeuchi713/spring-security-jwt-sample)


# デモアプリケーションの概要

メールアドレスとパスワードでユーザーを認証し、tokenを発行します。
発行されたtokenはヘッダーで返し、以降の認証・許可はリクエストヘッダーにセットしたtokenによって行います。

権限について
"USER","ADMIN"があり、権限の範囲内のAPIしか呼び出すことができません。
具体的にはADMIN権限を持つユーザーは全データへのアクセス権があり。
USER権限のユーザーは自身のデータへのアクセス権しかない状態とします。


### イメージ図
![application-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295610/39a44f7b-b8bb-0db2-ed71-b61a6f3a395f.png)


### 実装するAPI

|メソッド| パス                       | 権限  | 備考 |
|:------:|---------------------------|-------|--------------|
| GET| /api/vi/public/hello | なし | 未ログインで呼び出し可能 |
| GET| /api/vi/admin/find/{id} | ADMIN |利用者情報の取得|
| GET | /api/vi/admin/find/all | ADMIN |全利用者情報の取得              |
|POST  | /api/v1/admin/add         | ADMIN |利用者情報の登録|
|PUT   | /api/v1/admin/update      | ADMIN |利用者情報の変更|
|DELETE| /api/v1/admin/delete/{id} | ADMIN |利用者情報の削除|
|GET   | /api/v1/user/find/me      | USER  |自身の情報取得|
|PUT   | /api/v1/user/update/me      | USER  |自身の情報変更|
|DELETE   | /api/v1/user/delete/me      | USER  |自身の情報削除|

### DB
インメモリDBのH2を使用しています。
アプリケーション起動時にテーブルが自動的に作成。初期データがinsertされます。
今回はサンプルで初期データとしてUser権限、Admin権限、両権限を持つユーザーをINSERTしています。

また、各userのpasswordは12345をBCryptで暗号化した値です。

```SQL:schema.sql
--Userテーブル作成
CREATE TABLE user (
    id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    roles VARCHAR(100) NOT NULL,
    password VARCHAR(150) NOT NULL,
    is_active BOOLEAN NOT NULL
);
```

```SQL:data.sql
--INSERT Role USER
INSERT INTO USER(name,email,roles,password,is_active) VALUES(
    'user','test@test.com','ROLE_USER','$2a$10$27Kls5TCTZUttJHzlmuUqecS0Ab7jRFp2vmBMqKk3HeW3p3ebFx4m',true
);

--INSERT Role ADMIN
INSERT INTO USER(name,email,roles,password,is_active) VALUES(
    'admin','test2@test.com','ROLE_ADMIN','$2a$10$27Kls5TCTZUttJHzlmuUqecS0Ab7jRFp2vmBMqKk3HeW3p3ebFx4m',true
);

--INSERT Role ADMIN and USER
INSERT INTO USER(name,email,roles,password,is_active) VALUES(
    'admin_user','test3@test.com','ROLE_USER,ROLE_ADMIN','$2a$10$27Kls5TCTZUttJHzlmuUqecS0Ab7jRFp2vmBMqKk3HeW3p3ebFx4m',true
);

```

#### 起動後のh2-console
![h2-console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295610/84a3cfad-6d3e-1b79-6eb5-784c178fe734.png)


# 実装
1. [元となるアプリの作成](#1-元となるアプリの作成)
1. [spring securityを追加](#2-spring-securityを追加)
1. [動作確認](#3-動作確認)

#1. 元となるアプリの作成

まずはUserのCRUD処理を作成する。
ADMIN権限とUSER権限用の二種類のコントローラーとAPIを作成。

### 1-0. build.gradle
```gradle:build.gradle
plugins {
	id 'org.springframework.boot' version '2.5.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.takeuchi'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    //swaggerでAPIを一覧に出したかったので追加。趣旨とはあまり関係ない。
	implementation 'io.springfox:springfox-swagger2:2.8.0'
    implementation 'io.springfox:springfox-swagger-ui:2.8.0'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

```

### 1-1. Entity
```Java:User.java
@Data
@Entity
public class User {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	@Column(name = "name", length = 100, nullable = false)
	private String name;

	@Column(name = "email", length = 100, nullable = false, unique = true)
	private String email;

	@Column(name = "roles", length = 100, nullable = false)
	private String roles;

	@Column(name = "password", length = 150, nullable = false)
	private String password;

	@Column(name = "is_active", nullable = false)
	private Boolean active;
}
```
### 1-2. Controller

```Java:AdminController.java
@RestController
@RequestMapping(CommonConstants.API_BASE_PATH  + "/admin")
public class AdminUserController {
	private final UserService userService;

	@Autowired
	AdminUserController(UserService userService) {
		this.userService = userService;
	}

	//idからUser情報を取得
	@GetMapping("/find/{id}")
	public ResponseEntity<User> findById(@PathVariable(name="id", required = true) Integer id){
		User user = userService.findById(id);
		HttpHeaders headers = new HttpHeaders();
		return new ResponseEntity<User>(user, headers, HttpStatus.OK);
	}

	//全てのUser情報を取得
	@GetMapping("/find/all")
	public ResponseEntity<List<User>> findAll(){
		List<User> users = userService.findAll();
		HttpHeaders headers = new HttpHeaders();
		return new ResponseEntity<List<User>>(users, headers, HttpStatus.OK);
	}

	//Userを作成
	@PostMapping("/add")
	public ResponseEntity<User> addUser(@RequestBody @Valid UserRequest userRequest) {
		User user = userService.addUser(userRequest);
		HttpHeaders headers = new HttpHeaders();
		return new ResponseEntity<User>(user, headers, HttpStatus.CREATED);
	}

	//Userを更新
	@PutMapping("/update")
	public ResponseEntity<String> updateUser(@RequestBody @Valid UserRequest userRequest) {
		userService.updateUser(userRequest);
		HttpHeaders headers = new HttpHeaders();
		return new ResponseEntity<String>("update successed", headers, HttpStatus.OK);
	}

	//Userを削除
	@DeleteMapping("/delete/{id}")
	public ResponseEntity<String> deleteUserById(@PathVariable(name = "id", required = true) Integer id) {
		userService.deleteUserById(id);
		HttpHeaders headers = new HttpHeaders();
		return new ResponseEntity<String>("user id: " + id + " was deleted", headers, HttpStatus.OK);
	}
}
```

```Java:UserController.java
@RestController
@RequestMapping(CommonConstants.API_BASE_PATH + "/user")
public class UserController {
	private final UserService userService;

	@Autowired
	UserController(UserService userService) {
		this.userService = userService;
	}

	@GetMapping("/find/me")
	public ResponseEntity<User> findMe(){
		User user = userService.findMe();
		HttpHeaders headers = new HttpHeaders();
		return new ResponseEntity<User>(user, headers, HttpStatus.OK);
	}

	@PutMapping("/update/me")
	public ResponseEntity<User> updateMe(HttpServletRequest request, @RequestBody @Valid UserRequest userRequest){
		User user = userService.updateMe(userRequest);
		HttpHeaders headers = new HttpHeaders();
		return new ResponseEntity<User>(user, headers, HttpStatus.OK);
	}

	@DeleteMapping("/delete/me")
	public ResponseEntity<String> deleteMe(HttpServletRequest request){
		userService.deleteMe();
		HttpHeaders headers = new HttpHeaders();
		return new ResponseEntity<String>("delete successed", headers, HttpStatus.OK);
	}
}
```

```java:PublicController.java
@RestController
@RequestMapping(CommonConstants.API_BASE_PATH  + "/public")
public class PublicController {
	@GetMapping("/hello")
	public ResponseEntity<String> hello() {
		return new ResponseEntity<String>("hello", new HttpHeaders(), HttpStatus.OK);
	}
}
```

### 1-3. Service
```Java:UserServiceImp.java
@Service
public class UserServiceImp implements UserService {

	private final UserRepository userRepository;
	private final HttpServletRequest servletRequest;
	private final BCryptPasswordEncoder bCryptPasswordEncoder;

	@Autowired
	UserServiceImp(UserRepository userRepository, HttpServletRequest servletRequest,
			BCryptPasswordEncoder bCryptPasswordEncoder) {
		this.userRepository = userRepository;
		this.servletRequest = servletRequest;
		this.bCryptPasswordEncoder = bCryptPasswordEncoder;
	}

	@Override
	@Transactional(readOnly = true)
	public User findMe() {
		// filterで追加したidを取得
		Integer id = getId();
		if(id == null) {
			throw new UserNotFoundException("no id");
		}
		return userRepository.findById(id)
			.orElseThrow(() -> new UserNotFoundException("user with id: " + id + "was not found"));
	}

	@Override
	@Transactional
	public User updateMe(UserRequest userRequest) {
		Integer id = getId();
		if(id == null) {
			throw new UserNotFoundException("no id");
		}
		User updateUser = userRepository.findById(id)
			.orElseThrow(() -> new UserNotFoundException("\"user with id: \" + id + \"was not found\""));
		
		updateUser.setEmail(userRequest.getEmail());
		updateUser.setName(userRequest.getName());
		updateUser.setRoles(userRequest.getRoles());
		updateUser.setPassword(bCryptPasswordEncoder.encode(userRequest.getPassword()));
		updateUser.setActive(userRequest.getIs_active());
		userRepository.save(updateUser);
		return updateUser;
	}

	@Override
	@Transactional
	public void deleteMe() {
		Integer id = getId();
		if (id == null || userRepository.findById(id).get() == null) {
			throw new UserNotFoundException("no user was found");
		}
		userRepository.deleteById(id);
	}

	@Override
	@Transactional(readOnly = true)
	public User findById(Integer id) {
		User user = userRepository.findById(id)
			.orElseThrow(() -> new UserNotFoundException("user with id: " + id + "was not found"));
		return user;
	}

	@Override
	@Transactional(readOnly = true)
	public List<User> findAll() {
		List<User> users = userRepository.findAll();
		return users;
	}

	@Override
	@Transactional
	public User addUser(UserRequest userRequest) {
		User newUser = new User();
		newUser.setName(userRequest.getName());
		newUser.setEmail(userRequest.getEmail());
		newUser.setRoles(userRequest.getRoles());
		newUser.setPassword(bCryptPasswordEncoder.encode(userRequest.getPassword()));
		newUser.setActive(userRequest.getIs_active());
		User user = userRepository.save(newUser);
		return user;
	}

	@Override
	@Transactional
	public void updateUser(UserRequest userRequest) {
		if(userRequest.getId() == null) {
			throw new ValidationException("id must not be null");
		}
		User updateUser = userRepository.findById(userRequest.getId())
				.orElseThrow(() -> new UserNotFoundException("user by id: " + userRequest.getId() + " was not found"));

		updateUser.setEmail(userRequest.getEmail());
		updateUser.setName(userRequest.getName());
		updateUser.setRoles(userRequest.getRoles());
		updateUser.setPassword(bCryptPasswordEncoder.encode(userRequest.getPassword()));
		updateUser.setActive(userRequest.getIs_active());
		userRepository.save(updateUser);
	}

	@Override
	@Transactional
	public void deleteUserById(Integer id) {
		if(id == null) {
			throw new ValidationException("id must not be null");
		}
		userRepository.findById(id)
				.orElseThrow(() -> new UserNotFoundException("user by id: " + id + " was not found"));
		userRepository.deleteById(id);;
	}
	
	private Integer getId() {
		Integer id = null;
		try {
			String idStr = (String) servletRequest.getAttribute(CommonConstants.USER_ID);
			id = Integer.valueOf(idStr);
		}catch(Exception e) {
			return id;
		}
		return id;
	}
}
```

### 1-4. 動作確認
```bat:

rem 全ユーザーを取得
>curl http://localhost:8080/api/v1/admin/find/all
[{"id":1,"name":"user","email":"test@test.com","roles":"ROLE_USER","password":"$2a$10$27Kls5TCTZUttJHzlmuUqecS0Ab7jRFp2vmBMqKk3HeW3p3ebFx4m","active":true},
{"id":2,"name":"admin","email":"test2@test.com","roles":"ROLE_ADMIN","password":"$2a$10$27Kls5TCTZUttJHzlmuUqecS0Ab7jRFp2vmBMqKk3HeW3p3ebFx4m","active":true},
{"id":3,"name":"admin_user","email":"test3@test.com","roles":"ROLE_USER,ROLE_ADMIN","password":"$2a$10$27Kls5TCTZUttJHzlmuUqecS0Ab7jRFp2vmBMqKk3HeW3p3ebFx4m","active":true}]

rem ユーザー削除
>curl -X DELETE -H "Content-Type: application/json" http://localhost:8080/api/v1/admin/delete/1
user id: 1 was deleted
```

```json:userjson.txt
{
    "id": "",
    "name": "test3",
    "email": "test5@test.com",
    "roles": "ROLE_USER",
    "password": "12345",
    "is_active": true
}
```

# 2. spring securityを追加

1.で作成したアプリにspring securityを追加していく。
jwtのライブラリはjjwtを選択。
[https://github.com/jwtk/jjwt](https://github.com/jwtk/jjwt)
[https://jwt.io/](https://jwt.io/)


### 2-1. gradleに依存性を追加
spring-security、jjwtを追加。

```gradle:build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2','io.jsonwebtoken:jjwt-jackson:0.11.2'
//test用
testImplementation 'org.springframework.security:spring-security-test'
```
### 2-2. Configurationの設定
WebSecurityConfigurerAdapterを拡張した認証の為のConfigurationクラス。
エンドポイント別にAPIのアクセスを制限する。

| エンドポイント |    制限     |
|:--------------|-------------|
| /public/**| 誰でもアクセス可能   | 
| /user/**  | USER_ROLE、ADMIN_ROLEのみ |      
| /admin/** | ADMIN_ROLEのみ       |      



```java:WebSecurityConfig.java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring()
			//swaggerが認証エラーとならない為の設定
			.antMatchers("/v2/api-docs",
				"/configuration/ui",
				"/swagger-resources/**",
				"/configuration/security",
				"/swagger-ui.html",
				"/webjars/**");
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			//CSRFを許可
			.csrf().disable()
			//REST想定なのでセッションをステートレスに
			.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
			.and()
			//リクエスト認証
			.authorizeRequests()
				//tokenなしでも許容するAPI
				.mvcMatchers(CommonConstants.API_BASE_PATH + "/public/**", "/swagger-ui.html/**","/h2-console/**")
					.permitAll()
				//ROLE_USER or ROLE_ADMIN権限が必要なAPI
				.mvcMatchers(CommonConstants.API_BASE_PATH + "/user/**")
					.hasAnyRole("USER","ADNMIN")
				//ROLE_ADMIN権限が必要なAPI
				.mvcMatchers(CommonConstants.API_BASE_PATH + "/admin/**")
					.hasRole("ADMIN")
				.anyRequest().authenticated()
			.and()
				//request認証とtokenを発行するfilter
				.addFilter(new JWTAuthenticationFilter(authenticationManager()))
				//tokenの承認を行うfilter
				.addFilterAfter(new JWTAuthorizationFilter(), JWTAuthenticationFilter.class)
			//エラーハンドリング
			.exceptionHandling()
				// ログインエラー時のハンドラー設定(未ログインも)
				.accessDeniedHandler(new JWTAccessDeniedHandler())
				// 権限エラー時のハンドラー設定
				.authenticationEntryPoint(new JWTAuthenticationEntryPoint())
			.and()
			//h2-consoleへ接続するための設定
			.headers().frameOptions().disable();
	}

	@Bean
	//passwordのエンコーダー
	public BCryptPasswordEncoder bCryptPasswordEncoder () {
		return new BCryptPasswordEncoder(10);
	}
}

```

### 2-3. ログイン認証処理の作成

#### 概要
主要な処理とクラス。
・JWTAuthenticationFilter : Requetを認証しtokenを発行
・UserDetailsServiceImp : DBとrequestが一致するか判定
・LoginUser : ユーザ情報をspring-securityで使用できる形へ変換

処理イメージ
点線の箇所はspring-securityがいい感じで裏で呼び出してくれる。
![login認証処理の流れ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1295610/4dcd588a-0ab0-0f38-596f-9606083494af.png)

#### JWTAuthenticationFilter 
request認証とtokenを発行するFilter。
"/api/v1/login"へPOSTするとここを通過する。

```java:JWTAuthenticationFilter.java
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
	private final AuthenticationManager authenticationManager;

	public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
		this.authenticationManager = authenticationManager;

		//デフォルトのpathを変更
		setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(CommonConstants.LOGIN_URL, "POST"));
		//デフォルトのパラメーターを変更
		setUsernameParameter(CommonConstants.EMAIL);
		setPasswordParameter(CommonConstants.PASSWORD);
	}

	@Override
	//ログイン認証処理
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {

		try {
			LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
			Authentication authentication = new UsernamePasswordAuthenticationToken(
				loginRequest.getEmail(), //principal
				loginRequest.getPassword() //credencial
			);
			
			return authenticationManager.authenticate(authentication);
		} catch (Exception e) {
			log.warn(e.getMessage());
			throw new RuntimeException(e);
		}
	}

	@Override
	//ログイン認証成功後の処理
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
　　    //token発行
		String token = Jwts.builder()
			.setSubject(((LoginUser)authResult.getPrincipal()).getUser().getId().toString()) //id
			.claim(CommonConstants.AUTHORITES, authResult.getAuthorities()) //権限
			.setIssuedAt(new Date()) //発行日
			.setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) //有効期限
			.signWith(Keys.hmacShaKeyFor(CommonConstants.SECRET_KEY.getBytes())) //暗号化key
			.compact();

		response.addHeader(CommonConstants.AUTHORIZED_HEADER, CommonConstants.TOKEN_PREFIX + token);
	}
}
```

#### UserDetailsServiceImp

```java:UserDetailsServiceImp.java
@Service
@Slf4j
public class UserDetailsServiceImp implements UserDetailsService {
	private final UserRepository userRepository;

	@Autowired
	UserDetailsServiceImp(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	@Override
	public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
		log.info("user {} was accessed", email);
		return userRepository.findByEmail(email)
			.map(LoginUser::new)
			.orElseThrow(() -> new UsernameNotFoundException("User [" + email + "] was not found"));
	}
}
``` 

#### LoginUser

```java:LoginUser.java
//EntityのUserとUserDetailをマッピングさせるクラス
public class LoginUser extends org.springframework.security.core.userdetails.User {
	//Entity
	@Getter
	private final User user;

	public LoginUser(User user) {
		super(user.getName(), user.getPassword(), user.getActive(),
				true, true, true, convertGrandtedAuthorites(user.getRoles()));
		this.user= user;
	}
     //DBのrolesからGrantedAuthorityへ変換する
	private static Set<GrantedAuthority> convertGrandtedAuthorites(String roles){
		if (!StringUtils.hasText(roles)) {
			return Collections.emptySet();
		}

		Set<GrantedAuthority> authorites = Stream.of(roles.split(","))
			.map(String::trim)
			.map(SimpleGrantedAuthority::new)
			.collect(Collectors.toSet());

		return authorites;
	}
}
``` 

### 2-4. token承認filterの作成
```java:JWTAuthorizationFilter.java
@Slf4j
public class JWTAuthorizationFilter extends OncePerRequestFilter   {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		String header = request.getHeader(CommonConstants.AUTHORIZED_HEADER);
		if(!StringUtils.hasText(header) || !header.startsWith(CommonConstants.TOKEN_PREFIX) ) {
			log.info("request {} was called by token {}", request.getRequestURI(), header);
			filterChain.doFilter(request, response);
			return;
		}

		String token = header.replace(CommonConstants.TOKEN_PREFIX, "");
		if(token != null) {
			try {
				//tokenを復号し中身を取得。不正があるとJwtExceptionが発生する。
				Jws<Claims> claimsJws = Jwts.parserBuilder()
					.setSigningKey(Keys.hmacShaKeyFor(CommonConstants.SECRET_KEY.getBytes()))
					.build()
					.parseClaimsJws(token);

				//復号tokenから認証情報を作成する。
				Authentication authentication = getAuthentication(claimsJws);
				SecurityContextHolder.getContext().setAuthentication(authentication);
				//ユーザidを追加
				if(claimsJws.getBody().getSubject() != null) {
					request.setAttribute(CommonConstants.USER_ID, claimsJws.getBody().getSubject());
				}
				filterChain.doFilter(request, response);
			}catch(JwtException je) {
				log.error(je.getMessage());
				throw new IllegalStateException(String.format("Token %s cannnot be trusted ", token));
			}catch(Exception e) {
				log.error(e.getMessage());
				throw new RuntimeException(e);
			}
		}
	}

	private Authentication getAuthentication(Jws<Claims> claimsJws) {
		//user_id
		String userId = claimsJws.getBody().getSubject();
		//GrantedAuthorites
		@SuppressWarnings("unchecked")
		List<Map<String, String>> authorites = (List<Map<String, String>>) claimsJws.getBody()
			.get(CommonConstants.AUTHORITES);

		Set<GrantedAuthority> grantedAuthorities = authorites.stream()
			.map(k -> new SimpleGrantedAuthority(k.get("authority")))
			.collect(Collectors.toSet());

		return new UsernamePasswordAuthenticationToken(userId, null, grantedAuthorities);
	}
}
```

### 2-5. エラーハンドリング
レスポンスbodyにエラー情報を載せるカスタムクラスを作成。
JWTAuthenticationEntryPoint : ログインエラー、未ログインでAPIアクセス時のハンドラー
JWTAccessDeniedHandler : 権限エラー時のハンドラー

```java:JWTAuthenticationEntryPoint.java
@Slf4j
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
			throws IOException, ServletException {
		log.error(authException.getMessage());

		ErrorResponse er = ErrorResponse.builder()
			.status(HttpStatus.UNAUTHORIZED.value())
			.error(HttpStatus.UNAUTHORIZED.getReasonPhrase())
			.message(authException.getMessage())
			.path(request.getServletPath())
			.build();

		ObjectMapper om = new ObjectMapper();
		String json = er.toJson(om);
		
		response.setStatus(HttpStatus.UNAUTHORIZED.value());
		response.setContentType(MediaType.APPLICATION_JSON.toString());
		response.getWriter().write(json);
	}
}
```

```java:JWTAccessDeniedHandler.java
@Slf4j
public class JWTAccessDeniedHandler implements AccessDeniedHandler {

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		log.error(accessDeniedException.getMessage());

		ErrorResponse er = ErrorResponse.builder()
			.status(HttpStatus.FORBIDDEN.value())
			.error(HttpStatus.FORBIDDEN.getReasonPhrase())
			.message(accessDeniedException.getMessage())
			.path(request.getServletPath())
			.build();
		
		ObjectMapper om = new ObjectMapper();
		String json = er.toJson(om);
		
		response.setStatus(HttpStatus.FORBIDDEN.value());
		response.setContentType(MediaType.APPLICATION_JSON.toString());
		response.getWriter().write(json);
	}
}
```

<a id="#Chapter3"></a>
# 3. 動作確認

#### tokenなしで呼べるAPI
```batchfile:
>curl -i -X GET http://localhost:8080/api/v1/public/hello
HTTP/1.1 200
hello
```

#### token
```json:
user権限
{
  "sub": "1",
  "authorites": [
    {
      "authority": "ROLE_USER"
    }
  ],
  "iat": 1627136424,
  "exp": 1627137024
}

admin権限
{
  "sub": "2",
  "authorites": [
    {
      "authority": "ROLE_ADMIN"
    }
  ],
  "iat": 1627136448,
  "exp": 1627137048
}

両方の権限
{
  "sub": "3",
  "authorites": [
    {
      "authority": "ROLE_ADMIN"
    },
    {
      "authority": "ROLE_USER"
    }
  ],
  "iat": 1627136050,
  "exp": 1627136650
}
```

#### login成功
```batchfile:
>curl -i -X POST -H "Content-Type: application/json" -d "{\"email\":\"test3@test.com\",\"password\":\"12345\"}" http://localhost:8080/api/v1/login
HTTP/1.1 200
X-Authenticated-token: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIzIiwiYXV0aG9yaXRlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9LHsiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn1dLCJpYXQiOjE2MjcxMzQ4NTIsImV4cCI6MTYyNzEzNTQ1Mn0.inu8MZF4Qt2o547NsKCIaGsNDucwb211KQ0F4fkgeK02hiSccoKQeE1O3cnIJJz79joFIbsYAR5FQV04e15t9w
```

#### API実行
```batchfile:
rem API:find/me (user権限)
>curl -i -X GET -H "Content-Type: application/json" -H "X-Authenticated-token: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiYXV0aG9yaXRlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn1dLCJpYXQiOjE2MjcxMzUyMDAsImV4cCI6MTYyNzEzNTgwMH0.xhVVmd9gNXDwjEnfi2Ho0mRNp79s-zUIzgl5IY9A53OmT7uJPAqcz1cCd0lwmm6aPxAGymGjmvVT-C6Uv3B-0w" http://localhost:8080/api/v1/user/find/me
HTTP/1.1 200
{"id":1,"name":"user","email":"test@test.com","roles":"ROLE_USER","password":"$2a$10$27Kls5TCTZUttJHzlmuUqecS0Ab7jRFp2vmBMqKk3HeW3p3ebFx4m","active":true}

rem API:add (admin権限)
>curl -i -X POST -H "Content-Type: application/json" -H "X-Authenticated-token: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIzIiwiYXV0aG9yaXRlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9LHsiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn1dLCJpYXQiOjE2MjcxMzYwNTAsImV4cCI6MTYyNzEzNjY1MH0.mZyYMH3r7Hxtf7QCEQmTpsqr9XqXgDN5uRBE-0ZgNdl2Itp3aBscU6SGZRSIrVc8wz_86CayHgkBiP1RihA-vA" -d @userjson.txt http://localhost:8080/api/v1/admin/add
HTTP/1.1 201
{"id":4,"name":"test3","email":"test5@test.com","roles":"ROLE_USER","password":"$2a$10$YynA0I5cd8mAYJ9aorl4f.iaxg9RghCCW29ivzxuke5PWj0Y/BDci","active":true}

```

#### login失敗
```batchfile:
>curl -i -X POST -H "Content-Type: application/json" -d "{\"email\":\"test3@test.com\",\"password\":\"aaaa\"}" http://localhost:8080/api/v1/login
HTTP/1.1 401
{"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/error"}
```

#### token不足
```batchfile:
>curl -i -X POST -H "Content-Type: application/json" -d @userjson.txt http://localhost:8080/api/v1/admin/add
HTTP/1.1 401
{"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/api/v1/admin/add"}
```



#### USER権限でADMIN権限のAPIを実行
```batchfile:
>curl -i -X GET -H "X-Authenticated-token: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiYXV0aG9yaXRlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn1dLCJpYXQiOjE2MjcxMzY1ODYsImV4cCI6MTYyNzEzNzE4Nn0.QTfRhnWBSIhEYxFLLoL_1-gphWBuMnD6uI5jOySS2QAYzhJdgcb99VIjq_VxAYFTKngAQphgv9eaJNF3FmroTA" http://localhost:8080/api/v1/admin/find/all
HTTP/1.1 403
{"status":403,"error":"Forbidden","message":"Access is denied","path":"/api/v1/admin/find/all"}
```