Java

Jeus 6에서 fmt:formatDate 문제

프로젝트에서 Tomcat 5.5 환경에서 개발을 하다 실 서버 환경이 Tomcat에서 Jeus 6로 변경되었다.

다행스럽게 사용하는 태그라이브러리가 별다른 문제없이 동작되는 것을 확인했었는데,

우연히 IE를 사용하여 테스트 중 특정 페이지가 제대로 출력되지 않는 문제를 발견했다.

소스에는 문제가 없었는데 이유는 단순하게도 IE의 인코딩이 문제가 되는 페이지만

UTF-8 인코딩을 EUC-KR로 잘못 인지하는 것이다.

게다가 원인은 <fmt:formatDate />태그를 사용하면 인코딩이 잘못 인지되는 것이었다.

이를 해결하기 위해서는 <fmt:formatDate />태그를 사용하기 전에 <fmt:setLocale value=”UTF-8″/> 를 추가하면 된다.

<%@page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko" lang="ko">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
...
<jsp:useBean id="testDate" class="java.util.Date"/>
<fmt:setLocale value="UTF-8"/>
날짜: ${testDate}
<fmt:formatDate pattern="yyyy.MM.dd" value="${testDate}"/>
</body>
</html>

Tomcat Parameters: Invalid chunk ignored.

Tomcat을 사용할 때 가끔씩 다음과 같은 오류가 발생할 때가 있다.

경고: Parameters: Invalid chunk ignored.

이것은 Tomcat의 Parameters 클래스에서 출력하는 것으로 Tomcat 5.5.27 소스를 보면 다음과 같다.

apache-tomcat-5.5.27-src/connectors/util/java/org/apache/tomcat/util/Parameters.java

public void processParameters( byte bytes[], int start, int len,
        String enc )
{
...
    if( nameEnd<=nameStart ) {
        log.warn("Parameters: Invalid chunk ignored.");
        continue;
        // invalid chunk - it's better to ignore
    }
...
}

소스를 보면 이 경고 메시지는 매개변수 명과 값이 구분안되는 경우 (예를 들어 요청중에  &= 또는 &&와 같은 경우) 나타날 수 있다.

호출하는 쪽을 수정하면 좋으나 이것이 불가능할 때는 오류 메시지가 출력되지 않도록 할 수 밖에 없다.

CATALINA_HOME/conf/logging.properties 파일의 마지막에 다음 줄을 추가하고 Tomcat을 재시작 한다.

org.apache.tomcat.util.http.Parameters.level = SEVERE

Java UTF-8과 서울신용평가정보 가상식별 실명확인서비스

공공 프로젝트에서 서울신용평가정보의 가상식별 실명확인서비스를 적용하다 보니
확인된 성명(한글)이 깨지는 문제를 만나게 되었다.

여러가지 테스트를 해보니 문제 상황은 다음과 같다.

Java 소스 및 JSP와 Tomcat의 Connector의 URIEncoding은 UTF-8이다.

Tomcat을  Locale을 UTF-8 상태에서 실행하거나

Tomcat 실행 옵션에 file.encoding 시스템 프로퍼티를 UTF-8로 설정하고 실행하면 문제가 발생되었다.

만약 Locale이 EUC-KR이거나 file.encoding=EUC-KR 이면 성명이 깨지지 않았다.

이제 UTF-8 상태에서 Tomcat을 실행하는 것이 기본이 된 것 같은데 아직도 2% 부족하다는 것을 알 수 있다.

단순한 서비스 하나를 위해서 EUC-KR로 Tomcat을 실행하는 것은 앞뒤가 맞지 않다고 생각한다.

이를 해결하기 위해 서울신용평가정보에서 제공된 클래스의 메소드 com.sci.vname.secu.aes.SciPacketConversion#RecvWritePacket(String)를 보니 역시 bytes를 String으로 만들 때 그냥 만들도록 되어있었다.

문제를 해결할 겸 좀 더 쉽게 사용하기 위해서 클래스를 만들었다.

/**
 * @(#)SciVname.java
 *
 * Copyright (C) 2009 D.TRIBE. All rights reserved.
 *
 * THIS SOFTWARE IS THE PROPRIETARY INFORMATION OF DTRIBE, INC.
 * USE IS SUBJECT TO LICENSE TERMS.
 */
package com.dtribe.scivname;

import com.dtribe.model.BaseModel;
import com.sci.vname.secu.aes.SciHttpSecuX;
import com.sci.vname.secu.hmac.SciHmac;

