요즘 가장 관심이 있는 java쪽 기능/피쳐

모든 기존 프로덕트를 재 개발할수도 없고, 리액티브 스타일로 개발시 여러 허들과 트러블 슈팅도 있어서..

java virtual thread의 draft(2023/03/06 created)
https://openjdk.org/jeps/8303683

 

 

노이즈가 좀 있는 내용 같지만 추가 참고 글

 - https://news.hada.io/topic?id=9250

 

 

jjwt를 사용해서 보통 JWT 처리를 하는데 혹시 secretKey 기존에 대충 만들었다면(길이가 짧게) 0.10.0 버전부터 에러가 발생함

 - https://github.com/jwtk/jjwt/issues/334

https://github.com/jwtk/jjwt/blob/6b980553cfb4435ec14b7842af5b529735acbb2d/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java#L364

 

 

이에 간단히 사용하는 SecretKey 생성 소스

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;

public class JwtSecretKeyMaker {

	/**
	 * JJWT secretKey를 생성할때 알고리즘에 맞는 length로 만듬
	 *  - https://github.com/jwtk/jjwt#jws-key-create
	 *  - jjwt 0.10.0 부터는 length가 작으면 에러가 발생함: https://github.com/jwtk/jjwt/issues/334
	 *
	 * @param args
	 */
	public static void main(String[] args) {

		//Creating Safe Keys(length: 256bit))
		SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // HS256: HMAC-SHA256
		String secretString = Encoders.BASE64.encode(key.getEncoded());

		System.out.println(secretString);

	}
}
  1. 사전 필요사항
    1. AWS CloudFront 설정
1. 메뉴
 - AWS 웹 콘솔 -> CloudFront -> 대상 CloudFront ID(도메인에 맵핑된) -> Behaviors -> 해당 Behaviors 선택 -> Edit

2.  Cache key and origin requests 영역 설정
 1) Cache policy and origin request policy (recommended) 선택
 2) Cache policy 를  CachingDisabled 로 설정
  - CloudFront를 동적 컨텐츠 전송 목적과 위치정보 헤더 값등 필요 헤더를 획득할 목적이기 때문에
  - 참고: https://aws.amazon.com/ko/cloudfront/dynamic-content/
  - 참고: https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html
 3) Origin request policy - optional 에 커스텀 policy추가해서 반영
  - 샘플) CloudFront에서 추가되는 헤더를 모두 선택한 경우

설정샘플(주의: 뒷단 was까지 Query Strings와 Cookies를 넘겨줘야하기 때문에 ALL로 설정)

  1. Java 샘플소스
/**
	 * 클라이언트 요청자의 IP를 가져옴(WAS 앞단에 로드밸런서 등이 존재할 수 있음을 감안한 메소드)
	 *  - AWS CloudFront -> ALB(LB) -> Nginx-> 톰캣을 사용하는 케이스를 감안한 메소드
	 *  - AWS CloudFront사용시 해당 헤더의 IP정보를 최우선으로 신뢰함(즉, IP를 얻어오는 순서에 의미가 있음)
	 *  - 주의: Spring webflux는 해당 메소드를 사용불가(대신 ServerWebExchange exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() 같은 메소드를 사용해야함)
	 *
	 * @param request HttpServletRequest
	 * @return
	 */
	public static String getClientIp(HttpServletRequest request) {

		//참고 https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/using-cloudfront-headers.html#cloudfront-headers-viewer-location
		//참고 https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/WhatsNew.html
		final String awsCfAddress = request.getHeader("CloudFront-Viewer-Address"); //ex) 123.45.67.89:46532
		if (StringUtils.isNotBlank(awsCfAddress)) {
			log.trace("\nIP획득 출처: AWS CloudFront-Viewer-Address\n\t요청 http header => {}", HttpRequestUtil.getRequestHeaderToMap(request));
			return awsCfAddress.split(":")[0].trim(); //구분자 :
		}

		//프록시 레이어를 감안한 XFF값
		//TODO: XFF는 spoofed(속일 수 있음)가 가능하기 때문에 오른쪽부터 IP를 가져오도록 수정해야함. 이때 신뢰하는 프록시IP들은 제외하는 로직 작업도 필요 https://news.hada.io/topic?id=6098
		//https://en.wikipedia.org/wiki/X-Forwarded-For
		//https://blog.lael.be/post/8989
		final String xForwardedFor = request.getHeader("X-Forwarded-For"); //ex) 123.456.78.99,15.162.1.11
		if (StringUtils.isNotBlank(xForwardedFor)) {
			log.trace("\nIP획득 출처: X-Forwarded-For\n\t요청 http header => {}", HttpRequestUtil.getRequestHeaderToMap(request));
			return xForwardedFor.split(",")[0].trim(); //구분자 ,
		}

		final String realIp = request.getHeader("X-Real-IP"); //일반적으로는 인프라(프록시) 설정 들이 잘되어 있다면 다른 언어 및 프레임웤에서는 보안문제 때문에 Real IP가 XFF보다 우선순위 높음
		if (StringUtils.isNotBlank(realIp)) {
			log.trace("\nIP획득 출처: X-Real-IP\n\t요청 http header => {}", HttpRequestUtil.getRequestHeaderToMap(request));
			return realIp;
		}

		log.trace("\nIP획득 출처: request.getRemoteAddr\n\t요청 http header => {}", HttpRequestUtil.getRequestHeaderToMap(request));
		return request.getRemoteAddr();
	}

