캡차(Capcha) 란 무엇일까
로그인 시 자동 입력 방지를 위해 사람의 눈으로 식별가능한 문자가 포함된 이미지를 전송하고 입력값을 검증하는 REST API 입니다.
캡차 만들기 시작
만들고 싶은 캡챠 화면
위와 같은 캡챠 화면을 목표로 만들어 봅시다!
필요한 라이브러리
- json-simple-1.1.1
index.jsp
인덱스 페이지를 만들어 처음 화면을 구성한다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<a href="/18_CAPCHA/getImageCaptcha.do">로그인하러 가기</a>
</body>
</html>
common
PathNRedirect.java
pathNRedirect
class 를 만들어 경로와 redirect 여부를 반환해준다.
package common;
public class PathNRedirect {
private String path; //경로
private boolean isRedirect; // 리다이렉트 여부.
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isRedirect() {
return isRedirect;
}
public void setRedirect(boolean isRedirect) { // Redirect 여부를 묻는 메소드 true: forward | false: redirect
this.isRedirect = isRedirect;
}
}
Controller.java
컨트롤러를 이용해 요청을 처리한다.
package controller;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import command.Command;
import command.GetImageCaptchaCommand;
import command.InputKeyCheckCommand;
import common.PathNRedirect;
@WebServlet("*.do") // .do suffix가 .do인 모든 요청을 처리하는 Controller
public class Controller extends HttpServlet {
private static final long serialVersionUID = 1L;
public Controller() {
super();
// TODO Auto-generated constructor stub
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html; charset=UTF-8");
String requestUri = request.getRequestURI(); // requestUri 요청 주소 전체를 의미 // /18_CAPCHA/*.do
String contextPath = request.getContextPath(); // ContextPath: 프로젝트 이름 18_CAPCHA// /18_CAPCHA
String cmd = requestUri.substring(contextPath.length()); // /*.do
PathNRedirect pathNRedirect = null;
Command command = null;
Command
Command
인터페이스 생성 (MVC의 Model 공통 처리)
Model
- Controller에게 결과와 응답할 VIEW를 반환한다.
- 반환값은 응답VIEW이다.
- 결과값은 request에 저장한다.
- 매개변수로 HttpServletRequest 클래스 타입의 request가 필요하다.
- 매개변수로 HttpServletResponse 클래스 타입의 response도 필요할 수 있다.
Command
package command;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import common.PathNRedirect;
public interface Command {
public PathNRedirect execute(HttpServletRequest request, HttpServletResponse response);
}
Controller
Controller의 역할
요청을 확인하고, 요청을 처리할 Model(Command)을 호출한다.
Model(Command)이 반환한 결과와 응답VIEW를 이용해서 페이지이동(응답)을 한다.
Today today = new Today(); String path = today.execute(request, response); path로 이동 Today Command가 request에 result를 저장해 주었으므로 이를 전달하기 위해서 forward한다.
Controller
의 doGet메소드에 추가 작성
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html; charset=UTF-8");
String requestUri = request.getRequestURI(); // requestUri 요청 주소 전체를 의미 // /18_CAPCHA/*.do
String contextPath = request.getContextPath(); // ContextPath: 프로젝트 이름 18_CAPCHA// /18_CAPCHA
String cmd = requestUri.substring(contextPath.length()); // /*.do
PathNRedirect pathNRedirect = null;
Command command = null;
switch(cmd){
case "/getImageCaptcha.do":
command = new GetImageCaptchaCommand();
pathNRedirect = command.execute(request, response);
break;
}
indexPage.java
의 로그인하러 가기
의 /getImageCaptcha.do 요청을 Controller에서 처리해 준다.
로그인을 하러 가면 캡차 이미지가 생성됨
GetImageCaptchaCommand 생성
캡차 이미지 생성을 위해 만든다.
캡차 키 발급 요청 과 이미지를 다운받는다.
package command;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import common.PathNRedirect;
public class GetImageCaptchaCommand implements Command {
@Override
public PathNRedirect execute(HttpServletRequest request, HttpServletResponse response) {
// 네이버 캡차 API
// 1) 캡차 키 발급 요청하기
// 2) 캡차 이미지 요청하기
String clientId = "u7aGTrFmp005OR4CJWy4"; //애플리케이션 클라이언트 아이디값";
String clientSecret = "V00UJonEUP"; //애플리케이션 클라이언트 시크릿값";
// 1) 캡차 키 발급 요청하기
String code = "0"; // 키 발급시 0, 캡차 이미지 비교시 1로 세팅
String apiURL = "https://openapi.naver.com/v1/captcha/nkey?code=" + code;
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("X-Naver-Client-Id", clientId);
requestHeaders.put("X-Naver-Client-Secret", clientSecret);
String responseBody = get(apiURL, requestHeaders);
// System.out.println(responseBody); // {"key":"bgbl5MwSZRnQOllo"}
// responseBody는 {"key":"bgbl5MwSZRnQOllo"}와 같은 형식의 JSON 데이터
// json-simple-1.1.1.jar를 이용해서 responseBody에서 "bgbl5MwSZRnQOllo"를 뺀다.
JSONParser parser = new JSONParser();
JSONObject obj = null;
try {
obj = (JSONObject)parser.parse(responseBody);
} catch (ParseException e) {
e.printStackTrace();
}
// 입력값 비교(InputKeyCheckCommand)에서 캡차 키를 필요로 하므로,
// session에 올려 둔다.
// session은 request에서 알아낸다.
HttpSession session = request.getSession();
session.setAttribute("key", (String)obj.get("key")); // #key session에 보관
// 2) 캡차 이미지 요청하기
String key = (String)obj.get("key"); // https://openapi.naver.com/v1/captcha/nkey 호출로 받은 키값
// 이미지 수신 실패용(아무 키나 넘김) String key = "aldfakjlkajgj;fljg;sl";
String apiURL2 = "https://openapi.naver.com/v1/captcha/ncaptcha.bin?key=" + key;
// requestHeaders는 1) 캡차 키 발급 요청에서 이미 생성했으므로 또 생성할 필요가 없다.
/*
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("X-Naver-Client-Id", clientId);
requestHeaders.put("X-Naver-Client-Secret", clientSecret);
*/
// responseBody2
// 1) 성공: 이미지 캡차가 생성되었습니다.
// 2) 실패: {"result":false,"errorMessage":"Invalid key.","errorCode":"CT001"}
// String responseBody2 = get2(apiURL2, requestHeaders);
// 성공했을 때는 캡차 이미지 파일이 생성되므로 생성된 파일명을 알아야 한다.
// responseBody2 -> filename
String filename = get2(request, apiURL2, requestHeaders);
// System.out.println(filename);
PathNRedirect pathNRedirect = new PathNRedirect();
pathNRedirect.setPath("login/loginPage.jsp");
pathNRedirect.setRedirect(false); // request에 directory, filename 저장되어 있으므로 forward
return pathNRedirect;
}
// 1) 캡차 키 발급 요청용 get() 메소드
private static String get(String apiUrl, Map<String, String> requestHeaders){
HttpURLConnection con = connect(apiUrl);
try {
con.setRequestMethod("GET");
for(Map.Entry<String, String> header :requestHeaders.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
return readBody(con.getInputStream());
} else { // 에러 발생
return readBody(con.getErrorStream());
}
} catch (IOException e) {
throw new RuntimeException("API 요청과 응답 실패", e);
} finally {
con.disconnect();
}
}
// 1) 캡차 키 발급 요청용 connect() 메소드
// 2) 캡차 이미지 요청용 connect() 메소드
private static HttpURLConnection connect(String apiUrl){
try {
URL url = new URL(apiUrl);
return (HttpURLConnection)url.openConnection();
} catch (MalformedURLException e) {
throw new RuntimeException("API URL이 잘못되었습니다. : " + apiUrl, e);
} catch (IOException e) {
throw new RuntimeException("연결이 실패했습니다. : " + apiUrl, e);
}
}
// 1) 캡차 키 발급 요청용 readBody() 메소드
private static String readBody(InputStream body){
InputStreamReader streamReader = new InputStreamReader(body);
try (BufferedReader lineReader = new BufferedReader(streamReader)) {
StringBuilder responseBody = new StringBuilder();
String line;
while ((line = lineReader.readLine()) != null) {
responseBody.append(line);
}
return responseBody.toString();
} catch (IOException e) {
throw new RuntimeException("API 응답을 읽는데 실패했습니다.", e);
}
}
// 2) 캡차 이미지 요청용 get2() 메소드
private static String get2(HttpServletRequest request, String apiUrl, Map<String, String> requestHeaders){
HttpURLConnection con = connect(apiUrl);
try {
con.setRequestMethod("GET");
for(Map.Entry<String, String> header :requestHeaders.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
// getImage()에 request를 전달하려면 get2() 메소드가 HttpServletRequest request를 받아와야 한다.
// 기존: get2(String apiUrl, Map<String, String> requestHeaders) {
// 수정: get2(HttpServletRequest request, String apiUrl, Map<String, String> requestHeaders) {
return getImage(request, con.getInputStream());
} else { // 에러 발생
return error(con.getErrorStream());
}
} catch (IOException e) {
throw new RuntimeException("API 요청과 응답 실패", e);
} finally {
con.disconnect();
}
}
// 2) 캡차 이미지 요청용 getImage() 메소드
private static String getImage(HttpServletRequest request, InputStream is){
int read;
byte[] bytes = new byte[1024];
// 랜덤한 이름으로 파일 생성(X)
// 현재 시간: timestamp으로 파일 생성(O)
String filename = Long.valueOf(new Date().getTime()).toString();
// 캡차 이미지가 저장될 storage 디렉토리의 경로를 알아낸다.
String directory = "storage";
// HttpServletRequest request가 있어야 realPath를 구할 수 있다.
// 따라서 execute() 메소드에게서 HttpServletRequest request를 받아 온다.
// 기존: getImage(InputStream is) { ... }
// 수정: getImage(HttpServletRequest request, InputStream is)
String realPath = request.getServletContext().getRealPath(directory);
// storage 디렉토리가 안 생기면 강제로 만들어 주는 코드
File dir = new File(realPath); // File dir에는 storage 디렉토리 정보가 저장된다.
if ( !dir.exists() ) { // dir(storage 디렉토리)이 없으면
dir.mkdirs(); // 해당 디렉토리(storage 디렉토리)를 생성하라.
}
// storage 디렉토리 경로를 포함하도록 File f를 수정한다.
// 기존: File f = new File(filename + ".jpg");
// 수정: File f = new File(realPath, filename + ".jpg");
File f = new File(realPath, filename + ".jpg");
try(OutputStream outputStream = new FileOutputStream(f)){
f.createNewFile();
while ((read = is.read(bytes)) != -1) {
outputStream.write(bytes, 0, read);
}
// directory(상대경로)와 filename을 JSP(로그인화면)에서 확인할 수 있도록
// request에 저장해 둔다.
// GetImageCaptchaCommand의 execute() 메소드는 PathNRedirect를 반환하는데,
// 이 때 반환방법은 forward이다. (request의 데이터 유지를 위해서)
request.setAttribute("filename", filename + ".jpg");
request.setAttribute("directory", directory);
// return "이미지 캡차가 생성되었습니다.";
return filename;
} catch (IOException e) {
throw new RuntimeException("이미지 캡차 파일 생성에 실패 했습니다.",e);
}
}
// 2) 캡차 이미지 요청용 error() 메소드
private static String error(InputStream body) {
InputStreamReader streamReader = new InputStreamReader(body);
try (BufferedReader lineReader = new BufferedReader(streamReader)) {
StringBuilder responseBody = new StringBuilder();
String line;
while ((line = lineReader.readLine()) != null) {
responseBody.append(line);
}
return responseBody.toString();
} catch (IOException e) {
throw new RuntimeException("API 응답을 읽는데 실패했습니다.", e);
}
}
}
GetImageCaptchaCommand
메소드 순서 요약
- 네이버 캡차 API에서 clientId 와 password를 발급 받는다.
- 캡차 키 요청(get() 메소드 활용)을 받고 Json parse를위한 JSONSimple 라이브러리 사용
- 응답받은 키로 이미지를 요청(get2() 메소드 활용 과 동시에 get2()내부에서 getImage() 메소드도 실행됨.)
- getImage()메소드로 이미지 생성
- 파일명과 파일위치를 반환받아 request 영역에 저장 (String filename, directory)
indexPage -> loginPage
indexPage.jsp
에서 loginPage로 이동해서 생성된 캡차이미지 활용
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h3>로그인</h3>
<form action="/18_CAPTCHA/login.do" method="post">
<input type="text" name="id" placeholder="아이디" /><br/>
<input type="password" name="pw" placeholder="비밀번호" /><br/><br/>
아래 이미지를 보이는 대로 입력하세요.<br/>
<img alt="캡차이미지" src="${directory}/${filename}" style="width: 200px;">
<input type="button" value="새로고침" onclick="location.href='/18_CAPTCHA/getImageCaptcha.do'" /><br/>
<input type="text" name="input_key" placeholder="자동입력 방지문자" /><br/><br/>
<button>로그인</button>
</form>
</body>
</html>
login.do Controller 생성
위 코드 (
index의-로그인하러-가기
)에 추가
switch(cmd){
case "/getImageCaptcha.do":
command = new GetImageCaptchaCommand();
pathNRedirect = command.execute(request, response);
break;
case "/loginPage.do";
command = new InputKeyCheckCommand();
pathNRedirect = command.execute(request, response);
break;
}
InputKeyCheckCommand
login.do
의 모델 captcha 이미지와 입력값을 비교한다.
package command;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import common.PathNRedirect;
public class InputKeyCheckCommand implements Command {
@Override
public PathNRedirect execute(HttpServletRequest request, HttpServletResponse response) {
String clientId = "u7aGTrFmp005OR4CJWy4"; //애플리케이션 클라이언트 아이디값";
String clientSecret = "V00UJonEUP"; //애플리케이션 클라이언트 시크릿값";
String code = "1"; // 키 발급시 0, 캡차 이미지 비교시 1로 세팅
// session에서 key 가져오면 발급 받은 캡차 발급 키를 알 수 있다.
HttpSession session = request.getSession();
String key = (String)session.getAttribute("key"); // 캡차 키 발급시 받은 키값
String value = request.getParameter("input_key"); // 사용자가 입력한 캡차 이미지 글자값
String apiURL = "https://openapi.naver.com/v1/captcha/nkey?code=" + code + "&key=" + key + "&value=" + value;
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("X-Naver-Client-Id", clientId);
requestHeaders.put("X-Naver-Client-Secret", clientSecret);
String responseBody = get(apiURL, requestHeaders);
System.out.println(responseBody);
// responseBody
// 성공: {"result":true,"responseTime":21.39}
// 실패: {"result":false,"responseTime":5.76}
// result를 responseBody에서 꺼낸다.
JSONParser parser = new JSONParser();
JSONObject obj = null;
try {
obj = (JSONObject)parser.parse(responseBody);
} catch (ParseException e) {
e.printStackTrace();
}
boolean result = (boolean)obj.get("result");
// loginResult.jsp로 result값(true, false)을 보내기 위해서
// request에 result를 저장해 둔다. 그리고 forward 한다.
request.setAttribute("result", result);
PathNRedirect pathNRedirect = new PathNRedirect();
pathNRedirect.setPath("login/loginResult.jsp");
pathNRedirect.setRedirect(false); // forward
return pathNRedirect;
}
private static String get(String apiUrl, Map<String, String> requestHeaders){
HttpURLConnection con = connect(apiUrl);
try {
con.setRequestMethod("GET");
for(Map.Entry<String, String> header :requestHeaders.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
return readBody(con.getInputStream());
} else { // 에러 발생
return readBody(con.getErrorStream());
}
} catch (IOException e) {
throw new RuntimeException("API 요청과 응답 실패", e);
} finally {
con.disconnect();
}
}
private static HttpURLConnection connect(String apiUrl){
try {
URL url = new URL(apiUrl);
return (HttpURLConnection)url.openConnection();
} catch (MalformedURLException e) {
throw new RuntimeException("API URL이 잘못되었습니다. : " + apiUrl, e);
} catch (IOException e) {
throw new RuntimeException("연결이 실패했습니다. : " + apiUrl, e);
}
}
private static String readBody(InputStream body){
InputStreamReader streamReader = new InputStreamReader(body);
try (BufferedReader lineReader = new BufferedReader(streamReader)) {
StringBuilder responseBody = new StringBuilder();
String line;
while ((line = lineReader.readLine()) != null) {
responseBody.append(line);
}
return responseBody.toString();
} catch (IOException e) {
throw new RuntimeException("API 응답을 읽는데 실패했습니다.", e);
}
}
}
request에 비교한 입력값에대한 결과를 boolean
타입으로 저장시켜 준다.
그리고 Path와 Redirect 유무를 가진 PathNRedirect 를 반환해준다.
결과 출력 Page
loginPage.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<script>
if ( ${result} ) {
alert('성공입니다.');
location.href = '/18_CAPTCHA/index.do';
} else {
alert('실패입니다.');
// history.back(); 새로운 이미지를 받아서 다시 시도할 수 있도록 새로운 캡차 이미지를 받아야 한다.
location.href = '/18_CAPTCHA/getImageCaptcha.do';
}
</script>
결과 페이지
그 후에 맨 위와 같은 화면이 나오고
캡챠를 제대로 입력하면 뜨는 화면입니다.