/**
 * 서울신용평가정보 가상 식별 실명 확인<br/>
 * <p>
 * 서울신용평가정보에서 제공하는 클래스 코드 사용 예:
 * <pre>{@code
 * String org = request.getParameter("retInfo");
 * String result = com.sci.vname.secu.aes.SciPacketConversion.RecvWritePacket(org).toString();
 * String[] data = result.split("/");
 * String name = "";
 * if(data.length == 7) {
 *   log.debug("reqNum=" + data[0]);
 *   log.debug("vDiscrNo=" + data[1]);
 *   log.debug("name=" + data[2]);
 *   log.debug("result=" + data[3]);
 *   log.debug("discrHash=" + data[4]);
 *   log.debug("hashMsg=" + data[5]);
 *   String checksum = com.sci.vname.secu.hmac.SciHmac.HMacEncript(data[0]+data[1]+data[3]);
 *   if(checksum.equals(data[5])) {
 *   	name = data[2];
 *   }
 * }}</pre>
 * </p>
 *
 * <p>디트라이브 클래스 코드 사용 예:
 * <pre>{@code
 * String org = request.getParameter("retInfo");
 * SciVname vname = SciVname.parse(org);
 * if(vname != null && vname.isValid()) {
 *   //
 * }}</pre>
 * </p>
 *
 * @author	Barney Kim
 * @version	$Revision: 1.2 $, $Date: 2009/08/19 20:36:53 $
 */
public class SciVname extends BaseModel {

	/** 시리얼 버전 UID */
	private static final long serialVersionUID = 429479309042299567L;

	/** 요청 번호 */
	private String reqNum;

	/** 가상 식별 번호 */
	private String vDiscrNo;

	/** 성명  */
	private String name;

	/** 식별검증 결과 */
	private String result;

	/** 중복가입확인 정보 */
	private String discrHash;

	/** 위/변조 검증값 */
	private String hashMsg;

	// 속성 ------------------------------------------------------------------

	/**
	 * 요청 번호 (최대 30 자)
	 * 실명확인 요청마다 유일하게 생성.
	 */
	public String getReqNum() {
		return reqNum;
	}

	/**
	 * 요청번호
	 */
	public void setReqNum(String reqNum) {
		this.reqNum = reqNum;
	}

	/**
	 * 가상 식별 번호 (13 자)
	 * 주민등록번호와 매칭되는 SCI에서 생성한 가상의 13자리 번호
	 * 성공: SCI+식별번호11자리
	 * 실패: SCI+99999999999
	 */
	public String getVDiscrNo() {
		return vDiscrNo;
	}

	/**
	 * 가상 식별 번호
	 */
	public void setVDiscrNo(String discrNo) {
		vDiscrNo = discrNo;
	}

	/**
	 * 성명 (최대 20자)
	 * 실패: null
	 */
	public String getName() {
		return name;
	}

	/**
	 * 성명
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * 식별검증 결과 (1자)
	 * 1: 정상
	 * 2: 오류.
	 *    입력된 주민번호의 성명이 실명확인 DB의 성명과 불일치,
	 *    주민번호가 숫자가 아닌경우, 주민번호가 13자리가 아닌경우
	 *    성명이 한글이 아닌경우, 성명이 2~10자 아닌 경우
	 * 3: 없음.
	 *    입력된 주민번호와 성명이 실명확인 DB에 업ㅎㅅ는 경우
	 * 4: 주민민번호 오류
	 *    입력된 주민번호의 조합이 오류인 경우: 생년이 70년이후인 경우만.
	 * 5: 시스템 오류
	 */
	public String getResult() {
		return result;
	}

	/**
	 * 식별검증 결과
	 */
	public void setResult(String result) {
		this.result = result;
	}

	/**
	 * 중복가입확인 정보 (최대 64자)
	 * 아이핀서비스를 이용하는 경우만 값이 유효
	 */
	public String getDiscrHash() {
		return discrHash;
	}

	/**
	 * 중복가입확인 정보
	 */
	public void setDiscrHash(String discrHash) {
		this.discrHash = discrHash;
	}

	/**
	 * 위/변조 검증값 (최대 40자)
	 */
	public String getHashMsg() {
		return hashMsg;
	}

	/**
	 * 위/변조 검증값
	 */
	public void setHashMsg(String hashMsg) {
		this.hashMsg = hashMsg;
	}

	// 메소드 ----------------------------------------------------------------

