Post

Spring Boot Method Security

Posted by wonwoo on 2016-10-05 category TEST

오늘은 제목 그대로 Spring의 메서드로 권한을 체크해보자. 예를들어 어떤 메서드에 권한을 부여해 주는 것이다. 일단 테스트를 하기 위해 oauth2 서버도 아주 간단하게 테스트만 할 수 있을 정도로만 만들자. Spring boot 경우에는 쉽게 할 수 있다. 디펜더시부터 받자.

<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

일단 시큐리티만 적었다. 웹과 기타 등등 나머지는 알아서 넣길..

@SpringBootApplication
@EnableAuthorizationServer
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringStudyApplication {

  public static void main(String[] args)  {
    SpringApplication.run(SpringStudyApplication.class, args);
  }
}

그리고 메인 클래스에 위와 같이 @EnableAuthorizationServer, @EnableResourceServer, @EnableGlobalMethodSecurity(prePostEnabled = true) 어노테이션만 설정하자. 그리고 properties 혹은 yaml 파일에 다음과 같이 작성하자.

security.user.name=wonwoo
security.user.password=123

security.oauth2.client.client-id=foo
security.oauth2.client.client-secret=bar

사용자명과 비밀번호 client-id, client-secret 만 적어 주면 일단 인증 서버는 끝이 났다. 그냥 Spring으로 할려면 적어도 30분은 낑낑 거리면서 해야 할 거 같은데 boot라 금방 끝났다.

인증 서버가 제대로 작동하는지 보자. 서버를 올린 다음에 다음과 같이 로그인을 해보자.

curl localhost:8080/oauth/token -d "grant_type=password&scope=read&username=wonwoo&password=123" -u foo:bar

만약 로그인이 안된다면 설정을 잘못한거다. 다시 확인해보자. 로그인을 성공했다면 다음과 같이 인증 토큰이 발급 될 것이다.

{
  "access_token": "de174426-1d04-4032-9b0e-932d6efd7bb2",
  "token_type": "bearer",
  "refresh_token": "5ba8609e-54e6-47a2-a014-761bb387ef7b",
  "expires_in": 43199,
  "scope": "read"
}

일단 여기 까지 성공했다면 다음과 같이 작성부터 하자.

public class User {

  private final String name;

  public User(String name) {
    this.name = name;
  }
  //getter, equals, hashCode 생략
}

딱히 특별할거 없는 도메인 클래스이다.

@RestController
@RequestMapping("/security")
public class SecurityController {

  private final SecurityService securityService;

  public SecurityController(SecurityService securityService) {
    this.securityService = securityService;
  }

  @GetMapping("/read")
  public User userRead(){
    return securityService.userRead();
  }
  @GetMapping("/write")
  public User userWrite(){
    return securityService.userWrite();
  }

  @GetMapping("/user/{name}")
  public String username(@PathVariable String name){
    return securityService.username(name);
  }
}

다음은 컨트롤러 클래스이며 세개의 매핑이 있다. 각각은 read, write 혹은 권한을 말하며 마지막 세번째는 밑에 가서 설명하자.

@Service
public class SecurityService {

  @PreAuthorize("#oauth2.hasScope('read')")
  public User userRead(){
    return new User("wonwoo");
  }

  @PreAuthorize("#oauth2.hasScope('write')")
  public User userWrite(){
    return new User("wonwoo");
  }

  @PostAuthorize("(returnObject == 'good')")
  public String username(String name) {
    if("wonwoo".equals(name)){
      return "good";
    }
    return "bad";
  }
}

마지막으로 이런 간단한걸 테스트 하기 위해 위에 많은 작업을 했다. 일단 살펴보자. userRead메서드는 스코프가 read이다 read권한만 있을 경우에만 허용하고 나머지는 접근 거부가 되어야 한다. 두번째도 마찬가지로 write 권한만 허용 가능한 메서드이다. 스코프 말고도 다음과 같이 role도 지정 할 수 있다.

@PreAuthorize("hasRole(‘ROLE_USER’)") 
public User user(){
///....
}

테스트를 해보자. 아마도 다시 인증을 해야 할 것이다. 우리는 디비를 사용하지 않고 메모리에 토큰을 저장하므로 서버를 재시작하면 인증토큰이 날라간다. 다시 인증하자.

curl localhost:8080/oauth/token -d "grant_type=password&scope=read&username=wonwoo&password=123" -u foo:bar

현재 인증한 것은 scope가 read이다. userRead() 메서드만 정상적으로 호출 되어야 하며 userWrite 메서드는 접근 할 수 없다.

curl -H "authorization: bearer de174426-1d04-4032-9b0e-932d6efd7bb2" localhost:8080/security/read

아래와 같이 별 이상 없이 json이 출력 될 것이다.

{
  "name": "wonwoo"
}

다음은 write를 호출해보자.

curl -H "authorization: bearer de174426-1d04-4032-9b0e-932d6efd7bb2" localhost:8080/security/write

접근이 거부 되었다고 출력 되었다.

{
  "error": "access_denied",
  "error_description": "접근이 거부되었습니다."
}

실제로 로그를 출력 해보면 controller로그는 출력 된다. 하지만 userWrite() 메서드에서 권한이 없어서 에러가 발생한다. 두개는 테스트가 되었다. 마지막으로 @PostAuthorize 어노테이션이다. @PreAuthorize 과는 조금 다르게 메서드가 끝나고 결정한다. PreAuthorize는 메서드를 호출전에 권한을 체크한다면 PostAuthorize는 메서드가 끝나고 반환값을 통해 권한(?) 접근을 거부 할 수 있다. 위의 예제는 return 값이 good이면 접근을 허용한 것이다. 그 이외면 접근 거부가 된다. returnObject 값은 그냥 return 값의 키인듯 싶다. 테스트를 해보자.

curl -H "authorization: bearer de174426-1d04-4032-9b0e-932d6efd7bb2" localhost:8080/security/user/wonwoo

wonwoo라는 파라미터를 보내면 good이 리턴되므로 아래와 같이 good이라는 텍스트가 출력된다.

good

이번에는 wonwoo가 아닌 다른 문자열을 넣어보자.

curl -H "authorization: bearer de174426-1d04-4032-9b0e-932d6efd7bb2" localhost:8080/security/user/kevin

그럼 접근이 거부 되었다고 출력 된다.

{
  "error": "access_denied",
  "error_description": "Access is denied"
}

왜 이 아이는 영어고 위에놈은 한글이지? 흠 아무튼 이렇게 우리는 메서드에 권한을 부여해봤다. 어디다 써먹어 볼까?