팀프로젝트/Spring

스프링 팀플)20240228_약관 동의 페이지, 암호화/복호화, ajax 조정

일일일코_장민기 2024. 3. 28. 11:02
728x90

수정한 것이 겁나 많다...정리하는 것도 일

 

1. 가입 동의

 

<%@page import="java.io.Console"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>

<!DOCTYPE html>
<html>

<!-- 회원가입을 위한 약관에 동의하는 페이지.jsp -->

<head>
    <meta charset="UTF-8">
    <title>약관 동의 페이지</title>
	<link rel="stylesheet" type="text/css" href="<c:url value='/css/member/register_term.css'/>">
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
</head>
<body>

    <div class="container">
    
        <h1>약관 동의 페이지</h1>

        <form id="agreementForm" action="<c:url value='/CheckExistUser'/>" method="post">
		
			<input type="hidden" name="userName" value="${userName}">
			<input type="hidden" name="ssn1" value="${ssn1}">
			<input type="hidden" name="ssn2" value="${ssn2}">

            <div><Span>이용약관 동의(필수)</Span>
                <textarea readonly="readonly">
	                <jsp:include page="../Terms/Agreement.jsp" flush="true"></jsp:include>
                </textarea>
                <label><input type="checkbox" class="terms" name="checked_Agreement">이용 약관에 동의합니다.</label>
            </div>

            <div><Span>개인정보 처리방침(필수)</Span>
                <textarea readonly="readonly">
	                <jsp:include page="../Terms/Info.jsp" flush="true"></jsp:include>
                </textarea>
                <label><input type="checkbox" class="terms" name="checked_Info">개인정보 처리방침에 동의합니다.</label>
            </div>

            <div><Span>회원 탈퇴 및 서비스 이용 중지 규정(필수)</Span>
                <textarea readonly="readonly">
   	                <jsp:include page="../Terms/Withdraw.jsp" flush="true"></jsp:include>
                </textarea>
                <label><input type="checkbox" class="terms" name="checked_Withdraw">회원 탈퇴 및 서비스 이용 중지에 동의합니다.</label>
            </div>
            <br>

            <div>
                <label><input type="checkbox" id="allCheckbox" onclick="clickAllChk(this.checked)">모두 동의합니다.</label>
            </div>
            <button type="button" onclick="chkAgreement()">다음 페이지로 이동</button>
        </form>
    </div>

    <script type="text/javascript">
        $(function() {
        	//모두 동의를 클릭하면 clickAllChk함수 발동(다른 체크박스가 모두 체크)
            $("#allCheckbox").click(function() {
                clickAllChk(this.checked);
            });

         	// 개별 약관에 대한 체크박스 클릭 시 모두 동의 체크박스 상태 갱신
            $(".terms").click(function() {
                $("#allCheckbox").prop("checked", $(".terms:checked").length === $(".terms").length);
            });
        });

    	//모두 동의를 클릭하면 clickAllChk함수 발동(다른 체크박스가 모두 체크)
        function clickAllChk(tc) {
            $(".terms").prop("checked", tc);
        }

    	//모두 동의를 제외한 모든 체크박스가 체크되지 않으면 다음 페이지로 이동 불가
        function chkAgreement() {
            if ($(".terms:not(:checked)").length === 0) {
                alert("약관에 모두 동의하셨습니다. 다음 페이지로 이동합니다.");
                $("#agreementForm").submit();
            } else {
                alert("모든 약관에 동의해야 다음 페이지로 이동할 수 있습니다.");
            }
        }
    </script>

</body>
</html>

 

jsp 안에 넣기에는 양이 많아서 전부 include 처리했다.

 

textarea {
	width: 100%;
    height: 10em;
    border: none;
    resize: none;
	margin-bottom: 10px;
}

 

css에 추가

 

 

2. 암호화