	/**
	 * 서울신용평가정보의 실명확인결과의 암호화된 문자열을 복호화하여
	 * 평문으로 반환한다.
	 *
	 * SciPacketConversion.RecvWritePacket은 file.encoding 시스템 프로퍼티가
	 * EUC_KR이 아닌 UTF-8일 경우 한글이 깨지기 때문에 이를 방지하기 위해
	 * RecvWritePacket 메소드를 일부 수정함.
	 *
	 * @param s 인코드된 문자열
	 * @return 디코드된 문자열
	 * @see com.sci.vname.secu.aes.SciPacketConversion#RecvWritePacket(String)
	 */
	public static String decode(String s) {
		if(s == null) {
			return null;
		}
		StringBuffer sb = new StringBuffer("");
		int buffer = 1000;
		try {
			byte dec[] = SciHttpSecuX.Decryption(s);
			byte bytes[] = new byte[buffer];
			int i=0, j=0;
			do {
				if(j >= dec.length) {
					break;
				}
				bytes[i] = 0;
				if(dec[j] == 0 || dec[j] == 10) {
					// ORG
					// sb.append((new String(bytes)).trim());
					// for UTF-8
					sb.append((new String(bytes, "EUC_KR")).trim());
					break;
				}
				if(dec[j] >= 1 && dec[j] <= 126) {
					bytes[i] = dec[j];
					i++;
				} else {
					bytes[i] = dec[j];
					bytes[i + 1] = dec[j + 1];
					i += 2;
					j++;
				}
				if(i >= 998) {
					// ORG
					// sb.append(new String(bytes));
					// for UTF-8
					sb.append(new String(bytes, "EUC_KR"));
					i = 0;
					bytes = new byte[buffer];
				}
				j++;
			} while(true);
		}
		catch(Exception ex) {
			ex.printStackTrace();
		}
		return sb.toString();
	}

	/**
	 * 서울신용평가정보의 실명확인결과의 암호화된 문자열을 복호화하고 이를 이용하여
	 * SciVname 객체를 생성하여 반환한다.
	 *
	 * @param s 인코드된 문자열
	 * @return SciVname 객체
	 */
	public static SciVname parse(String s) {
		if(r == null) {
			return null;
		}
		String r = decode(s);
		String data[] = r.split("/");
		if(data.length != 7) {
			return null;
		}
		SciVname vname = new SciVname();
		vname.setReqNum   (data[0]);
		vname.setVDiscrNo (data[1]);
		vname.setName     (data[2]);
		vname.setResult   (data[3]);
		vname.setDiscrHash(data[4]);
		vname.setHashMsg  (data[5]);
		return vname;
	}

	/**
	 * 가상 식별 실명 확인이 성공하고 올바른지 확인한다.
	 *
	 * @return 위/변조 검증값이 올바르고 결과가 성공이면 true
	 */
	public boolean isValid() {
		if (this.reqNum == null   || this.reqNum.length() == 0 ||
			this.vDiscrNo == null || this.vDiscrNo.length() != 13 ||
			this.result == null   || this.result.length() != 1 ||
			this.hashMsg == null  || this.hashMsg.length() == 0) {
			return false;
		}
		StringBuffer sb = new StringBuffer();
		sb.append(this.reqNum).append(this.vDiscrNo).append(this.result);
		String checksum = SciHmac.HMacEncript(sb.toString());
		return (checksum.equals(this.hashMsg) && this.result.equals("1"));
	}

	/**
	 * 디코드된 문자열로 가상 식별 실명 확인이 성공하고 올바른지 확인한다.
	 *
	 * @param s 디코드된 문자열
	 * @return 위/변조 검증값이 올바르고 결과가 성공이면 true
	 */
	public static boolean isValid(String s) {
		String[] data = s.split("/");
		if(data.length != 7) {
			return false;
		}
		if (data[0] == null || data[0].length() == 0 ||
			data[1] == null || data[1].length() != 13 ||
			data[3] == null || data[3].length() != 1 ||
			data[5] == null || data[5].length() == 0) {
			return false;
		}
		StringBuffer sb = new StringBuffer();
		sb.append(data[0]).append(data[1]).append(data[3]);
		String checksum = SciHmac.HMacEncript(sb.toString());
		return (checksum.equals(data[5]) && data[3].equals("1"));
	}
}
// EOF

이것을 Spring Framework에서 사용하면 다음과 같다.

import com.dtribe.scivname.SciVname;

public ModelAndView output(HttpServletRequest request,
		HttpServletResponse response)
	throws Exception
{
	String org = ServletRequestUtils.getRequiredStringParameter(request, "retInfo");
	SciVname vname = SciVname.parse(org);
	log.debug("org=" + org);
	log.debug("vname=" + vname);

	ModelAndView mav = new ModelAndView("vname/output");
	if(vname != null && vname.isValid()) {
		mav.addObject("vname", vname);
	}
	return mav;
}

JSP 페이지 (vname/output.jsp)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
%><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"
%><c:if test="${vname != null}">
<script type="text/javascript">
top.document.getElementById("name").value="${vname.name}";
</script>
</c:if>