API에 따라 권한을 다르게 주어야합니다. 어떤 API는 누구나 실행해볼 수 있지만, 로그인한 사용자만 또는 관리자만 실행할 수 있는 API가 있습니다.
Spring Security와 연동하면 쉽게 이런 권한들을 API별로 줄 수 있습니다.
Secured Annotation
@Secured("ROLE_USER")
위와 같이 사용합니다. 이 어노테이션은 컨트롤러의 API에 붙어서 동작합니다.
1
2
@Secured("ROLE_USER")
public void create(Contact contact);
이렇게 달아두면 create API는 User role을 가져야만 요청할 수 있습니다. 아래처럼 여러 개의 Role들을 지정할 수도 있습니다.
1
2
@Secured({"ROLE_USER", "ROLE_VIEWER"})
public void create(Contact contact);
비슷하게 @RolesAllowed
도 있는데요. 이건 JSR-250의 annotation입니다. 즉 자바 표준 어노테이션인데, 사용법은 동일합니다.
1
2
3
4
5
@RolesAllowed("ROLE_USER")
public void create(Contact contact);
@RolesAllowed({"ROLE_USER", "ROLE_VIEWER"})
public void create(Contact contact);
자바 표준이냐, Spring Security 전용이냐의 차이인데 그냥 편하신거 쓰시면 될 것 같습니다. 스프링에서 사실상 벗어날 일이 거의 없기 때문에 반드시 @RolesAllowed
를 사용하실 필요는 없을 것 같습니다.
전역 상수화
매번 @Secured("ROLE_USER")
와 같이 붙이면 문제점이 있습니다. 혹시나 나중에 ROLE_USER
가 아니라 ROLE_USERS
로 바뀌면 이걸 사용했던 모든 곳을 바꾸어주어야 합니다.
그래서 전역 상수로 빼서 사용해야합니다. 이렇게하면 IDE에서 auto completion도 지원받을 수 있고, 실수로 오타를 낼 일도 없으며, 향후 변경에도 대비할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserAuthNames {
public static final String ROLE_ADMIN = "ROLE_ADMIN";
public static final String ROLE_USER = "ROLE_USER";
public static final String ROLE_GUEST = "ROLE_GUEST";
// 이런 방식은 안됩니다.
public static final String ROLE_USER = role("USER");
}
... ...
@RolesAllowed(UserAuthNames.ROLE_USER)
public void create(Contact contact);
참고로 반드시 문자열 리터럴 상수만 넣어야합니다. Annotation은 컴파일 타임에 값이 결정되기 때문에 인자로 상수만 지정할 수 있습니다.
커스텀 Annotation
Swagger를 사용하신다면 아래 annotation도 사용해야할거에요.
1
@SecurityRequirement(name = "Authorization")
이건 API가 인증이 필요하다는걸 알려주는 swagger annotation입니다. 이걸 달지 않으면 @Secured
가 있어도 swagger에서 api 테스트할 때 인증을 지원해주지 않습니다.
1
2
3
4
// Authorization도 전역 상수화
@SecurityRequirement(name = JwtProvider.AUTHORIZATION)
@Secured(UserAuthNames.ROLE_USER)
public void create(Contact contact);
그래서 이런 방식으로 2개의 annotation을 달아주어야 api에 인증이 적용되며, swagger에서도 동작합니다.
매번 User인증이 필요할 때마다 이렇게 하면 좀 지저분하겠죠? 추가로 다른 모듈을 사용하면서 annotation을 하나 더 붙여야 하는 경우에는 모든 인증이 필요한 곳을 수정해주어야 합니다.
아까 위에서 “ROLE_USER”를 전역 상수화 시킨 것 처럼 이 annotation들도 공통화할 수 있습니다.
1
2
3
4
5
6
7
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@SecurityRequirement(name = JwtProvider.AUTHORIZATION)
@Secured(UserAuthNames.ROLE_USER)
public @interface UserOnly {
}
이런식으로 annotation을 직접 생성합니다. 위의 3개 @Target
, @Retention
, @Documented
는 이 annotation에 대한 설명입니다.
@Target
은 이 annotation이 어디에 붙을 수 있는지 저장합니다. 저는 Method에만 붙을 수 있게 했습니다.
@Retention
은 이 annotation이 언제까지 유지될건지 지정합니다. SOURCE
, CLASS
, RUNTIME
을 지정할 수 있는데요. 간단하게 설명드리면
- Source: 소스코드에만 존재할 수 있고, 컴파일하면 사라집니다.
- Class: 컴파일된 class파일까지도 존재하지만, 런타임시에는 사라집니다.
- Runtime: Runtime시에도 계속 존재합니다.
Security관련 annotation은 api가 들어오는 runtime시에도 계속 있어야하므로 runtime으로 지정했습니다.
@Documented
는 JavaDoc에 이 annotation을 표시한다는 의미입니다. 없어도 큰 문제는 없습니다. 다만 지정하지 않으면 api문서에 이 annotation이 표시되지 않습니다. 이 api가 UserOnly인지 AdminOnly인지 문서를 통해 알 수 있어야겠죠? 그래서 지정했습니다.
그리고 사실상 그 아래 2개가 핵심이죠. 이렇게 annotation을 넣어두면 앞으로 @UserOnly
를 사용할 때 자동으로 @SecurityRequirement
와 @Secured
를 붙여줍니다.
1
2
3
4
5
6
7
@UserOnly
public void create(Contact contact);
// 위와 동일
@SecurityRequirement(name = JwtProvider.AUTHORIZATION)
@Secured(UserAuthNames.ROLE_USER)
public void create(Contact contact);
훨씬 보기 편해졌죠?
참고
- https://www.baeldung.com/spring-security-method-security