단방향과 양방향 암호화 중에서 고민했는데, 관리 감독의 용이성을 위해 양방향 암호화를 선택했다.
스프링부트 작업하면서 단방향도 있으면 좋겠다는 생각을 하긴 했지만,
양방향을 쓰면서 단방향화시키는 방법도 있으니 양방향 위주로 간 건 잘한 작업인 듯하다.

이거 만드는데 생각보다 오래걸렸는데 몇일 안되서 스프링부트로 변경되었다.
=> 당연히 새로운 암호화를 할 수 밖에 없었기에 진짜 3일 천하였던 비운의 작업
적용했던 방식은 사용할 수 있어서 다행이었다.
pom.xml
<!-- 암호화 복호화 관련 디펜던시 시작 -->
		<!--스프링시큐리티 web 라이브러리 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
			<version>5.1.2.RELEASE</version>
		</dependency>
		<!--스프링시큐리티 core 라이브러리 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-core</artifactId>
			<version>5.1.2.RELEASE</version>
		</dependency>
		<!--스프링시큐리티 config 라이브러리 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
			<version>5.1.2.RELEASE</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
		<dependency>
			<groupId>commons-codec</groupId>
			<artifactId>commons-codec</artifactId>
			<version>1.16.1</version>
		</dependency>
		<!-- 암호화 복호화 관련 디펜던시 끝 -->

라이브러리 추가

servlet-context.xml
<!-- 단방향 암호화 -->
	<beans:bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
	 
	<!-- 양방향 암호화 -->
	<beans:bean id="AES256Util" class="com.controller.member.AES256Util">
		<beans:constructor-arg>
			<beans:value>1111111111111111</beans:value> <!-- 16자리로 제한 -->
		</beans:constructor-arg>
	</beans:bean>
AES256Util
package com.controller.member;

import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class AES256Util {
	
	private String iv;
	private Key keySpec;

	/**
	 * 16자리의 키값을 입력하여 객체를 생성
	 * @param key 암/복호화를 위한 키값
	 * @throws UnsupportedEncodingException 키값의 길이가 16이하일 경우 발생
	 */
	
	public AES256Util(String key) throws UnsupportedEncodingException {
		this.iv = key.substring(0, 16);
		byte[] keyBytes = new byte[16];
		byte[] b = key.getBytes("UTF-8");	//throws 필요
		
		int len = b.length;
		if (len > keyBytes.length) {
			len = keyBytes.length;
		}

		System.arraycopy(b, 0, keyBytes, 0, len);
		SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); // 공통 키 생성
		this.keySpec = keySpec;
	}
	
	/**
	 * AES256 으로 암호화
	 * @param str 암호화할 문자열
	 * @throws NoSuchAlgorithmException
	 * @throws GeneralSecurityException
	 * @throws UnsupportedEncodingException
	 */
	public String encrypt(String str) throws NoSuchAlgorithmException, GeneralSecurityException, UnsupportedEncodingException {
		
		Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 암호화 패딩 기법 설정	//throws 필요
		c.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes()));
		
		byte[] encrypted = c.doFinal(str.getBytes("UTF-8"));
		String enStr = new String(Base64.encodeBase64(encrypted));
		return enStr;

	}

	/**
	 * AES256으로 암호화된 txt를 복호화
	 * @param str 복호화할 문자열
	 * @throws NoSuchAlgorithmException
	 * @throws GeneralSecurityException
	 * @throws UnsupportedEncodingException
	 */
	public String decrypt(String str) throws NoSuchAlgorithmException, GeneralSecurityException, UnsupportedEncodingException {
		Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
		c.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes()));
		
		byte[] byteStr = Base64.decodeBase64(str.getBytes());
		return new String(c.doFinal(byteStr), "UTF-8");
	}
}
SecurityController
package com.controller.member;