AWS ALB의 Access log의 파서 프로그램입니다. 파이썬 버전이고 간혹 사용해서 메모 용도르 저장해둡니다.

 

import re

#참고
# 1) https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/application/load-balancer-access-logs.html
# 2) https://stackoverflow.com/questions/68875527/regex-for-python-based-logparser-for-printing-aws-elb-logs

fields = [ "type",
"time",
"elb",
"client_ip",
"client_port",
"target_ip",
"target_port",
"request_processing_time",
"target_processing_time",
"response_processing_time",
"elb_status_code",
"target_status_code",
"received_bytes",
"sent_bytes",
"request_type",
"request_url",
"request_protocol",
"user_agent_browser",
"ssl_cipher",
"ssl_protocol",
"target_group_arn",
"trace_id",
"domain_name",
"chosen_cert_arn",
"matched_rule_priority",
"request_creation_time",
"actions_executed",
"redirect_url",
"lambda_error_reason",
"target_port_list",
"target_status_code_list",
"classification",
"classification_reason" ]



field = str(input("what is the field needed? "))
regex = r'([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\s]+?)\" \"([^\s]+)\" \"([^ ]*)\" \"([^ ]*)\"'

def ParseLogFile(file):
    resultDict = {}

    with open(file, 'r') as log:
        line = log.readline()
        while line:
            line_split = re.split(regex, line)
            line_split = line_split[1:len(line_split) - 1]
            index = fields.index(field)
            val = line_split[index]
            resultDict.setdefault(val, 0)
            resultDict[val] += 1
            line = log.readline()
        return resultDict
if __name__ == '__main__':
    result=ParseLogFile("C:\\HOME\\2.log")
    print(result)

자주 잊어버려서 메모

 

nginx location 기본 문법
 - https://lahuman.github.io/nginx_location_options/
-----

# 정확하게 일치 
location = / {
    [ configuration A ]
}

# 지정한 패턴으로 시작
location / {
    [ configuration B ]
}

# 지정한 패턴으로 시작
location /documents/ {
    [ configuration C ]
}

# 지정한 패턴으로 시작 패턴이 일치 하면 다른 패턴 탐색 중지( 정규식 아님 )
location ^~ /images/ {
    [ configuration D ]
}

# 정규식 표현 일치 - 대소문자 구분
location ~ \.(gif|jpg|jpeg)$ {
    [ configuration E ]
}

# 정규식 표현 일치 - 대소문자 구분 안함
location ~* \.(gif|jpg|jpeg)$ {
    [ configuration F ]
}

AWS 람다 + SNS조합해서 바운스된 이메일 DB구축 -> 서비스에서는 적절하게 로직/유저 메시지 처리해야함

 

관련해서 참고 링크

 - https://docs.aws.amazon.com/ses/latest/dg/notification-contents.html

 - 바운스되는 종류(bounce type): https://docs.aws.amazon.com/ses/latest/dg/notification-contents.html#bounce-types

  => 케이스별로 적절하게 로직처리(예. MailboxFull이면 유저에게 메일함이  꽉 찾는지 확인해서 조치요청 UI)

 

 

추가글 참고
- https://blog.eomsh.com/199

'AWS > ' 카테고리의 다른 글

CloudWatch 인스턴스의 메모리 및 디스크 Matrix 추가  (0) 2016.08.07

자동화를 위해서 애플 앱스토어의 여러 기능을 개발할 수 있습니다.(애플에서 API일부를 제공해줘서)

 

그 중 앱의 IAP 리스트를 조회하는 API샘플입니다.

 - 주의: API는 호출양 제한이 걸려있습니다.(링크 참고)

 

List All In-App Purchases for an App

  - https://developer.apple.com/documentation/appstoreconnectapi/list_all_in-app_purchases_for_an_app

 

java 샘플 소스

 - git 주소: https://github.com/oshnew/spring-boot-ver2-study/commit/b04e4293e983bf7425be17a97aaac6c80f70a26b

import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import sample.apple.AppStoreConnectApi.AppStoreConnectApiJWT;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * 특정 애플 앱의 IAP로 등록된 상품 리스트 조회
 *  - 참고: https://developer.apple.com/documentation/appstoreconnectapi/list_all_in-app_purchases_for_an_app
 *
 * @author
 */
public class ListInAppPurchasesForAnApp {

