はじめに
アサインされたプロジェクの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
- Hello Spring Security with Boot
- Spring Security & JWT with Spring Boot 2.0で簡単なRest APIを実装する
 
- JWTによるREST APIのログインを実現する
- [JWTトークンについて]
 (https://hiyosi.tumblr.com/post/70073770678/jwt)
ソース
https://github.com/Takeuchi713/spring-security-jwt-sample
デモアプリケーションの概要
メールアドレスとパスワードでユーザーを認証し、tokenを発行します。
発行されたtokenはヘッダーで返し、以降の認証・許可はリクエストヘッダーにセットしたtokenによって行います。
権限について
"USER","ADMIN"があり、権限の範囲内のAPIしか呼び出すことができません。
具体的にはADMIN権限を持つユーザーは全データへのアクセス権があり。
USER権限のユーザーは自身のデータへのアクセス権しかない状態とします。
イメージ図
実装する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で暗号化した値です。
--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
);
--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
実装
1. 元となるアプリの作成
まずはUserのCRUD処理を作成する。
ADMIN権限とUSER権限用の二種類のコントローラーとAPIを作成。
1-0. 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
@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
@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);
	}
}
@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);
	}
}
@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
@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. 動作確認
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
{
    "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://jwt.io/
2-1. gradleに依存性を追加
spring-security、jjwtを追加。
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のみ | 
@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がいい感じで裏で呼び出してくれる。

JWTAuthenticationFilter
request認証とtokenを発行するFilter。
"/api/v1/login"へPOSTするとここを通過する。
@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
@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
//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の作成
@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 : 権限エラー時のハンドラー
@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);
	}
}
@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);
	}
}
3. 動作確認
tokenなしで呼べるAPI
>curl -i -X GET http://localhost:8080/api/v1/public/hello
HTTP/1.1 200
hello
token
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成功
>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実行
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失敗
>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不足
>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を実行
>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"}