import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecurityController {

	@Autowired
	private BCryptPasswordEncoder encoder; 
	
	@Autowired
	private AES256Util aesutil;

//	단방향 암호화
//	@RequestMapping("/encodepassword")
//	public String bcript() {
//		String str = "password";							//DB에 있는 비밀번호
//		String encodingStr = encoder.encode(str);			//암호화 처리된 문자열로 리턴(로그인 할 때 비밀번호)
//		Boolean result = encoder.matches(str, encodingStr);	//비밀번호 비교
//		return "원래 비밀번호: " + str + "<br>--> 이런 식으로 바뀜: " + encodingStr + "<br>" + "str = encodingStr(2개가 같은 지 비교) --> " + result;
//	}
	
	//양방향 암호화
	@RequestMapping(value = "/EncodePW", method = RequestMethod.POST)
	public String EncodePW(String userPw) throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {
		String encodingStr = aesutil.encrypt(userPw); 		// 암호화
		System.out.println(" 비밀번호: " + userPw + " 인코딩: " + encodingStr);
		return encodingStr;
	}
	
	@RequestMapping(value = "/DecodePW", method = RequestMethod.POST)
	public String DecodePW(String encodingStr) throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {
		String decodingStr = aesutil.decrypt(encodingStr); // 복호화
		System.out.println(" 인코딩: " + encodingStr + " 디코딩: " + decodingStr);
		return decodingStr;
	}
	
	
}

 

 

이제 암호화 시스템을 구축했으니 적용할 차례다.
나는 비밀번호에 적용했다.

 

package com.controller.member;

import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.dto.MemberDTO;
import com.service.member.LoginService;

@Controller
public class LoginController {

	@Autowired
	LoginService serv;
	
	@Autowired
	SecurityController sc;
	
	//로그인
	@RequestMapping(value = "/Logined", method = RequestMethod.POST)
	public String LoginToMypage(String userId, String userPw, HttpSession session) throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {
		String realUserPw = sc.EncodePW(userPw);
		System.out.println(realUserPw);
		MemberDTO dto = serv.login(userId, realUserPw);

		if (dto != null) {
			session.setAttribute("loginUser", dto);
			return "main";
		} else {
			return "member/Find_Info/cantFindUserdata";
		}
	}

	//로그아웃
	@RequestMapping(value = "/Logout", method = RequestMethod.GET)
	public String Logout(HttpSession session) {
		MemberDTO dto = (MemberDTO) session.getAttribute("loginUser");
		if (dto != null) {
			session.removeAttribute("loginUser");
			return "main";
		} else {
			return "member/Find_Info/cantFindUserdata";
		}
	}
	
	//아이디 찾기
	@RequestMapping(value = "/SearchID", method = RequestMethod.POST)
	public String SearchID(Model model, String userName, String ssn1, String ssn2) {
		MemberDTO dto = serv.findUserId(userName, ssn1, ssn2);
		System.out.println(dto);
		if (dto != null) {
			model.addAttribute("dto", dto);
			return "member/Find_Info/viewID";
		} else {
			return "member/Find_Info/cantFindUserdata";
		}
	}
	
	//비밀번호 찾기
	@RequestMapping(value = "/SearchPartPW", method = RequestMethod.POST)
	public String SearchPartPW(Model model, HttpServletResponse response, String userId, String userName, String ssn1, String ssn2) throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {
		MemberDTO dto = serv.findUserPW(userId, userName, ssn1, ssn2);
		String userPw = sc.DecodePW(dto.getUserPw());
		System.out.println(userPw);
		dto.setUserPw(userPw);
		
		if (dto != null) {
			Cookie userIdCookie = new Cookie("findPW_userid",userId);
			userIdCookie.setMaxAge(30*60);
			response.addCookie(userIdCookie);
			
			model.addAttribute("dto", dto);
			return "member/Find_Info/viewPartPW";
		} else {
			return "member/Find_Info/cantFindUserdata";
		}
	}
	
