본문으로 바로가기

728x90

이번 포스트에서는 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 =====");
	}
}

 

참고자료

 

 

728x90