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>

어플리케이션 서버를 사용한 인증

Tomcat과 같은 어플리케이션 서버를 사용한 인증 방법을 간단히 정리하면 다음과 같다.

1. 어플리케이션 서버

Tomcat에서는 server.xml 파일에 사용자 지정 Realm 설정을 한다.

예제에서는 member 테이블의 email 컬럼을 사용자명으로, MD5로 암호화된 passwd 컬럼을 암호로 사용하고,

역할은 role 컬럼을 사용하도록 했다.

<Realm className="org.apache.catalina.realm.JDBCRealm"
 driverName="com.mysql.jdbc.Driver"
 connectionURL="jdbc:mysql://localhost:3306/sample?useUnicode=true"
 connectionName="user1" connectionPassword="password"
 userTable="member" userNameCol="email" userCredCol="passwd" digest="MD5"
 userRoleTable="member" roleNameCol="role"
/>

2. 웹 어플리케이션

해당 웹 어플리케이션의 web.xml 파일을 설정한다.
여기에서는 예로 /protected 란 디렉토리의 모든 파일을 요청할 때 인증이 필요하며,
인증된 계정은 Admin이나 SuperAdmin의 Role을 가지고 있어야 한다고 설정했다.

<security-constraint>
 <display-name>보안 설정</display-name>
 <web-resource-collection>
 <web-resource-name>SiteManager</web-resource-name>
  <url-pattern>/protected/*</url-pattern>
  <http-method>GET</http-method>
  <http-method>POST</http-method>
 </web-resource-collection>
 <auth-constraint>
  <description>사이트 관리자</description>
  <role-name>Admin</role-name>
  <role-name>SuperAdmin</role-name>
 </auth-constraint>
</security-constraint>

<login-config>
 <auth-method>FORM</auth-method>
 <realm-name>Site Managers</realm-name>
 <form-login-config>
  <form-login-page>/login.jsp</form-login-page>
  <form-error-page>/login_error.jsp</form-error-page>
 </form-login-config>
</login-config>

<security-role>
 <role-name>Admin</role-name>
</security-role>
<security-role>
 <role-name>SuperAdmin</role-name>
</security-role>

로그인 폼인 login.jsp 파일은 다음과 같이 간단하며 로그인이 성공하면 요청한 페이지로 자동으로 이동된다.

<form method="POST" action="j_security_check">
  <input type="text" name="j_username">
  <input type="password" name="j_password">
  <input type="submit" value="로그인">
</form>

로그인이 되면 다음과 같이 로그인 된 회원 정보를 확인할 수 있다.

String remoteUser = request.getRemoteUser();
java.security.Principal principal = request.getPrincipal();
String username = principal.getName();
boolean isAdmin = request.isUserInRole("Admin");
boolean isSuperAdmin = request.isUserInRole("SuperAdmin");

3. 참고

Windows용 Apache 2.0.4x 에서 SSL 지원하도록 컴파일

현재 Apache에서는 Windows용 Apache 2.0.59 버전이하에서는 SSL을 지원하는 바이너리 파일을 제공하고 있지 않다.

지인의 다급한 요청으로 Windows용 Apache 2.0.4x(2.0.40, 2.0.43)에서 SSL이 지원하도록 컴파일을 해야했다.

필요한 소프트웨어

※ OpenSSL 0.9.8k 과 zlib 1.2.3으로 컴파일을 하려고 했으나 Apache 소스와 호환이 되지 않는 문제가 있었다.

컴파일 준비

OpenSSL 준비 및 컴파일

다운로드 받은 OpenSSL 소스를 Apache 소스의 srclib 디렉토리안에 압축 해제하고 openssl 디렉토리명으로 변경하고

Perl을 사용하여 Makefile을 생성하여 Visual C++에 포함된 nmake를 사용하여 컴파일 한다.

C:\httpd-2.0.40\srclib> ren openssl-0.9.7m openssl
C:\httpd-2.0.40\srclib> cd openssl
perl Configure VC-WIN32 --prefix=c:/USR/openssl
perl util\mkfiles.pl >MINFO
perl util\mk1mf.pl dll no-asm no-mdc2 no-rc5 no-idea VC-WIN32 > Makefile.win
perl util\mk1mf.pl dll debug no-asm no-mdc2 no-rc5 no-idea VC-WIN32 > Makefile.dbg
perl util\mkdef.pl 32 libeay no-asm no-mdc2 no-rc5 no-idea > ms\libeay32.def
perl util\mkdef.pl 32 ssleay no-asm no-mdc2 no-rc5 no-idea > ms\ssleay32.def
nmake -f Makefile.win
nmake -f Makefile.dbg

zlib 준비

다운받은 zlib 소스를 역시 Apache 소스의 srclib 디렉토리 안에 압축 해제하고 zlib 디렉토리명으로 변경한다.

zlib는 사전에 컴파일 할 필요가 없다.

C:\httpd-2.0.40\srclib> ren zlib-1.1.4 zlib

Makefile 수정

Visual C++의 IDE에서 컴파일을 해도 되지만 편의를 위해서 명령 프롬프트에서 컴파일할 수 있도록 Makefile을 수정한다.

이렇게 수정하는 이유는 Apache 소스에 포함된 Makefile이 OpenSSL 0.9.7m 및 zlib 1.1.4 소스의 정의 값이 일치하지 않기 때문이다.

  1. Visual C++ 실행 > Project > Setting 메뉴 선택
  2. 왼쪽의 빌드 대상에서 abs 를 선택하고, Settings For를 All Configurations 로 선택
  3. 오른쪽의 C/C++ 탭을 선택하고 Preprocessor definitions에 ,OPENSSL_NO_RC5,OPENSSL_NO_IDEA,OPENSSL_NO_MDC2 추가 (처음에 콤마가 있어야 하며 이미 동일한 정의 값이 있는 경우는 필요 없음)
  4. 왼쪽의 빌드 대상에서 mod_deflate 를 선택하고, Settings For를 All Configurations 로 선택
  5. 오른쪽의 C/C++ 탭을 선택하고 Preprocessor definitions에 ,_WIN32 추가
  6. 왼쪽의 빌드 대상에서 mod_ssl 을 선택하고, Settings For를 All Configurations 로 선택
  7. 오른쪽의 C/C++ 탭을 선택하고 Preprocessor definitions에 ,OPENSSL_NO_RC5,OPENSSL_NO_IDEA,OPENSSL_NO_MDC2 추가 (이미 있는 경우는 필요 없음)
  8. OK 클릭
Visual C++ 6.0 Project Setting

Visual C++ 6.0 Project Setting

  1. Project > Export Makefile 메뉴 선택
  2. 기본적으로 모두 선택되어 있으므로 Select All 클릭 후 선택항목를 클릭하여 모두 선택 해제
  3. abs, mod_deflate와 mod_ssl 만 체크하고 OK 클릭
Visual C++ 6.0 Project Export Make

Visual C++ 6.0 Project Export Make

컴파일

이제 모든 컴파일 준비가 끝났다.

컴파일 대상은 다음과 같다.

  • _apacher: 릴리즈 모드로 컴파일
  • _apached: 디버그 모드로 컴파일
  • installr: 릴리즈 모드로 컴파일 후 설치
  • installd: 디버그 모드로 컴파일 후 설치
  • clean: 생성된 파일 삭제
  • _cleanr: 릴리즈 모드로 컴파일 할 때 생성한 파일 삭제
  • _cleand: 디버그 모드로 컴파일 할 때 생성한 파일 삭제

선택 사항은 다음과 같다.

  • INSTDIR: 설치 디렉토리 (기본 값: Apache2)
  • PORT: 포트 (80)
  • SERVERNAME: 서버명 (localhost)

예로는 포트 번호를 80으로 설정하고 설치 디렉토리를 C:USRApache-2.0.40으로 지정하고 릴리즈 모드로 컴파일 하고 설치하도록 했다.

C:\httpd-2.0.40> nmake /f Makefile.win PORT=80 INSTDIR="C:\USR\Apache-2.0.40" installr

참고