	//전체 비밀번호 출력용
	@RequestMapping(value = "/SearchAllPW", method = RequestMethod.GET)
	public String SearchAllPW(Model model, String userId, HttpSession session) throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {
		MemberDTO dto = serv.selectMemberData(userId);
		String userPw = sc.DecodePW(dto.getUserPw());
		System.out.println(userPw);
		dto.setUserPw(userPw);
		System.out.println(dto);
		
		if (dto != null) {
			model.addAttribute("dto", dto);
			
			//디버그 코드************************************************************************
			return "member/Find_Info/viewAllPW";
			//디버그 코드******
						
//			RequestDispatcher dis = request.getRequestDispatcher("SendEmailServlet");
//			dis.forward(request, response);
						
		} else {
			return "member/Find_Info/cantFindUserdata";
		}

	}
}

AjaxController(처리하지 않으면 유저가 입력한 비밀번호와 암호화된 비밀번호를 비교하게 된다)
	//메인에서 로그인 여부 확인 에이젝스
	@RequestMapping(value = "AjaxCheckIDPW", method = RequestMethod.POST)
	public String AjaxCheckIDPW(String userId, String userPw) throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {
		String realUserPw = sc.EncodePW(userPw);
		System.out.println(" 로그인페이지: 디코딩된 비밀번호: " + realUserPw);
		
		boolean canLogin = lServ.loginPossible(userId, realUserPw);
		String mesg = "loginSuccess";
		if (!canLogin) {
			mesg = "loginFail";                
        }
		return mesg;
	}
RegisterController(비밀번호 파트)
// 비밀번호 검증
		String userPw2 = dto.getUserPw();
		String userPw = sc.EncodePW(userPw2);
		String userPwConfirm = request.getParameter("userPwConfirm");

		if (!(userPw2.equals(userPwConfirm))) { // 비밀번호와 비밀번호 재확인 번호 일치 확인
			System.out.println("비밀번호 일치 오류 " + userPw2 + " " + userPwConfirm);
			System.out.println("회원 가입 실패");
			failMesg = false;
			request.setAttribute("mesg", "비밀번호가 일치하지 않습니다. 확인해주세요");
			return result;

		} else if (userPw2.length() < 6) { // 비밀번호 길이 규격확인
			System.out.println("비밀번호 길이 오류 " + userPw2 + " " + userPw.length());
			System.out.println("회원 가입 실패");
			request.setAttribute("mesg", "비밀번호 길이가 규정에 맞지 않습니다. 확인해주세요");
			return result;

		} else { // 비밀번호 규격 통과
			System.out.println("비밀번호 확인");
		}

비밀번호가 암호화되서 보인다.

하지만 이 작업만 할 경우에는 디버그를 해야 하는데 원래 암호를 알 수 없다.
그래서 비밀번호를 클릭하면 복호화하도록 만들었다.
겸사겸사 유저 삭제 기능도 추가했다.
<c:forEach var="dto" items="${memberList}">
			<tr>
				<td>${dto.userId}</td>
				<td><div class="pw" data-pw="${dto.getUserPw()}">${dto.getUserPw()}</div></td>
				<td>${dto.getUserName()}</td>
				<td>${dto.getNickname()}</td>
				<td>${dto.getUserSSN1()}</td>
				<td>${dto.getUserSSN2()}</td>
				<td>${dto.getUserGender()}</td>
				<td>${dto.getUserPhoneNum1()}</td>
				<td>${dto.getUserPhoneNum2()}</td>
				<td>${dto.getUserPhoneNum3()}</td>
				<td>${dto.getUserEmailId()}</td>
				<td>${dto.getUserEmailDomain()}</td>
				<td>${dto.getUserSignDate()}</td>
				<td>${dto.getUserType()}</td>
				<td><button class="deleteBtn" data-id="${dto.getUserId()}">삭제(참조 시 X)</button></td>
			</tr>
		</c:forEach>
	</table>

	<div id="sitesShortCut">
		<a href="<%=request.getContextPath()%>/Login">로그인 폼</a>
	</div>

	<script type="text/javascript">
		$(function() {

			$(".deleteBtn").on("click", function() {
				var userId = $(this).attr("data-id");
				var tr = $(this)
				
				$.ajax({
					type: "GET",
					url: "<c:url value='/IDDelete'/>", 
					data: {
						userId : userId
						},
					dataType: "text",
					success: function(){
						tr.closest("tr").remove();
						},
					error: function(xhr, status, error){
						console.log(error)
					}
				})
			})

			$(".pw").on("click", function() {

				var rp = $(this);
				var ecPW = rp.attr("data-pw");
				var userPw = rp.text();
				console.log(ecPW);

				if (ecPW == userPw) {
					
					$.ajax({
						type : "POST",
						url : "<c:url value='/DecodePW'/>",
						data : {
							encodingStr : userPw
						},
						dataType : "text",
						success : function(response) {
							rp.text(response)
						},
						error : function(xhr, status, error) {
							console.log(error)
						}
					})
					
				} else {
					
					$.ajax({
						type : "POST",
						url : "<c:url value='/EncodePW'/>",
						data : {
							userPw : userPw
						},
						dataType : "text",
						success : function(response) {
							rp.text(response)
						},
						error : function(xhr, status, error) {
							console.log(error)
						}
					})

				}
			})
		}) 
	</script>
