조건

  • egovframe.rte.version : 4.1.0
  • 기존 환경에 RESTful API를 제공할 수 있도록 환경 변경
  • restful URL은 "/api/..."로 고정

 

프로젝트 구조

 

 

 

 

web.xml

  • /src/main/webapp/WEB-INF/web.xml
<!-- 기존에 있는 것 -->
<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <!-- 기존의 것을 정확하게 지정 -->
        <param-value>/WEB-INF/config/egovframework/springmvc/egov-com-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

<!-- 
	RESTful 용 : 
    rest란 이름의 DispatcherServlet 추가 생성
-->
 <servlet>
    <servlet-name>rest</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <!-- servlet.xml을 분리한 후 정확하게 지정 -->
        <param-value>/WEB-INF/config/egovframework/springmvc/egov-api-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>rest</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

 

 

context-security.xml 수정

  • /src/main/resources/egovframework/spring/com/context-security.xml
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/html/**" security="none"/>
<security:http pattern="/images/**" security="none"/>
<security:http pattern="/js/**" security="none"/>
<security:http pattern="/resource/**" security="none"/>
<!-- 추가 -->
<security:http pattern="/api/**" security="none"/>

 

 

 

egov-api-servlet.xml 신규 생성

  • /src/main/webapp/WEB-INF/config/egovframework/springmvc/egov-api-servlet.xml
  • 추가 내용
    • 테스트 결과 static resource가 정상적으로 mapping 되지 않는 현상이 발생함
    • jsp에서 /css/...를 호출하면 이게 rest Dispatcher가 처리하는데 매핑 관계 정보가 없어서 no mapping이 나타나는 것으로 보임
    • 매핑 관계 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <context:component-scan base-package="biz.api" >
    	<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
    
    <mvc:resources mapping="/css/**" location="/css/"></mvc:resources>
	<mvc:resources mapping="/images/**" location="/images/" ></mvc:resources>
	<mvc:resources mapping="/js/**" location="/js/" ></mvc:resources>
    
    <!-- 기본 메시지 converter 자동 추가 --> 
    <mvc:annotation-driven />
    
</beans>

 

 

 

pom.xml 수정

	<-- jackson 추가 -->
	<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.3</version>
    </dependency>

</dependencies>

 

 

 

 

다음 사이트의 글을 내 맘대로 번역한 글입니다

다운로드

myweblearner.com/springboot_2_aop?fbclid=IwAR3EGtIE2ig2QqvCErXp_Hg26V0HYgM0A18EDdCshE2-FvCicQx5FM6aIsg

 

Spring Boot and Spring AOP - Aspect Oriented Programming

In this tutorial, We are going to learn about using restful web service using spring boot

myweblearner.com

 

Overview

Aspect Oriented Programming (AOP)는 써야하지만 우아하지 않은 코드, 비지니스 로직들을 줄여주고 가독성을 유지시켜주는데 도움이 된다. 어떤 메소드를 실행하기 전 또는 후에 특정 task를 실행하고 싶다면 AOP를 사용하면 된다. 내부적으로 Spring은 proxy를 사용해서 실제 메소드 실행 클래스를 둘러싼 wrapper를 호출한다

  • JointPoint : 대상 메소드
  • PointCut : joint point를 매칭하기 위해 사용하는 predicate
  • Advice : PointCut call 전/후에 실행할 Code/Task
  • Aspect : pointcut과 advice의 묶음
  • Weaving : aspect가 실행되는 시점

 

About Demo

이번 demo에서는 restful webservice에서 Aspect를 사용하는 방법을 보여준다. 전체 package, 특정 class/method처럼 PointCut을 정의하는 방법에는 여러가지가 있다. 여기서는 AOP기반의 @annotation을 사용한다. 2개의 AOP를 만들어서 , 하나는 메소드의 전/후에 로그를 출력하고 다른 하나는 method의 성능을 계산하고 출력한다. DB 대신 List를 사용한다

 

Prerequisites

  • Maven 3.3.9
  • Java 8
  • STS (or Eclipse)

pom.xml

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

 

Model

import lombok.Data;

@Data
public class Employee {
	private String empId;
	private String empName;
	private String deptName;
	
}

 

RestControler

CRUD를 지원한다

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.budnamu.sample.aop.model.Employee;
import com.budnamu.sample.aop.service.EmployeeService;

@RestController
@RequestMapping("/aop")
public class EmployeeController {

	@Autowired
	private EmployeeService dummyService;
	
	// Get the employee detail based on employee id

	@GetMapping("/employee/{empid}")
	public Employee getEmployee(@PathVariable("empid") String empId) {
		return dummyService.getEmployee(empId);
	}
	
	//post the new employee details to backend

	@PostMapping("/employee")
	public String newEmployee(@RequestBody Employee employee) {
		return dummyService.addEmployee(employee);
	}
	
	// update the employee depart using emp id

	@PutMapping("/employee/{empid}")
	public String updateEmployee(@PathVariable("empid") String empId, @RequestParam("deptname") String deptName) {
		return dummyService.updateEmployeeDept(empId, deptName);
	}
	
	// Delete the employee

	@DeleteMapping("/employee/{empid}")
	public String deleteEmployee(@PathVariable("empid") String empId) {
		return dummyService.deleteEmployee(empId);
	}
	

}

 

Custom Annotation

MethodLogger.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.stereotype.Component;

@Component
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodLogger {

}

CalculatePerformance.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.stereotype.Component;

@Component
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CalculatePerformance {

}

 

Aspect Definition

@Aspect와 @Configuration annotation을 사용해서 aspect를 정의한다.

여기에서는 적용할 대상, 적용시점 그리고 무엇을 실행할 것인가에 대한 정보를 정의한다.

예를 들어 beforeMethodStart 메소드는 메소드를 호출하기 전에 실행하며 그 대상은 MethodLogger annotation이 선언된 메소드를 대상으로 한다.

import java.time.LocalDateTime;
import java.time.LocalTime;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Configuration;

import static java.time.temporal.ChronoUnit.SECONDS;;

@Configuration
@Aspect
public class LoggerAspect {

	@Before("@annotation(com.budnamu.sample.aop.aop.MethodLogger)")
	public void beforeMethodStart(JoinPoint point) {
		System.out.println("Method " + point.getSignature().getName() + " Started at " + LocalDateTime.now());

	}

	@After("@annotation(com.budnamu.sample.aop.aop.MethodLogger)")
	public void afterMethodStart(JoinPoint point) {
		System.out.println("Method " + point.getSignature().getName() + " Ended at " + LocalDateTime.now());

	}

	@Around("@annotation(com.budnamu.sample.aop.aop.CalculatePerformance)")
	public void calculate(ProceedingJoinPoint point) {
		LocalTime startTime = LocalTime.now();
		try {
			point.proceed();
		} catch (Throwable e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			LocalTime endTime = LocalTime.now();
			System.out.println("Processing time of Method " + point.getSignature().getName() + " -> "
					+ SECONDS.between(startTime, endTime));
		}	

	}

}

 

Service

annotaion을 적용할 메소드에 선언하면  aspect가 알아서 처리해 준다

getEmployee 메소드를 보면 @CalculatePerformance가 선언되어 있는데 이것은 메소드 호출 전후에 적용된다

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import org.springframework.stereotype.Component;

import com.budnamu.sample.aop.aop.CalculatePerformance;
import com.budnamu.sample.aop.aop.MethodLogger;
import com.budnamu.sample.aop.model.Employee;

@Component
public class EmployeeServiceImpl implements EmployeeService {

	private static List<Employee> employeeLst = new ArrayList<>();

	@Override
	@CalculatePerformance
	public Employee getEmployee(String empId) {
		Stream<Employee> empStream = employeeLst.stream().filter(emp -> {
			return emp.getEmpId().equalsIgnoreCase(empId);
		});
		sleepForSeconds(5000L);
		return empStream.findAny().get();
	}

	@Override
	@MethodLogger
	public String addEmployee(Employee e) {
		employeeLst.add(e);
		sleepForSeconds(3000L);
		return "Success";
	}

	@Override
	@MethodLogger
	public String updateEmployeeDept(String empId, String deptName) {
		employeeLst.stream().forEach(emp -> {
			if (emp.getEmpId().equalsIgnoreCase(empId)) {
				emp.setDeptName(deptName);
			}
		});
		sleepForSeconds(2000L);

		return "SUCCESS";
	}

	@Override
	@MethodLogger
	public String deleteEmployee(String empId) {
		System.out.println("Employee Id -->" + empId);
		if (employeeLst.removeIf(emp -> emp.getEmpId().equalsIgnoreCase(empId)))
			return "SUCCESS";
		else
			return "FAIURE";
	}

	public void sleepForSeconds(Long period) {
		try {
			Thread.sleep(period);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	static {
		Employee emp1 = new Employee();
		Employee emp2 = new Employee();
		// employee 1
		emp1.setEmpId("A1234");
		emp1.setEmpName("Sam");
		emp1.setDeptName("IT");
		// employee 2
		emp2.setEmpId("B1234");
		emp2.setEmpName("Tom");
		emp2.setDeptName("Finance");

		employeeLst.add(emp1);
		employeeLst.add(emp2);
	}

}

등록

로그

 

 

 

 

aop.zip
0.07MB

이 글은 다음의 글을 내 맘대로 번역한 글입니다

www.baeldung.com/spring-boot-repackage-vs-mvn-package

 

Difference Between spring-boot:repackage and Maven package | Baeldung

Learn the difference between mvn:package and spring-boot:repackage

www.baeldung.com

 

 

1. Overview

Apach Maven은 널리 사용되는 프로젝트 의존성 관리 tool이자 project building tool이다. 지난 몇 년간 Spring Boot는 application 개발을 위한 아주 인기가 많은 프레임워크가 되었다. Spring boot Maven Plugin이 있는데 Maven에서 Spring Boot supoort를 제공한다

 

 

application을 JAR or WAR artifact 형태로 Maven 을 이용해서 패키징하고 싶은 때가 있다. 이 때 mvn package를 사용하면 된다. 하지만 Spring Boot Maven Plugin은 repackage goal로 출고되는데 mvn command라고 한다.

 

가끔 두개의 명령어가 혼란스럽다. 이번 toturial에서는 mvn pckagespring-boot:repackage의 차이점에 대해서 설명한다

 

 

 

2. A Spring Boot Application Example

맨 처음 바로 Spring Boot application 을 만든다

@SpringBootApplication
public class SpringBootArtifacts2Application {

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

}

그리고 application이 빌드되고 기동된다는 것을 검증하기 위해 간단한 REST endpoint를 생성한다

@RestController
public class DemoRestController {
    @GetMapping(value = "/welcome")
    public ResponseEntity welcomeEndpoint() {
        return ResponseEntity.ok("Welcome to Baeldung Spring Boot Demo!");
    }
}

 

3. Maven package Goal

Spring Boot application을 빌드하려면 spring-boot-starter-web 의존성만 있으면된다.

<artifactId>spring-boot-artifacts-2</artifactId>
<packaging>jar</packaging>
...
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
...

하지만 STS를 이용해서 Spring Boot 프로젝트를 선택하면 아래처럼 maven-plugin도 자동으로 적용된다. 일단 이부분은 주석처리를 하고 실행한다

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

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
	</plugins>
</build>

 

 

Maven's package goal은 코드를 컴파일하고 패키징한다. 이 예제에서는 JAR format이다

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------< com.example:spring-boot-artifacts-2 >-----------------
[INFO] Building spring-boot-artifacts-2 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ spring-boot-artifacts-2 ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ spring-boot-artifacts-2 ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ spring-boot-artifacts-2 ---
[INFO] Not copying test resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ spring-boot-artifacts-2 ---
[INFO] Not compiling test sources
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ spring-boot-artifacts-2 ---
[INFO] Tests are skipped.
[INFO] 
[INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ spring-boot-artifacts-2 ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.016 s
[INFO] Finished at: 2021-02-03T01:14:27+09:00
[INFO] ------------------------------------------------------------------------

mvn package 명령어를 실행하고 나면 jar 파일이 생성된 것을 확인할 수 있고 JAR 파일 내부를 살펴보자

 

 

위 사진에서 볼 수 있듯이 mvn package 명령어의 결과로 JAR 파일이 만들어졌고 내부에는 resources와 컴파일된 Java class만이 포함되어 있다

실행에 필요한 libs도 없다.

 

이 JAR 파일을 이용해서 다른 프로젝트에 의존성으로 사용할 수도 있다. 하지만 Spring Boot application이면 이 JAR파일을 "java -jar JAR_FILE" 명령어를 이용해서 기동시키지 못한다. runtime 의존성이 포함되어 있지 않기 때문이다. 예를 들자면 web context를 기동시킬 수 있는 servlet container가 없다.

 

"java -jar" 명령어를 이용해서 간단하게 실행시킬려면 fat JAR이 필요하다. 이것을 만들기 위해 Spring Boot Maven Plugin이 필요하다

 

 

4. The Spring Boot Maven Plugin's repackage Goal

spring-boot:repackage가 무엇을 하는지 알아보자

 

 

4.1 Spring Boot Maven Plugin 추가하기

이미 위에서 설명했듯이 기본적으로 적용되어 있다.

 

4.2 spring-boot:repackage Goal 실행

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------< com.example:spring-boot-artifacts-2 >-----------------
[INFO] Building spring-boot-artifacts-2 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ spring-boot-artifacts-2 ---
[INFO] Deleting D:\workspaces\git\spring-boot-artifacts-2\target
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.4.2:repackage (default-cli) @ spring-boot-artifacts-2 ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.193 s
[INFO] Finished at: 2021-02-03T01:35:19+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.4.2:repackage (default-cli) on project spring-boot-artifacts-2: Execution default-cli of goal org.springframework.boot:spring-boot-maven-plugin:2.4.2:repackage failed: Source file must not be null -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginExecutionException

 

실패했다. 왜냐하면 spring-boot:repackage goal은 존재하는 JAR or WAR archive를 입력소스로 취하고 project runtime 의존성을 전부 final artifact내에 재패키징한다. 이런 방법으로 재패키징 된 artifact는 "java -jar JAR_FILE.jar" 명령어로 실행이 가능하다

 

따라서 먼저 JAR 파일을 만들고 그 후에 spring-boot:repackage goal을 실행해야 한다

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------< com.example:spring-boot-artifacts-2 >-----------------
[INFO] Building spring-boot-artifacts-2 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ spring-boot-artifacts-2 ---
[INFO] Deleting D:\workspaces\git\spring-boot-artifacts-2\target
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ spring-boot-artifacts-2 ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ spring-boot-artifacts-2 ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to D:\workspaces\git\spring-boot-artifacts-2\target\classes
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ spring-boot-artifacts-2 ---
[INFO] Not copying test resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ spring-boot-artifacts-2 ---
[INFO] Not compiling test sources
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ spring-boot-artifacts-2 ---
[INFO] Tests are skipped.
[INFO] 
[INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ spring-boot-artifacts-2 ---
[INFO] Building jar: D:\workspaces\git\spring-boot-artifacts-2\target\spring-boot-artifacts-2-0.0.1-SNAPSHOT.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.4.2:repackage (repackage) @ spring-boot-artifacts-2 ---
[INFO] Replacing main artifact with repackaged archive
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.4.2:repackage (default-cli) @ spring-boot-artifacts-2 ---
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.056 s
[INFO] Finished at: 2021-02-03T01:41:21+09:00
[INFO] ------------------------------------------------------------------------

  

아래 그림에서 볼 수 있듯이 original JAR file과 repackaged JAR file을 볼 수 있다.

 

JAR 파일 내부를 살펴보자

repackaged JAR 파일을 보면 컴파일된 Java classes와 Spring Boot application을 실행하는데 필요한 모든 runtime libraries 가 모두 포함되어 있다. 예를 들어, embedded tomcat library는 BOOT-INF/lib directory에 패키징되어 있다

 

4.3 Maven's package Lifecycle동안 spring-boot:repackage Goal 실행하기

pom.xml에서 Maven lifecycle의 package phase동안 Spring Boot Maven Plugin이 artifact를 재패키징한다는 것을 안다. 다른 말로하면 mvn package를 실행하면 spring-boot:repackage가 자동으로 실행된다다는 것이다

 

 

 

https://github.com/jwtk/jjwt

 

jwtk/jjwt

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

 

Java JWT : JSON Web Token for Java and Andorid

JJWT는 JVM과  Android에서 JWTs(JSON Web Tokens)를 생성하고 검증하기 위한 라이브러리를 쉽게 이해하고 쉽게 사용하는 것을 목적으로 한다

 

JJWT는 순수 자바 구현체이며 JWT, JWS, JWE, JWK 그리고 JWA는 제외하며 Apache 2.0 License를 준수한다.

 

이 라이브러리는 Okta의 Senior Architect인 Les Hazlewood가 만들었으며 community에 의해 유지된다.

 

Okta는 개발자들을 위한 완벽한 인증과 사용과 관리 API이다.

 

 

 

Features

 

 

 

 

 

 

 

 

 

 

JSON Web Token이란 무엇인가?

JWT는 양측 사이에서 간단하고, , 증명할 수 있는 형태로 정보를 수신하는 수단이다.

정보의 일부분은 claims라고 불리우는 JWT의 body에 인코딩된다. JWT의 확장된 형태는 JSON 형식이며, 따라서 각각의 claim은 JSON object에서 하나의 key다.

 

JWT는 암호학적으로 서명(JWS)하거나 암호화(JWE)할 수 있다.

 

이 기능들은 JWT 의 사용에 있어 신뢰성 있는 강력한 단계를 추가해준다. 수신자는 고차원의 신뢰성을 가지며 JWT는 서명을 검증함에 있어 간섭하지 않는다.

 

서명된 JWT의 간단한 표현은 하나의 스트링이며 '.'을 이용해서 3부분으로 나뉜다

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtypruRC4fb1DWtoLL62SY

각각의 부분은 Base64URL로 인코딩된다.

첫번째 부분은 header라고 하며, JWT를 서명하는 사용된 알고리즘에 대한 정보다.

두번재 부분은 body라고 하며 claimr을 인코딩한 정보를 포함하고 있다.

세번째 부분은 signature(서명)이며 header + body를 header에 명시된 알고리즘으로 계산한 결과값이다.

 

만약에 header와 body를 base64 디코딩하면 다음과 비슷한 결과를 얻을 수 있다.

header

{
  "alg": "HS256"
}

body

{
  "sub": "Joe"
}

이 경우에,  서명에 사용된 알고리즘은 SHA-256알고리즘을 이용한 HMAC이라는 것을 의미하며 body에는 1개의 claim이 존재하며 그 안에는 "sub"-"Joe" 형태를 가진다는 의미다.

 

Registered Claims라고 하는 표준 claim들이 있으며 , "sub"는 그런 종류 중 하나이다.

 

서명을 하기위해서 secret key가 필요하다.

 

설치

명심할 것은 JDK projectAndorid project에 따라 의존성에 차이가 있다는 것을 명심할 것!!!

JDK Projects

Maven

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<!-- Uncomment this next dependency if you are using JDK 10 or earlier and you also want to use 
     RSASSA-PSS (PS256, PS384, PS512) algorithms.  JDK 11 or later does not require it for those algorithms:
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.60</version>
    <scope>runtime</scope>
</dependency>
-->

JDK 10이하의 환경에서 RSASSA-PSS(PS256, PS384, PS512) 알고리즘을 사용하고자 하는 경우 주석부분을 해제하면 된다. JDK 11이상의 경우 자동으로 지원하기 때문에 명시할 필요는 없다.

 

2020.07.31 현재 https://mvnrepository에는 0.9.1 버전이 명시되어 있다.

 

Gradle

dependencies {
    compile 'io.jsonwebtoken:jjwt-api:0.11.2'
    runtime 'io.jsonwebtoken:jjwt-impl:0.11.2',
            // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
            //'org.bouncycastle:bcprov-jdk15on:1.60',
            'io.jsonwebtoken:jjwt-jackson:0.11.2' // or 'io.jsonwebtoken:jjwt-gson:0.11.2' for gson
}

 

 

Android Projects

안드로이드 프로젝트의 경우 다음의 의존성을 추가하고 코드에서는 Proguard에 따라야 한다

dependencies {
    api 'io.jsonwebtoken:jjwt-api:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' 
    runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.11.2') {
        exclude group: 'org.json', module: 'json' //provided by Android natively
    }
    // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
    //runtimeOnly 'org.bouncycastle:bcprov-jdk15on:1.60'
}

Proguard

-keepattributes InnerClasses

-keep class io.jsonwebtoken.** { *; }
-keepnames class io.jsonwebtoken.* { *; }
-keepnames interface io.jsonwebtoken.* { *; }

-keep class org.bouncycastle.** { *; }
-keepnames class org.bouncycastle.** { *; }
-dontwarn org.bouncycastle.**

 

 

JJWT 의존성의 이해

위에서 언급한 의존성은 complie-time 의존이며 그 외 나머지는 runtime 의존이다라는 사실을 명심할 것!

 

This is because JJWT is designed so you only depend on the APIs that are explicitly designed for you to use in your applications and all other internal implementation details - that can change without warning - are relegated to runtime-only dependencies. This is an extremely important point if you want to ensure stable JJWT usage and upgrades over time:

 

JJWT는 의미론적으로 jjwt-impl.jar 를 제외하고 나머지 artifact들에 대해 버전 호환성을 제공한다. 즉 jjwt-impl.jar에 대해서는 보장하지 않으며 내부 구현은 어느 순간에 변경된다. jjwt-impl.jar를 compile scope에 넣지 말고 runtime scope로 선언하라

 

This is done to benefit you: great care goes into curating the jjwt-api .jar and ensuring it contains what you need and remains backwards compatible as much as is possible so you can depend on that safely with compile scope. The runtime jjwt-impl .jar strategy affords the JJWT developers the flexibility to change the internal packages and implementations whenever and however necessary. This helps us implement features, fix bugs, and ship new releases to you more quickly and efficiently.

 

 

QuickStart

가장 복잡한 것들은 편리하고 읽기쉬운 builder 기반의 fluent interface 뒤에 존재하며 IDE를 사용하면 자동완성을 지원한다.

 

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;

// We need a signing key, so we'll create one just for this example. Usually
// the key would be read from your application configuration instead.
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

String jws = Jwts.builder().setSubject("Joe").signWith(key).compact();

 

  1. registered claim인 sub(subject)에 'Joe'를 가지는 JWT를 build
  2. HMAC-SHA-256알고리즘과 key를 이용해서 서명값 계산
  3.  JWS라고 하는 서명된 JWT 값을 생성하기

결과값인 jws String은 다음과 비슷하게 보일 것이다

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4

JWT를 검증해 보자

assert Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jws).getBody().getSubject().equals("Joe");

주의사항 : parseClaimsJws 메소드를 반드시 호출해야 한다. 비슷한 메소드들이 무수히 많은데 잘못 호출하면 UnsupportedJwtException을 던진다.

 

알아야 할 2가지 항목이 있다. 이전에 사용했던 key는 JWT의 서명을 검증하는데도 사용된다. 검증에 실패하면 SignatureException(JwtException을 확장한)이 던져진다. JWT가 올바르다면 claims를 분석하고 subject에 'Joe'가 설정되어 있는 것을 주장한다.

 

강렬한 효과를 가진 1줄짜리 코딩을 애용해야만 한다.

 

만약에 분석이나 서명 검증에 실패하면 어떻게 될까? JwtException을 catch하고 처리해야 한다

try {

    Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compactJws);

    //OK, we can trust this JWT

} catch (JwtException e) {

    //don't trust the JWT!
}

 

Signed JWTs

JWT 명세는 JWT를 서명하는 방법을 제공한다

  1. JWT가 우리가 잘 알고 있는 누군가에 의해 생성되었음을 보장한다
  2. JWT를 서명하고 나서는 JWT를 바꾸거나 변경하지 않았음을 보장한다.

2가지 속성 - authenticity(진본임) and integrity(완전성)-은 JWT에 우리가 신뢰할 수 있는 정보를 포함하고 있다는 것을 보장해 준다. 만약에 authenticity나 integrity 검사에 실패하게 되면, 신뢰할 수 없기 때문에 반려해야 한다

 

JWT 서명은 어떻게 하는가? 간단하게 읽기 쉬운 pseudocode를 가지고 진행한다

 

1. JSON 형태의 header와 body('Claim'이라 불리우는)를 가지는 JWT가 있다고 가정한다

header

{
  "alg": "HS256"
}

body

{
  "sub": "Joe"
}

 

2. JSON에서 필요없는 공백을 전부 제거한다

String header = '{"alg":"HS256"}'
String claims = '{"sub":"Joe"}'

 

3. base64로 인코딩한다

String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )

 

4. 인코딩된 header와 claims를 '.'로 연결한다

String concatenated = encodedHeader + '.' + encodedClaims

 

5. 충분히 강력한 secret or private key와 설정된 알고리즘을 이용해서 서명한다.

Key key = getMySecretKey()
byte[] signature = hmacSha256( concatenated, key )

 

6. 서명값이 byte array이므로, 서명값을 base64 인코딩하고 위의  concatenated 을 '.'를 이용해서 결합한다

String jws = concatenated + '.' + base64URLEncode( signature )

최종 결과값인 jws는 다음과 비슷하다

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4

이것을 서명된 JWT라고 하는,  짧게 JWS라고 한다

 

Of course, no one would want to do this manually in code, and worse, if you get anything wrong, you could cause security problems or weaknesses. As a result, JJWT was created to handle all of this for you: JJWT completely automates both the creation of JWSs as well as the parsing and verification of JWSs for you.

 

JJWT를 이용해서 JWS를 어떻게 생성하는지에 대해 보여줬지만 잠시 서명 알고리즘과 Key 그리고 JWT 명세와 관련된 부분에 대해 논의해 보자. 이것들에 대해 이해하는 것은 매우 중요하다.

 

Signature Algorithms Keys

JWT 명세에는 12가지의 표준 서명 알고리즘을 정의하고 3개의 secret key 알고리즘과 9개의 비대칭키 알고리즘에 대해 명시하고 있다.

 

  • HS256: HMAC using SHA-256
  • HS384: HMAC using SHA-384
  • HS512: HMAC using SHA-512
  • ES256: ECDSA using P-256 and SHA-256
  • ES384: ECDSA using P-384 and SHA-384
  • ES512: ECDSA using P-521 and SHA-512
  • RS256: RSASSA-PKCS-v1_5 using SHA-256
  • RS384: RSASSA-PKCS-v1_5 using SHA-384
  • RS512: RSASSA-PKCS-v1_5 using SHA-512
  • PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
  • PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
  • PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512

이것들은 전부 io.jsonwebtoken.SignatureAlgorithm enum에 정의되어 있다.

이 중에서 가장 중요한 것은 선택한 알고리즘에 대해 충분히 강력한 Key를 사용해야 한다는 것이다.

JJWT가 선택한 서명 알고리즘에 대해 충분히 강력한 key를 사용하게끔 JJWT가 강제한다는 것을 의미한다.

만약, 선택한 알고리즘에 대해 약한 key를 제공하면 JJWT를 거부하고 예외를 던진다

 

이것은 개발자의 삶을 피곤하게 하기 위함이 아니다. 잔말 말고 반드시 특정 길이 이상의 key를 사용해라

 

HMAC-SHA

각각의 알고리즘에 대해 필수요구사항은 다음과 같다

  • HS256(HMAC-SHA-256)의 서명값은 256bits(32 bytes)이며, 32 bytes 이상의 key length를 요구한다
  • HS384(HMAC-SHA-384)의 서명값은 384bits(48 bytes)이며, 48 bytes 이상의 key length를 요구한다
  • HS512(HMAC-SHA-512)의 서명값은 512bits(64 bytes)이며, 64 bytes 이상의 key length를 요구한다

 

RSA

모든 알고리즘에는 최소한 2048 bits 이상이어야만 한다. 이보다 작으면 InvalidKeyException을 던진다

  • RS256, PS256 : 2048 bits 이상
  • RS384, PS384 : 3072 bits 이상
  • RS512, PS512 : 4096 bits  이상

이것들은 JJWT의 제안사항이지 요구사항은 아니다. 모든 RSA 알고리즘에 대해 2048 bits 이상의 key를 적용하면 동작한다.

 

Elliptic Curve

  • ES256 : 256 bits 이상
  • ES384 : 384 bits 이상
  • ES512 : 512  bits 이상

 

안전한 Key 생성하기

key 길이에 대해 고민하고 싶지 않은 경우 JJWT는 io.jsonwebtoken.security.Keys utility class를 제공하는 , 선택한 알고리즘에 대해 충분히 안전한 key를 생성해 준다.

 

 

Secret Keys

JWT HMAC-SHA 알고리즘에서 충분히 안전한 key를 생성하고자 한다면 Keys.secretKeyFor(SignatureAlgorithm) helper 메소드를 사용하자

SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512

JJWT는 JCA provider's KeyGenerator를 사용해서 주어진 알고리즘에 대해 정확한 최소 길이이 secure-random key를 생성한다.

 

생성된 secretKey를 저장할 때 base64 인코딩해서 저장하라

String secretString = Encoders.BASE64.encode(key.getEncoded());

결과값인 secretString은 결과 안전하지가 않다. 추후에 안전하게 암호화 등의 방법을 사용한 후에 저장하도록 해야 한다.

 

Asymmetric Keys

ECDSA나 RAS 알고리즘을 선택하는 경우 Elliptic Curve or RSA 비대칭  key를 생성하려면 Keys.keyPairFor(SignatureAlgorithm) helper 메소드를 사용한다.

KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512

private key(keyPair.getPrivate())를 사용해서 JWS를 생성하고 public key(keyPair.getPublic())을 이용해서 JWS 분석하고 검증한다.

 

주의사항 : PS256. PS384, PS512는 JDK 11을 사용하거나 적절한 JCA provider(BouncyCastle)을 runtime classpath에 등록하면 사용가능하다.

 

JWS 생성하기

  1. Jwts.builder() 메소드를 이용해서 JwtBuilder 인스턴스를 생성한다
  2. JwtBuilder 메소드를 이용해서 header parameters와 claims를 추가한다
  3. SecretKey나 asymmetric PrivateKey를 설정한다
  4. 마지막으로 compact() 메소드를 호출해서 최종 jws를 생성한다
String jws = Jwts.builder() // (1)

    .setSubject("Bob")      // (2) 

    .signWith(key)          // (3)
     
    .compact();             // (4)

 

Header Parameters

JWT Header는 JWT의 Claims에 관련한 content, format and cryptographic operations에 대한 meta-data를 제공한다.

 

1개 이상의 JWT header parameters를 설정하고 싶다면, JwtBuilder의 setHeaderParam를 이용하면 된다

String jws = Jwts.builder()

    .setHeaderParam("kid", "myKeyId")
    
    // ... etc ...

setHeaderParam이 호출될 때마다 내부의 Header instance에 name/value를 확장한다. 동일한 이름의 name/value는 덮어쓴다.

 

주의사항 : Header에 alg나 zip은 설정하지 마라. 자동으로 설정된다

 

Header Instance

한번에 완전한 header를 지정하고 싶으면 Jwts.header() 메소드를 호출하라

Header header = Jwts.header();

populate(header); //implement me

String jws = Jwts.builder()

    .setHeader(header)
    
    // ... etc ...

주의사항 : setHeader를 호출하면 기존에 존재하는 동일한 이름의 name/value 를 덮어쓸 수 있다.

 

Header Map

Map을 이용하는 방법도 있다.

Map<String,Object> header = getMyHeaderMap(); //implement me

String jws = Jwts.builder()

    .setHeader(header)
    
    // ... etc ...

 

Claims

Claims은 JWT's 'body'이며 JWT 생성자가 수신자에게 알리고 싶은 내용을 포함한다.

 

Standard Claims

JwtBuilder 다음의  Claim들을 set 메소드를 이용해서 설정한다

String jws = Jwts.builder()

    .setIssuer("me")
    .setSubject("Bob")
    .setAudience("you")
    .setExpiration(expiration) //a java.util.Date
    .setNotBefore(notBefore) //a java.util.Date 
    .setIssuedAt(new Date()) // for example, now
    .setId(UUID.randomUUID()) //just an example id
    
    /// ... etc ...

 

Custom Claims

표준 claim말고 1개 이상의 custom claims를 설정하려면 JwtBuilder의 claim 메소드를 사용한다

String jws = Jwts.builder()

    .claim("hello", "world")
    
    // ... etc ...

동일한 이름이면 덮어쓴다.

 

standart claim name에 대해 claim을 호출할 필요는 없다. 특정  setter 메소드를 사용하는 것을 추천한다

 

Claim Instance

 

한번에 설정하고자 하는 경우 Jwts.claims()를 사용한다

Claims claims = Jwts.claims();

populate(claims); //implement me

String jws = Jwts.builder()

    .setClaims(claims)
    
    // ... etc ...

동일한 이름이면 덮어쓴다.

 

Claim Map

Map을 이용해서 JwtBuidler.setClaims(Map)를 사용할 수 있다

Map<String,Object> claims = getMyClaimsMap(); //implement me

String jws = Jwts.builder()

    .setClaims(claims)
    
    // ... etc ...

동일한 이름이면 덮어쓴다.

 

 

Signing Key

Jwtbuilder의 signWith 메소드를 호출해서 서명 key를 설정하는 것을 추천한다.

String jws = Jwts.builder()

   // ... etc ...
   
   .signWith(key) // <---
   
   .compact();

256 bits(32 bytes) 길이를 가진 key를 SecretKey로 사용하더라도 HS384나 HS512 알고리즘에 대해서는 충분하지가 않다. HS256알고리즘에 대해서는 충족한다.

 

signWith를 사용하면 JJWT가 header의 alg에 값을 자동으로 설정해 준다

 

4096 bits 길이의 RSA private key를 signWith 메소드에 사용한다면, JJWT는 header에 alg값으로 'RS512'를 설정한다.

 

Elliptic Curve privateKey에 대해서도 동일하게 동작한다.

 

주의사항 : public key를 이용해서 JWT를 서명할 수 없다. publick key를 사용하면 JJWT 는 InvalidKeyException을 던진다.

 

SecretKey Formats

HMAC-SHA 알고리즘을 이용하는 경우 secret key의 타입은 String이나 인코딩된 byte array이다. signWith 메소드를 호출하려면 이 값을 SecretKey instance를 변환해야 한다

 

  • 인코딩된 byte array
SecretKey key = Keys.hmacShaKeyFor(encodedKeyBytes);
  • base64 인코딩된 String
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
  • base64URL 인코딩된 String
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString));
  • 평문 string
SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));

 

secretString.getBytes()(charset없이 사용하는)은 항상 부정확하니 사용하지 말것!

평문을 암호화  key로 사용하지 말것!. 안전하지 못하다.

Secure-random key가 가장 강력하다. 이걸 사용해라!

 

 

SignatureAlgorithm Override

예를들어 key는 RS256용인데 알고리즘은 RS384나 RS512를 쓰고 싶은 경우

.signWith(privateKey, SignatureAlgorithm.RS512) // <---
   
.compact();

이것은 RSA 알고리즘의 특징인데 key의 길이가 2048 bits 이상이면 아무 알고리즘에서도 동작한다.

 

선택한 알고리즘에서 정한 key 길이를 준수하는게 정신건강에 좋다

 

JWS Compression

JWT claims set이 크고 동일한 lib를 이용해서 분석과 읽기를 한다면 JWT의 사이즈를 감소시키는게 도움이 된다.

JWS에 대해서는 쓰지 마라!

 

 

Reading a JWS

  1. Jwts.parserBuilder()를 이용해서 JwtParserBuilder instance를 생성한다
  2. SecretKey 또는 asymmetric PublicKey를 설정한다
  3. JwtParserBuilder의 build()를 호출해서 thread-safe한 JwtParser를 생성한다
  4. 마지막으로 parseClaimsJws(String)를 호출해서 원래의 JWS를 생성한다
  5. try/catch를 이용해서 parsing 오류나 서명 검증 오류를 처리한다
Jws<Claims> jws;

try {
    jws = Jwts.parserBuilder()  // (1)
    .setSigningKey(key)         // (2)
    .build()                    // (3)
    .parseClaimsJws(jwsString); // (4)
    
    // we can safely trust the JWT
     
catch (JwtException ex) {       // (5)
    
    // we *cannot* use the JWT as intended by its creator
}

결국 이 과정은 서명이 올바르게 된 것인지를 검증하고 claim에 저장된 값을 추출하는 과정이다.

 

Verification Key

 

JWS을 읽는데 가장 중요한 것은 서명 때 사용한 key다.

어떤 key를 사용해서 검증해야 할까?

  • SecretKey를 사용했다면 검증할 때에도 동일한 SecretKey를 사용해야 한다
Jwts.parserBuilder()
    
  .setSigningKey(secretKey) // <----
  
  .build()
  .parseClaimsJws(jwsString);
  • PrivateKey를 사용했다면 검증할 때에는 PublicKey를 사용해야 한다
Jwts.parserBuilder()
    
  .setSigningKey(publicKey) // <---- publicKey, not privateKey
  
  .build()
  .parseClaimsJws(jwsString);

 

만약에 SecretKey나 KeyPair를 사용하지 않는다면 어떻게 해야 할까?

SigningKeyResolver를 사용할 수 있다.

 

Singing Key Resolver

SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();

Jwts.parserBuilder()

    .setSigningKeyResolver(signingKeyResolver) // <----
    
    .build()
    .parseClaimsJws(jwsString);

아니면 SigningKeyResolverAdapter를 구현하면 된다

public class MySigningKeyResolver extends SigningKeyResolverAdapter {
    
    @Override
    public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
        // implement me
    }
}

The JwtParser will invoke the resolveSigningKey method after parsing the JWS JSON, but before verifying the jws signature. This allows you to inspect the JwsHeader and Claims arguments for any information that can help you look up the Key to use for verifying that specific jws. This is very powerful for applications with more complex security models that might use different keys at different times or for different users or customers.

 

 

어떤 데이터를 검사할 것인가?

JWT 명세는 kid (Key Id) 필드를 이용해서 지원한다.

Key signingKey = getSigningKey();

String keyId = getKeyId(signingKey); //any mechanism you have to associate a key with an ID is fine

String jws = Jwts.builder()
    
    .setHeaderParam(JwsHeader.KEY_ID, keyId) // 1
    
    .signWith(signingKey)                    // 2
    
    .compact();

파싱하는 과정에서 SigningKeyResolver는 JwsHeader에서 kid 값을 추출한 다음 이를 이용해서 key를 생성한다

public class MySigningKeyResolver extends SigningKeyResolverAdapter {
    
    @Override
    public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
        
        //inspect the header or claims, lookup and return the signing key
        
        String keyId = jwsHeader.getKeyId(); //or any other field that you need to inspect
        
        Key key = lookupVerificationKey(keyId); //implement me
        
        return key;
    }
}

jwsHeader.getKeyId()를 검사하는 것은 가장 일반적인 방법이다. 임의의 heder 필드를 사용해도 괜찮다.

 

 

Claim Assertions

분석하는 과정에서 특정 값을 가지고 있는지 확인하게끔 할 수 있다.

예를 들어, sub(subject)가 존재하는지 검사하고 싶다면 require* 메소드를 사용하면 된다

try {
    Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s);
} catch(InvalidClaimException ice) {
    // the sub field was missing or did not have a 'jsmith' value
}

확인과정에서 해당 key가 존재하지 않는다면 MissingClaimException을, key가 존재하지만 값이 일치하지 않는다면 IncorrectClaimException을 던진다

try {
    Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s);
} catch(MissingClaimException mce) {
    // the parsed JWT did not have the sub field
} catch(IncorrectClaimException ice) {
    // the parsed JWT had a sub field, but its value was not equal to 'jsmith'
}

custom claim에 대해서는 require(fieldName, requiredFieldValue) 메소드를 사용하면 된다

try {
    Jwts.parserBuilder().require("myfield", "myRequiredValue").setSigningKey(key).build().parseClaimsJws(s);
} catch(InvalidClaimException ice) {
    // the 'myfield' field was missing or did not have a 'myRequiredValue' value
}

여기서도 해당 key가 존재하지 않는다면 MissingClaimException을, key가 존재하지만 값이 일치하지 않는다면 IncorrectClaimException을 던진다

 

 

Accounting for Clock Skew

 

 

Custom Clock Support

 

 

JWS Decompression

 

+ Recent posts