웹페이지를 제작할 때 가장 중요한 것은 보안이다.
만약, 해킹을 통해 DB가 뚫렸을 때 사용자의 비밀번호가 그대로 저장이 되어있으면 어떻게 될까 ? 사람 심리란게 비밀번호가 다 비슷하기 때문에 해당 아이디와 비밀번호로 다른 사이트까지 해킹이 가능할 것이다.
따라서 사용자의 비밀번호를 암호화 시켜서 DB에 저장하고 그 암호화 코드를 이용해서 로그인도 할 수 있게 하는 라이브러리를 제작할 것이다.
그렇다면 생각해야 될 점은 무엇일까 ? 복호화가 불가능하게 단방향 암호로 만들어 만약 DB가 뚫려도 원본 비밀번호를 알 수 없게 해야할 것이다.
그래서 각 아이디의 고유 랜덤값을 주고, 그 해당 랜덤값과 비밀번호 더해 해쉬코드로 만들어 DB에 저장할 것이다.
CodeEncryptionOfOneWay.java
public class CodeEncryptionOfOneWay {
private String idCode;
private String pwdCode;
private Salt ms = new Salt();
private HashCode hc = new HashCode();
public CodeEncryptionOfOneWay(String idCode, String pwdCode) {
this.idCode = idCode;
this.pwdCode = pwdCode;
} -------- 1
public ArrayList<CodeEntity> getEncryptingCode() {
return getArrayListOfCodeEncryptionOfOneWay();
} -------- 2
private ArrayList<CodeEntity> getArrayListOfCodeEncryptionOfOneWay() {
ArrayList<CodeEntity> ceArr = new ArrayList<CodeEntity>();
ce.add(actionOfEncryptingCode(idCode, pwdCode));
return ceArr;
} -------- 3
private CodeEntity actionOfEncryptingCode(String idCode, String pwdCode) {
CodeEntity cObj = new CodeEntity(idCode,
ms.actionOfMakeSalt(),
hc.actionOfMakeHash(ms.getSalt(), pwdCode));
return cObj;
} -------- 4
}
- 사용자로 부터 아이디와 비밀번호를 받아온다.
CodeEntity
타입의 ArrayList를 반환하는 메서드- ArrayList에 값이 담긴
CodeEntity
를 추가 하는 메서드CodeEntity
객체에 아이디와, 랜덤값을 담당할 고유의salt
값을Salt
클래스의actionOfMakeSalt()
를 통해 가져와 담아주고,Salt
클래스로 부터 생성된salt
값과 기존의 비밀번호를 합쳐HashCode
클래스의actionOfMakeHash()
를 통해 해쉬코드를 가져와 담는 메서드
Salt.java
public class Salt {
public String actionOfMakeSalt() {
byte[] randomByte = makeRandomByteArray();
return new String(Base64.getEncoder().encode(randomByte));
} ----- 1
private byte[] makeRandomByteArray() {
byte[] randomByte = null;
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
randomByte = new byte[16];
random.nextBytes(randomByte);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return randomByte;
} ----- 2
public String getSalt() {
return actionOfMakeSalt();
} ----- 3
}
makeRandomByte()
를 통해 임의의 난수값 배열을 받아온다. 그 다음 받아온 배열값을 코드화 하고 제어문자와 특수문자를 제외한 안전한 문자만 받아와 DB의 값에 저장하기 위해 BASE64로 인코딩을 진행한다. 그 후 그 값을 반환한다.SHA1PRNG
알고리즘을 사용함으로써 복호화가 불가능하게 만들어 준다. 그리고 기존의Random
(48비트)보다 복호화가 더 힘든SecureRandom
(128비트)를 사용하여 난수를 생성해준다. 그렇게 생성된 난수를 byte배열인randomByte
에 담아서 반환한다.- DB에는 해당 아이디와 고유의 랜덤값이 담겨야 하므로 salt 값을 반환해주는 메서드이다.
HashCode.java
public class HashCode {
public String actionOfMakeHash(String salt, String pwdCode) {
return actionOfEncryption(plusStr(salt, pwdCode));
} ----- 1
public String actionOfEncryption(String plusStr) {
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
md.update(plusStr.getBytes());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return String.format("%064x", new BigInteger(1, md.digest()));
} ----- 2
public String plusStr(String salt, String pwdCode) {
return salt+pwdCode;
} ----- 3
}
- salt값과 비밀번호값을 받아와 합친 해쉬코드값을 반환하는 메서드
- Java에서 SHA를 이용한 알고리즘을 사용하기 위해
MessageDigest
클래스를 사용하고update()
함수를 이용하여 지정된 바이트 데이터를 사용해 다이제스트를 갱신합니다. 그리고 그 해당 다이제스트값을digest()
로 해쉬를 바이트 배열로 반환하고 64자리의 16진수 값으로 패딩합니다.
(Message : Hash 함수를 통과하기 전 원본데이터 / Digest : Hash 함수 통과 후 데이터)- 랜덤값인 salt값과, 비밀번호를 합쳐주는 메서드
CheckMachingCode.java
public class CheckMachingCode {
private HashCode hc = new HashCode(); ----- 1
public boolean checkMatchingCode(String pwdCode, String salt, String hashCode) {
return checkMachingHashCode(pwdCode, salt, hashCode);
}----- 2
private boolean checkMatchingHashCode(String pwdCode, String salt, String hashCode) {
if(hc.actionOfEncryption(hc.plusStr(salt, pwdCode)).equals(hashCode)){
return true;
}
return false;
}----- 3
}
- DB에 저장된 고유의 salt값을 해쉬코드화 하기 위해 HashCode를 전역객체로 선언한다.
- 사용자로부터 서버로부터 온 패스워드와 DB로부터 고유의 랜덤값과 암호화된 해쉬코드값을 받아온다.
HashCode
클래스의actionOfEncryption()
함수에 받아온 데이터 값을 넣어 해쉬코드화 시키고 DB로부터 온 해쉬코드와 값이 같은지 비교하여 boolean값을 반환한다.
기존에 했던 서블릿 + jsp 기반의 프로젝트에 해당 기능 라이브러리로 적용시켜보았다.
다만, 기존의 로직이 짜여진 상태에서 추가한 것 이기때문에 구조가 비효율적으로 보일 수 있다.
public class NoticeBoardMakeMemeberController extends HttpServlet{
ArrayList<CodeEntity> hashCodeArr = new ArrayList<CodeEntity>();
.
.
private ArrayList<CodeEntity> changePwdToHashCode(String userId, String userPwd) {
ArrayList<CodeEntity> ceArr = new ArrayList<CodeEntity>();
CodeEncryptionOfOneWay cw = new CodeEncryptionOfOneWay(userId, userPwd);
arr = cw.getEncryptingCode();
return ceArr;
}
.
.
}
- 기존 회원가입 컨트롤단 로직에
CodeEntity
타입의 ArrayList를 반환하는 메서드를 만들어 주었다.- 그리고 비밀번호를 암호화 해주는
CodeEncryptionOfOneWay
클래스에 인자로 클라이언트로 부터 받아온 아이디와 비밀번호를 인자로 넘겨 인스턴스를 생성한다.- 해당 인스턴스의 함수
getEncryptingCode()
를 통해CodeEntity
타입의 ArrayList를 받아와 해당 리스트를 반환한다.
(ArrayList<CodeEntity>
= <userId, salt, hashCode>)
public class NoticeBoardMakeMemeberController extends HttpServlet{
ArrayList<CodeEntity> hashCodeArr = new ArrayList<CodeEntity>();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
hashCodeArr = changePwdToHashCode(request.getParameter("user_ID"),
request.getParameter("user_PW"));
makeMemberAction(hashCodeArr.get(0).getIdCode(),
........ hashCodeArr.get(0).getHashCode(),
........ hashCodeArr.get(0).getSaltCode());
}
private ArrayList<CodeEntity> changePwdToHashCode(String userId, String userPwd) {
ArrayList<CodeEntity> ceArr = new ArrayList<CodeEntity>();
CodeEncryptionOfOneWay cw = new CodeEncryptionOfOneWay(userId, userPwd);
arr = cw.getEncryptingCode();
return ceArr;
}
.
.
}
- 기존의 서블릿코드에 전역으로 선언된
CodeEntity
타입의 ArrayList에 바로 전에 만들었던changePwdToHashCode()
메서드를 통해CodeEntity
객체를 담는다.- 그렇게 담겨진 리스트에서 id와 salt값과 hashcode값을 인자로 넘겨준다.
- 그리고 해당 아이디의 고유값을 담아줄 테이블을 생성한다.
- MEMBERID컬럼은
BOARDMEMBER
의 USERID의 외래키가 될 것 이다.
MEMEBERIDCODE
테이블에 아이디와 고유의 랜덤값이 저장되어 있는 것을 볼 수 있다.- 또한
BOARDMEMBER
테이블에 패스워드가 암호화된 값으로 저장되어 있는 것을 볼 수 있다.
그렇다면 로그인할때는 어떻게 해야할까 ?
public class LoginDAO extends NoticeBoardProjectDAO{
.
.
.
private boolean checkPwd(Connection con, PreparedStatement pst, ResultSet rs,
String userId, String userPwd) {
CheckMachingCode cmc = new CheckMachingCode();
String salt = getSalt(con, pst, rs, userId);
return cmc.checkMachingCode(userId, userPwd, salt,
getHashCode(con,pst,rs,userId));
}
.
.
.
}
- 기존 로그인 DAO로직단에서 클라이언트로부터 온 userid와 userPwd, DB로부터 온 salt값과 암호화된 비밀번호 값을 받아와
CheckMachingCode
클래스의checkMachingCode()
함수를 통해 다시 암호화로 변환 후 기존의 DB에 저장된 값과 일치하는지 알아본다.
(salt값과 hashCode값은 단순히 디비에서 가져오도록 했다.)- 그 다음 반환타입을 boolean으로 해주어 일치하지 않으면 false, 일치하면 true를 반환 할 것이다.
public class LoginDAO extends NoticeBoardProjectDAO{
public Token getTokenOfLoginCheckIfLogic(Connection con, PreparedStatement pst, ResultSet rs,String userId, String userPwd) {
if(checkPwd(con,pst,rs,userId,userPwd)) {
JdbcClose(con, pst, rs);
return Token.LOGINSUCCESS;
}
return Token.LOGINFAIL;
}
}
- 그래서 최종적으로
checkPwd()
메서드의 반환값을 통해 토큰발행해서 로그인 성공 실패를 알린다.
잘 읽었습니다