TestController
// 멤버 삭제
	@RequestMapping(value = "/IDDelete", method = RequestMethod.GET)
	@ResponseBody
	public void IDDelete(String userId) {
		int num = serv.IDDelete(userId);
	}

삭제 버튼
삭제

3. 회원 가입 시 사용하는 ajax가 이전에 입력한 값과 동일할 경우, 처리되지 않도록 조정

다른 ajax를 처리한 코드를 보다가 이전에 입력한 값이 재입력한 값과 동일할 경우,
ajax 처리되지 않는 것을 확인했다.
이럴 경우, ajax처리로 인한 부하가 적어지기 때문에 렉을 더 줄이고자 적용시켰다.

		//닉네임 중복 확인
		var prevNickname = ""; 
		
		$("#nickname").on("focusout", function() {
			var nickname = $("#nickname").val();
		    var errorSpan = $("#confirmNicknameError");
		    
		    if (nickname !== prevNickname) {
				$.ajax({
	                type: "POST",
	                url: "<c:url value='/AjaxNicknameDuplicate'/>", 
	                data: { nickname: nickname },
	                
	                beforeSend: function () {
	                    // AJAX 요청 전에 로딩 표시 보여주기
	                	$("#loadingSpinner_for_nickname").show();
	                	// 가입 버튼 비활성화
	                	$("#register_button").prop("disabled", true);
	                	$("#userIdButton").prop("disabled", true);
	              },
	                
	                success: function (response) {
	                	//닉네임이 DB에 저장된 닉네임과 일치하는 데이터가 있을 경우, ajax 출력
	                    if (response === "duplicate") {
	                        errorSpan.text("이미 사용 중인 닉네임입니다.");
	                    } else {
	                        errorSpan.text("");
	                    } 
	                },
	                error: function (error) {
	                    console.error("닉네임 중복 검사 에러:", error);
	                }, 
	                
	                complete: function () {
	                    // AJAX 요청 완료 후에 로딩 표시 숨기기
	                	$("#loadingSpinner_for_nickname").hide();
	                	// 가입 버튼 활성화
		               	$("#register_button").prop("disabled", false);
		               	$("#userIdButton").prop("disabled", false);
	                }
				})
				prevNickname = nickname;
            };
		});
ajax를 처리하기 전에 변수에 저장하고, 그 값을 비교하는 식으로 만들었다.






이것으로 스프링에서의 팀플도 끝이 났다.
이 다음부터는 스프링부트에서 팀플을 진행했다.

그동안 어떤 작업을 했는지 재확인하고 상기시키는 작업을 해왔는데 생각보다 많이 하긴 했다.
더 배우고 싶고, 더 만들고 싶은데 시간이 없는게 한스러울 뿐...