본문 바로가기
Java/Servlet & JSP

JSP) 암호화 - salt 처리, 비밀번호 변경 페이지 생성

by 박채니 2022. 6. 26.

안녕하세요, 코린이의 코딩 학습기 채니 입니다.

 

개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.


암호화 (Encryption)

 

 

HelloMvcUtils

getInstance - 입력한 해시 알고리즘을 수행하는 MessageDigest 객체 생성

update(byte[]) - 객체 내에 저장된 digest 값 갱신

digest - update()를 실행, 해시 계산 완료 후 해시화된 값을 반환

public class HelloMvcUtils {
	public static String getEncryptedPassword(String rawPassword) {
		String encryptedPassword = null;
		
		try {
			// 1. 암호화
			MessageDigest md = MessageDigest.getInstance("SHA-512");
			byte[] input = rawPassword.getBytes("utf-8");
			md.update(input);
			byte[] encryptedBytes = md.digest();
			System.out.println(new String(encryptedBytes));
			
			// 2. 인코딩처리
			Encoder encoder = Base64.getEncoder();
			encryptedPassword = encoder.encodeToString(encryptedBytes);
			
		} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
			e.printStackTrace();
		}
				
		return encryptedPassword;
	}
}

@콘솔출력값
�U�`.�o��v������603^�	z���v��(Q/.
member@MemberEnrollServlet = Member [memberId=abcde, password=1ARVn2Auq2/WAqx2gNrL+q3RNjAzXpUfCXrzkA6d4Xa22yhRLy4AC50E+6UTPoscbo31nbOoq51gvkuXzJ6B2w==, memberName=알파벳, memberRole=null, gender=M, birthday=2000-01-01, email=abcde@abc.com, phone=01012341234, hobby=운동,등산,게임, point=0, enrollDate=null]

'1234'가 SHA-512 알고리즘을 통해 hash값이 생성된 것을 확인할 수 있습니다.

다만, '1234'를 SHA-512 알고리즘에 넣으면 항상 위와 같은 값을 생성해주기 때문에 비밀번호가 동일하다면 쉽게 유추해낼 수도 있을 것입니다.

eclipse의 비밀번호도 '1234'로 지정했더니 abcde와 동일한 hash 값이 생성된 것을 확인할 수 있습니다.

(단방향 알고리즘의 특징)

 

이를 방지하기 위해, salt 처리를 같이 해줌


salt 처리

 

ID를 salt 값으로 받아 처리해보기

public class HelloMvcUtils {
	public static String getEncryptedPassword(String rawPassword, String salt) {
		String encryptedPassword = null;
		
		try {
			// 1. 암호화
			MessageDigest md = MessageDigest.getInstance("SHA-512");
			byte[] input = rawPassword.getBytes("utf-8");
			byte[] saltBytes = salt.getBytes("utf-8");
			md.update(saltBytes);	// salt 전달
			byte[] encryptedBytes = md.digest(input);
			System.out.println(new String(encryptedBytes));
			
			// 2. 인코딩처리
			Encoder encoder = Base64.getEncoder();
			encryptedPassword = encoder.encodeToString(encryptedBytes);
			
		} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
			e.printStackTrace();
		}
				
		return encryptedPassword;
	}
}

이번에도 '1234'로 회원가입을 했지만 hash 값이 기존 'abcde', 'eclipse'와는 전혀 다른 것을 확인할 수 있습니다.

 

그렇다면 로그인 시, 해당 hash값을 입력해야 하나.. 싶지만 단방향의 특징을 이용하여 ID 값을 salt 처리한 비문과 DB상의 비문을 비교하면 될 것 같습니다.

 

MemberLoginServlet 상의 패스워드 처리

String password = HelloMvcUtils.getEncryptedPassword(request.getParameter("password"), memberId);

이렇게 해주면, 비문과 비문을 비교하는 것이기 때문에 사용자는 '1234'만 입력해도 로그인이 가능합니다.


기존 회원 업데이트 일괄 갱신

public class PasswordUpdater {
	public static void main(String[] args) {
		new PasswordUpdater().start();
	}

	private void start() {
		// 1. 회원아이디 조회 및 신규 비번 설정
		Connection conn = getConnection();
		String sql = "select * from member";
		List<Member> memberList = new ArrayList<>();
		try (
			PreparedStatement pstmt = conn.prepareStatement(sql);
			ResultSet rset = pstmt.executeQuery();
		){
			while(rset.next()) {
				String memberId = rset.getString("member_id");
				String password = HelloMvcUtils.getEncryptedPassword("1234", memberId);
				
				Member member = new Member();
				member.setMemberId(memberId);
				member.setPassword(password);
				 
				memberList.add(member);
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		// 2. 일괄 업데이트
		sql = "update member set password = ? where member_id = ?";
		try(
			PreparedStatement pstmt = conn.prepareStatement(sql);
		){
			for(Member member : memberList) {
				pstmt.setString(1, member.getPassword());
				pstmt.setString(2, member.getMemberId());
				pstmt.executeUpdate();
				System.out.printf("%s 비밀번호 업데이트 성공!\n", member.getMemberId());
			}
			commit(conn);
		} catch (SQLException e) {
			rollback(conn);
		}
		close(conn);
		System.out.println("[비밀번호 일괄갱신이 끝났습니다.]");
	}
}

@콘솔출력값
chany 비밀번호 업데이트 성공!
honggd 비밀번호 업데이트 성공!
qwerty 비밀번호 업데이트 성공!
admin 비밀번호 업데이트 성공!
sinsa 비밀번호 업데이트 성공!
abcde 비밀번호 업데이트 성공!
eclipse 비밀번호 업데이트 성공!
chany123 비밀번호 업데이트 성공!
[비밀번호 일괄갱신이 끝났습니다.]


다만 내 정보 보기에서의 기존 비밀번호가 hash 값을 그대로 가지고 오기 때문에 아래와 같이 확인됩니다.


비밀번호 변경 페이지 별도로 생성하기

 

passwordUpdate.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ include file="/WEB-INF/views/common/header.jsp" %>

	<section id=enroll-container>
		<h2>비밀번호 변경</h2>
		<form 
			name="passwordUpdateFrm" 
			action="<%=request.getContextPath()%>/member/passwordUpdate" 
			method="post" >
			<table>
				<tr>
					<th>현재 비밀번호</th>
					<td><input type="password" name="oldPassword" id="oldPassword" required></td>
				</tr>
				<tr>
					<th>변경할 비밀번호</th>
					<td>
						<input type="password" name="newPassword" id="newPassword" required>
					</td>
				</tr>
				<tr>
					<th>비밀번호 확인</th>
					<td>	
						<input type="password" id="newPasswordCheck" required><br>
					</td>
				</tr>
				<tr>
					<td colspan="2" style="text-align: center;">
						<input type="submit"  value="변경" />
					</td>
				</tr>
			</table>
		</form>
	</section>
	<script>
	document.querySelector("#newPasswordCheck").onblur = (e) => {
		const newPassword = document.querySelector("#newPassword");
		if(newPassword.value !== e.target.value) {
			alert("비밀번호가 일치하지 않습니다.");
			newPassword.select();
		}
	}
	
	document.passwordUpdateFrm.onsubmit = () => {
		const newPassword = document.querySelector("#newPassword");
		const oldPassword = document.querySelector("#oldPassword");
		const re = /[a-zA-Z0-9!@#$%^&*(){4,}]/;
		if(!re.test(newPassword.value)) {
			alert("비밀번호는 영문자/숫자/!@#$%^&*() 포함 최소 4글자 이상이여야 합니다.");
			newPassword.select();
			return false;
		}
		if(!re.test(oldPassword.value)) {
			alert("비밀번호는 영문자/숫자/!@#$%^&*() 포함 최소 4글자 이상이여야 합니다.");
			oldPassword.select();
			return false;
		}
		
		const newPasswordCheck = document.querySelector("#newPasswordCheck");
		if(newPassword.value !== newPasswordCheck.value) {
			alert("비밀번호가 일치하지 않습니다.");
			newPassword.select();
			return false;
		}
	};
	</script>
	
<%@ include file="/WEB-INF/views/common/footer.jsp" %>

 

Controller

PasswordUpdateServlet

@WebServlet("/member/passwordUpdate")
public class PasswordUpdateServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private MemberService memberService = new MemberService();
	
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		request.getRequestDispatcher("/WEB-INF/views/member/passwordUpdate.jsp").forward(request, response);
	}
	
	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		try {
			// 1. 사용자 입력 값 처리
			HttpSession session = request.getSession();
			Member loginMember = (Member) session.getAttribute("loginMember");
			String memberId = loginMember.getMemberId();
			String oldPassword = HelloMvcUtils.getEncryptedPassword(request.getParameter("oldPassword"), memberId);
			String newPassword = HelloMvcUtils.getEncryptedPassword(request.getParameter("newPassword"), memberId);
			
			// 2. 업무로직
			// a. 기존 비밀번호 검증
			Member member = memberService.findById(memberId);
			
			String msg = null;
			String location = request.getContextPath();
			if(member != null && oldPassword.equals(member.getPassword())) {
				// b. 신규 비밀번호 업데이트
				int result = memberService.updatePassword(newPassword, memberId);
				msg = "비밀번호 변경 성공!";
				location += "/member/memberView";
				
				// 세션 비밀번호도 갱신
				loginMember.setPassword(newPassword);
			} else {
				msg = "기존 비밀번호와 일치하지 않습니다.";
				location += "/member/passwordUpdate";
			}
			
			// 3. 응답
			// a. 비밀번호 정상 변경 후 내정보보기 페이지로 이동
			// b. 비밀번호 변경 실패 시 (기존 비밀번호 불일치) 비밀번호 변경페이지로 이동
			session.setAttribute("msg", msg);
			response.sendRedirect(location);
		} catch(Exception e) {
			e.printStackTrace();
			throw e;
		}
	}
}

 

Service

MemberService

public int updatePassword(String newPassword, String memberId) {
    Connection conn = getConnection();
    int result = 0;
    try {
        result = memberDao.updatePassword(conn, newPassword, memberId);
        commit(conn);
    } catch(Exception e) {
        rollback(conn);
        throw e;
    } finally {
        close(conn);
    }
    return result;
}

 

Dao

MemberDao

// 비밀번호 변경
// update member set password = ? where member_id = ?
public int updatePassword(Connection conn, String newPassword, String memberId) {
    PreparedStatement pstmt = null;
    int result = 0;
    String sql = prop.getProperty("updatePassword");

    try {
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, newPassword);
        pstmt.setString(2, memberId);
        result = pstmt.executeUpdate();
    } catch(Exception e) { 
        throw new MemberException("비밀번호 변경 오류!", e);
    } finally {
        close(pstmt);
    }
    return result;
}

정보 변경 시에는 session에 있는 정보도 수정해줘야 버그 발생 확률이 낮아지기 때문에, loginMember의 password도 변경된 password로 업데이트 해주었습니다.