	public static void main(String[] args) throws IOException {

		final String issuer = "입력필요";
		final String keyIdentifier = "입력필요"; //전달받은 사설키의 키 ID
		final String privateKeyFilePath = "입력필요"; //사설키 경로(제품개발 권한 액세스). 애플 앱스토어 웹 콘솔에서 다운로드 받음. 키에 허용권한(ACCOUNT_HOLDER, ADMIN, APP_MANAGER 중 1가지) 필요

		int expireAfterMinutes = 20; //20분 넘으면 안됨

		AppStoreConnectApiJWT clzJWT = new AppStoreConnectApiJWT();
		final String jwt = clzJWT.makeApiAuthJwt(issuer, keyIdentifier, expireAfterMinutes, privateKeyFilePath); //인증용 JWT생성

		System.out.println("인증용 JWT: " + jwt);

		OkHttpClient client = new OkHttpClient.Builder().readTimeout(4, TimeUnit.SECONDS).connectTimeout(1, TimeUnit.SECONDS).writeTimeout(5, TimeUnit.SECONDS).build();

		final String id = "입력필요"; //애플 앱스토어 웹 콘솔에서 'Apple ID'로 표시되는 부분, 앱의 구분용ID로 추정됨
		String prefixUri = String.format("https://api.appstoreconnect.apple.com/v1/apps/%s/inAppPurchasesV2", id);

		HttpUrl.Builder httpBuilder = HttpUrl.get(prefixUri).newBuilder();
		httpBuilder.addQueryParameter("limit", "200"); //최대 200건까지 API에서 제약됨
		httpBuilder.addQueryParameter("filter[inAppPurchaseType]", "NON_CONSUMABLE"); //비 소모성만 조회하기 필터링

		final String fullUrl = httpBuilder.build().toString();
		System.out.println("fullrUrl: " + fullUrl);

		Request request = new Request.Builder().addHeader("Authorization", "Bearer " + jwt).url(fullUrl).build();
		Response response = client.newCall(request).execute();

		if (response.isSuccessful()) {
			String rtnMsg = response.body().string();
			System.out.println(String.format("성공. 응답 msg => '%s", rtnMsg));
		} else {
			System.out.println(String.format("응답에러(200이 아님). 응답 code:'%s'\n errMsg:'%s'", response.code(), response.body().string()));
		}

	}
}

 

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;

import java.io.BufferedReader;
import java.io.FileReader;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 애플 App Store Connect API에서 사용할 인증용 JWT토큰을 생성
 *
 * @author
 */
public class AppStoreConnectApiJWT {

	/**
	 * 애플 App Store Connect API에서 사용할 인증용 JWT토큰을 생성하는 샘플 소스
	 * - 참고: https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests
	 *
	 * @param issuer Your issuer ID from the API Keys page in App Store Connect; for example, 57246542-96fe-1a63-e053-0824d011072a.
	 * @param keyIdentifier
	 * @param expireAfterMinutes
	 * @param privateKeyFilePath
	 * @return
	 */
	public String makeApiAuthJwt(String issuer, String keyIdentifier, int expireAfterMinutes, String privateKeyFilePath) {

		if (expireAfterMinutes > 20) {
			throw new IllegalArgumentException(
				"expireAfterMinutes는 20보다 작아야합니다. 참고: https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests");
		}

		final String strPrivateKey = readPrivateKeyBody(privateKeyFilePath);
		PrivateKey privateKeyObj = convertToPrivateKeyObj(strPrivateKey);

		Map<String, Object> header = new HashMap<>();
		header.put("alg", SignatureAlgorithm.ES256);
		header.put("kid", keyIdentifier);
		header.put("typ", "JWT");

		final Date now = new Date();

		//주의: 일반적으로 만료시간이 20분보다 크면 애플에서 거부함. https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests#3878467
		LocalDateTime expLdt = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault()).plusMinutes(10);
		Date expiredAt = java.sql.Timestamp.valueOf(expLdt);

		// @formatter:off
		String jwt =  Jwts.builder()
		.setHeader(header)
		.setIssuer(issuer)
		.setAudience("appstoreconnect-v1") //애플 API정의서
		.setIssuedAt(now)  //발행 시간
		.setExpiration(expiredAt) //만료시간
		.signWith(SignatureAlgorithm.ES256, privateKeyObj)
		.compact();

		// @formatter:on

		return jwt;
	}

	private static PrivateKey convertToPrivateKeyObj(String strPrivateKey) {

		try {
			byte[] encodedKey = Base64.decodeBase64(strPrivateKey);
			return KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
		} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * private key 내용을 얻어옴
	 *  -  -----BEGIN PRIVATE KEY----- 또는 -----END PRIVATE KEY----- 와 같은 가이드라인 줄은 제외하고 실제 사용하는 부분만 파일에서 가져옴
	 *
	 * @param filePath
	 * @return
	 */
	private String readPrivateKeyBody(String filePath) {

		try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {

			String line;
			StringBuilder sb = new StringBuilder();
			while ((line = br.readLine()) != null) {
				if (line.contains("PRIVATE KEY")) { //guard line은 pass
					continue;
				}
				sb.append(line);
			}

			return sb.toString();

		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}
}

 

java 11 vs 17 성능 관련 자료 정리(작성 중)

 

 

참고 자료

  1. https://www.optaplanner.org/blog/2021/09/15/HowMuchFasterIsJava17.html

+ Recent posts