이번 포스트에서는 Inerceptor에서 OAuth 인증을 처리하기 위해여 Annotation을 사용하는 방법에 대해 설명합니다. 이전의 포스트들에서는 규격에 의해 구현된 OAuth를 사용하였습니다. 현재 진행중인 프로젝트에서는 규격과는 조금 다르게 처리하도록 로직을 변경하였습니다.
변경된 내용
- "Implicit Grant Type"에서도 Refresh Token 사용을 사용하도록 처리
- Refresh Token을 통한 Access Token 발급 시 데이터베이스 에서 Refresh Token을 조회하여 검증
- Refresh Token 및 Access Token 은 RSA 2048 규격으로 암호화하여 발급/검증
- 단말장치의 식별값을 통하여 발급주체와 인증주체가 동일한 단말장치에서의 접근인지 추가 검증
- Token에 Client Scope 및 User Authrity를 포함하여 저장
아래의 설명에서는 Client Scope 와 User Authrity의 접근정보는 Annotation을 통해 지정하고 Interceptor에서 Request객체에 저장된 토큰 값과 비교하여 인가여부를 처리하도록 합니다.
OAuth Annotation 생성
Annotation을 통해서 사용하려는 속성값과 기본값을 선언합니다.
@Target : Annotation이 적용 대상을 지정합니다.
@Retention : Annotation의 사용시기를 정의합니다.
public enum ElementType {
TYPE, /** Class, interface (including annotation type), or enum declaration */
FIELD, /** Field declaration (includes enum constants) */
METHOD, /** Method declaration */
PARAMETER, /** Formal parameter declaration */
CONSTRUCTOR, /** Constructor declaration */
LOCAL_VARIABLE, /** Local variable declaration */
ANNOTATION_TYPE, /** Annotation type declaration */
PACKAGE, /** Package declaration */
TYPE_PARAMETER, /** Type parameter declaration @since 1.8 */
TYPE_USE /** Use of a type @since 1.8 */
}
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* 컴파일러에게서 버려진다. 즉 클래스에는 포함이 안된다
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
* default로서 Compiler에게 class file이 기록되지만 런타임시 가상머신에 의해 retain되지 않는다.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
* Compiler에게 class file이 기록되고 At runtime에 VM에 의해 retain 된다고 나와있습니다.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
아래는 OAuth 인증을 위한 Annotation 예시 입니다. 인증을 사용할 지 여부와 범위(Client scope), 역할(User Authority)을 지정합니다.
package com.oauth.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PreOAuthAuthorize {
// int level() default 1;
boolean authorize() default true;
String scope() default "*";
String role() default "*";
}
Annotation 사용
@PreOAuthAuthorize("임의의값") 형태로 사용하기 위해서는 value()가 정의되어 있어야 합니다. Annotation 사용시 키값을 설정하지 않고 사용할 때 사용하는 방법입니다. Annotation.value() 의 반환값은 "임의의값"입니다.
/**
* 모든 메뉴 목록 출력
* @return
*/
@PreOAuthAuthorize(scope="read", role="user,manager,developer,administrator")
@RequestMapping(value="/json/menu.list.ijc", method=RequestMethod.POST)
public Map<String, Object> menuList() {
logger.debug("menuList");
// Menu;
List<Menu> menuList = menuService.selectAllByParent(0);
if(menuList != null){
menuList = arrangeList(menuList);
}
Map<String, Object> map = new HashMap<String, Object>();
map.put("list", menuList);
return map;
}
Interceptor에서 Annotation 값 읽어오기
아래의 코드는 Request Header의 인증정보과 Annotation에 정의된 권한,범위를 읽어와서 비교하는 Interceptor의 예입니다.
package com.oauth.interceptor;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.oauth.annotation.PreOAuthAuthorize;
import com.oauth.token.CipherKeypair;
import com.oauth.token.TokenManager;
import com.oauth.util.OAuthUtil;
import com.oauth.util.StringUtil;
import io.jsonwebtoken.Claims;
public class OAuthInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = LoggerFactory.getLogger(OAuthInterceptor.class);
@Autowired
private TokenManager tokenManager;
@Autowired
@Qualifier("cipherKeypair")
private CipherKeypair cipherKeypair;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
logger.info("===== before(interceptor) =====");
if(!(handler instanceof HandlerMethod)) return true;
HandlerMethod method = (HandlerMethod) handler;
PreOAuthAuthorize preAuthorize = method.getMethodAnnotation(PreOAuthAuthorize.class);
if(preAuthorize == null) return true;
boolean authorize = preAuthorize.authorize();
// 비인가상태에서도 접근할 수 있는지 여부 확인(인증을 사용하지 않는 경우)
if(authorize == false) return true;
// 허용 범위 확인
String scope = preAuthorize.scope();
String role = preAuthorize.role();
logger.debug("PreAuthorize value : {} : {}", scope, role);
if(scope.equals("*") && role.equals("*")) return true;
String tokenString = request.getHeader("Authorization");
Claims tokenBody = tokenManager.parseToken(tokenString);
if(tokenBody == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token is not availiable.");
return false;
}
List<String> tokenScope1 = (List<String>) tokenBody.get("scope");
Set<String> tokenScope = OAuthUtil.toSet(tokenScope1);
Set<String> authorizeScope = OAuthUtil.toSet(scope);
List<String> tokenAuthority1 = (List<String>) tokenBody.get("authority");
Set<String> tokenAuthority = OAuthUtil.toSet(tokenAuthority1);
Set<String> authorizeAuthority = OAuthUtil.toSet(role);
if(!OAuthUtil.hasSet(authorizeScope, tokenScope) || !OAuthUtil.hasSet(authorizeAuthority, tokenAuthority)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "You do not have permission to the resource.");
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.info("===== after(interceptor) =====");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
logger.info("===== afterCompletion =====");
}
}
참고자료
- Spring boot 에서 세션 체크 커스텀 어노테이션 만들기
https://gs.saro.me/dev?page=11&tn=505 - Java 커스텀 Annotation(Custom Annotation 만들기)
https://medium.com/@ggikko/java-%EC%BB%A4%EC%8A%A4%ED%85%80-annotation-436253f395ad
'Development > Spring Framework, Security, OAuth2' 카테고리의 다른 글
Spring Framework Event 사용하기(Using Spring Framework Event) (0) | 2022.07.05 |
---|---|
Network Socket Tunnel Server via SSL(SSL을 통한 보안 네트워크 소켓 터널링) (0) | 2021.03.29 |
Spring Framework, Interceptor와 Filter 사용하기 (0) | 2020.02.17 |
Spring Boot with OAuth2 - 10. OAuth2 Client 구현 (0) | 2019.12.23 |
Spring Boot with OAuth2 - 9. OAuth2 Server 로그인화면 수정 (0) | 2019.12.17 |