JAVA-WEB December 29, 2020

JSP-15 네이버 캡차 (capcha)

Words count 24k Reading time 22 mins.

캡차(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

  1. Controller에게 결과와 응답할 VIEW를 반환한다.
    1. 반환값은 응답VIEW이다.
    2. 결과값은 request에 저장한다.
  2. 매개변수로 HttpServletRequest 클래스 타입의 request가 필요하다.
  3. 매개변수로 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의 역할

  1. 요청을 확인하고, 요청을 처리할 Model(Command)을 호출한다.

  2. 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메소드 순서 요약

  1. 네이버 캡차 API에서 clientId 와 password를 발급 받는다.
  2. 캡차 키 요청(get() 메소드 활용)을 받고 Json parse를위한 JSONSimple 라이브러리 사용
  3. 응답받은 키로 이미지를 요청(get2() 메소드 활용 과 동시에 get2()내부에서 getImage() 메소드도 실행됨.)
  4. getImage()메소드로 이미지 생성
  5. 파일명과 파일위치를 반환받아 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>

결과 페이지

그 후에 맨 위와 같은 화면이 나오고

캡챠를 제대로 입력하면 뜨는 화면입니다.

0%