5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-07-25

はじめに

アサインされたプロジェクの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

参考

ソース

https://github.com/Takeuchi713/spring-security-jwt-sample

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

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

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

イメージ図

application-image.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で暗号化した値です。

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
);
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

実装

  1. 元となるアプリの作成
  2. spring securityを追加
  3. 動作確認

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

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

1-0. build.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

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

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);
	}
}
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);
	}
}
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

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. 動作確認


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
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://jwt.io/

2-1. gradleに依存性を追加

spring-security、jjwtを追加。

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のみ
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

JWTAuthenticationFilter

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

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

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

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の作成

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 : 権限エラー時のハンドラー

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);
	}
}
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);
	}
}

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"}
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?