공부/그룹 스터디

[5회차 01] 로그인 - 비밀번호 검증

junani0v0 2024. 5. 1. 23:57

로그인


이전까지 회원가입을 해보았으니 이번에는 로그인을 해보겠습니다

login.jsp 로그인을 위한 준비작업

먼저 login.jsp에서 login 부분의 코드를 먼저 봐보겠습니다

<div class="form login">
    <span class="title">Login</span>

    <form action="#">
        <div class="input-field">
            <input type="text" placeholder="Enter your email" required>
            <i class="uil uil-envelope icon"></i>
        </div>
        <div class="input-field">
            <input type="password" class="password" placeholder="Enter your password" required>
            <i class="uil uil-lock icon"></i>
            <i class="uil uil-eye-slash showHidePw"></i>
        </div>

        <div class="checkbox-text">
            <div class="checkbox-content">
                <input type="checkbox" id="logCheck">
                <label for="logCheck" class="text">Remember me</label>
            </div>

            <a href="#" class="text">Forgot password?</a>
        </div>

        <div class="input-field button">
            <input type="button" value="Login">
        </div>
    </form>

    <div class="login-signup">
        <span class="text">Not a member?
            <a href="#" class="text signup-link">Signup Now</a>
        </span>
    </div>
</div>
  • name 추가
    코드를 보면 input쪽에 name이 없는게 보이실 꺼에요 먼저 로그인을 위해 필요한 email과 password에 name을 추가해줍니다
<div class="form login">
    <span class="title">Login</span>

    <form action="#">
        <div class="input-field">
            <input type="text" placeholder="Enter your email" name="memberId" required>
            <i class="uil uil-envelope icon"></i>
        </div>
        <div class="input-field">
            <input type="password" class="password" placeholder="Enter your password" name="password" required>
            <i class="uil uil-lock icon"></i>
            <i class="uil uil-eye-slash showHidePw"></i>
        </div>

        <div class="checkbox-text">
            <div class="checkbox-content">
                <input type="checkbox" id="logCheck">
                <label for="logCheck" class="text">Remember me</label>
            </div>

            <a href="#" class="text">Forgot password?</a>
        </div>

        <div class="input-field button">
            <input type="button" value="Login">
        </div>
    </form>

    <div class="login-signup">
        <span class="text">Not a member?
            <a href="#" class="text signup-link">Signup Now</a>
        </span>
    </div>
</div>

여기서 값을 보내기위해 login에도 컨트롤러에서 맵핑을 해줘야 합니다

LoginController 맵핑

현재 코드들을 페이지를 열때도 그리고 액션을 실행할때도 .do를 사용하고 있습니다.

페이지를 실행하는 것은 Page.do , 단순 실행만 하는건 .do로 해서 구분하겠습니다

@RequestMapping("login.do")
public ModelAndView login(@RequestParam HashMap<String, String> params) {
    ModelAndView mv = new ModelAndView();
    mv.setViewName("main");

    return mv;
}

login.jsp에서 로그인을 하면 받아주는 login.do를 맵핑해줍니다

MemberDto.java 생성

dto패키지에 MemberDto를 만들어 줍니다

그리고 DB에가서 간단하게 Member 테이블을 복사 MemberDto에 붙여넣어 줍니다

public class MemberDto {

    member_seq
    member_id
    passwd
    member_nm
    email
    auth_yn
    pwd_chng_dtm
    join_dtm

}

접근제어자와 타입을 추가하겨 카멜형식으로 바꿔줍니다
그리고 우클릭-> source -> getter & setter에 가서 getter, setter를 만들어 줍니다

public class MemberDto {

    private int memberSeq;
    private String memberId;
    private String passwd;
    private String memberNm;
    private String email;
    private String authYn;
    private String pwdChngDtm;
    private String joinDtm;

    public int getMemberSeq() {
        return memberSeq;
    }
    public void setMemberSeq(int memberSeq) {
        this.memberSeq = memberSeq;
    }
    public String getMemberId() {
        return memberId;
    }
    public void setMemberId(String memberId) {
        this.memberId = memberId;
    }
    public String getPasswd() {
        return passwd;
    }
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
    public String getMemberNm() {
        return memberNm;
    }
    public void setMemberNm(String memberNm) {
        this.memberNm = memberNm;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getAuthYn() {
        return authYn;
    }
    public void setAuthYn(String authYn) {
        this.authYn = authYn;
    }
    public String getPwdChngDtm() {
        return pwdChngDtm;
    }
    public void setPwdChngDtm(String pwdChngDtm) {
        this.pwdChngDtm = pwdChngDtm;
    }
    public String getJoinDtm() {
        return joinDtm;
    }
    public void setJoinDtm(String joinDtm) {
        this.joinDtm = joinDtm;
    }
}

로그인 처리


JoinService

public MemberDto login(HashMap<String, String> params) {
    //사용자 찾기

    //비밀번호 비교

}

JoinService로 가서 MemberDto를 사용하겠습니다
반환형은 MemberDto로하고 값은 컨트롤러와 같은 HashMap으로 받겠습니다

사용자 찾기

먼저 일반적으로 DB에서 값을 찾을 때는 ID로 찾습니다
사용자의 모든 필드를 가지고와서 사용할껀데, 모든 필드를 가져오기위해 MemberDto를 만들었습니다

MemberDao

MemberDao에 MemberId로 조회하는 쿼리를 만들껀데요
이전과는 다르게 쿼리에안에 memberId를 넣는게 아닌 ?를 넣어서 처리해보겠습니다

이제 memberId는 Object배열에 넣어주면 순서대로 ?안에 들어가게 됩니다

public MemberDto getMember(String memberId) {
    String sql = "SELECT member_seq FROM member WHERE member_id= ? ";
    Object[] args = {memberId};
}

그 이유는 Object는 모든 클래스의 최상위 클래스로 is a 관계가 무조건 성립합니다
즉, 상속이 무조건 가능하기에 값이 들어갑니다
하지만 쿼리를 실행시켜 하나의 DTO 또는 참조형 클래스로 뭘 땡겨오려면 전처럼 RowMapper가 필요한데요
이번에는 따로 java파일을 만들지 않고 Dao아래에 클래스를 만들어 사용해 보겠습니다

memberRowMapper를 만들고 RowMapper안 MemberDto를 넣어서 추상메서드를 구현 시켜줍니다
memberRowMapper에 마우스를 가져다대면 추상매서드 생성이 나옵니다

class memberRowMapper implements RowMapper<MemberDto>{

    @Override
    public MemberDto mapRow(ResultSet rs, int rowNum) throws SQLException {

        return null;
    }

}

< 주의 >

여기서 RowMapper를 tree말고 아래꺼로 선택하세요

class MemberRowMapper implements RowMapper<MemberDto>{

    @Override
    public MemberDto mapRow(ResultSet rs, int rowNum) throws SQLException {
        MemberDto memberDto = new MemberDto();
        memberDto.setMemberSeq(rs.getInt("member_seq"));
        memberDto.setMemberId(rs.getString("member_id"));
        memberDto.setPasswd(rs.getString("passwd"));
        memberDto.setMemberNm(rs.getString("member_nm"));
        memberDto.setEmail(rs.getString("email"));
        memberDto.setAuthYn(rs.getString("auth_yn"));
        memberDto.setPwdChngDtm(rs.getString("pwd_chng_dtm"));
        memberDto.setJoinDtm(rs.getString("join_dtm"));    
        return memberDto;
    }

}

이렇게 memberRowMapper를 추상 메서드를 완성해 줍니다
그리고 이제 다시 getMember로 가서 memberRowMapper를 사용해봅시다

public MemberDto getMember(String memberId) {
    String sql = "SELECT * FROM member WHERE member_id= ? ";
    Object[] args = {memberId};

    return queryForObject(sql, new MemberRowMapper(), args);
}

하나의 값만 가져올 것이기에 queryForObject를 사용하고 첫번째에는 무조건 sql을 넣어주고,
두번째꺼는 어떤 형태로 받을것인지가 들어갑니다 그리고 마지막으로 만들어준 배열을 넣어 값을 넣어줍니다

queryForObject(sql, 리턴타입, 배열);

이제 이렇게하면 sql에 직접 입력안해주고 파라미터 들어갈 자리에 ? 넣고 ?에 들어갈 값은 Object배열에 순서대로 넣어줍니다

JoinService

이제 만들어준 getMember를 서비스에서 호출해 보겠습니다

public MemberDto login(HashMap<String, String> params) {
    //사용자 찾기
    MemberDto memberDto = memberDao.getMember(params.get("memberId"));
    String passwd = params.get("passwd"); 
        //사용자가 입력한 비밀번호
    String dbpasswd = memberDto.getPasswd(); 
        //암호화해서 DB에 저장된 비밀번호
    String encPasswd = BCrypt.withDefaults().hashToString(12, passwd.toCharArray());
    //사용자가 입력한 비밀번호 암호화
//비밀번호 비교
}

< 주의 >

Dao에있는 String 쿼리문에서 비밀번호 비교는 절대로 하면 안됩니다
이유는 보안 코딩에 위배되기 때문입니다
그렇기에 사용자의 아이디를 이용해 한건만 가져옵니다

  • DB에서 가져온 암호화된 비밀번호
  • 사용자가 입력한 비밀번호 암호화

사용자가 입력한 비밀번호를 암호화하고 이 암호화된 비밀번호를 DB에서 가져온 암호화된 비밀번호와 비교해야합니다

즉, 사용자에게 입력받은 비밀번호가 맞다 틀리다는 우리가 판단하는게 아닌 암호화를 해서 암호화한 두개를 비교

비밀번호 비교하기


JoinService

먼저 값이 어떻게 나오는지 보기위해 값을 찍어보겠습니다
그리고 에러방지를 위해 시스템을 끄고, return null을 해줍니다

public MemberDto login(HashMap<String, String> params) {
    //사용자 찾기
    MemberDto memberDto = memberDao.getMember(params.get("memberId"));
    String passwd = params.get("passwd"); 
        //사용자가 입력한 비밀번호
    String dbpasswd = memberDto.getPasswd(); 
        //암호화해서 DB에 저장된 비밀번호
    String encPasswd = BCrypt.withDefaults().hashToString(12, passwd.toCharArray());
        //사용자가 입력한 비밀번호 암호화
    //비밀번호 비교
    System.out.println("--------------dbpasswd : " + dbpasswd);
    System.out.println("--------------encPasswd : " + encPasswd);
    System.exit(0);
    return null;
}

LoginController

loginController에 joinService 호출을 추가해줍니다

@RequestMapping("login.do")
public ModelAndView login(@RequestParam HashMap<String, String> params) {
    ModelAndView mv = new ModelAndView();
    joinService.login(params);
    mv.setViewName("main");

    return mv;
}

이러면 사용자가 아이디 패스워드 넣으면 -> controller에 연결되고 ->service호출->service에서 비밀번호 가져와 비교

login.jsp

값을 보내기 위해 form action을 설정해주고 로그인 버튼타입을 submit으로 바꿔줍니다
그리고 값은 post로 보내겠습니다

<form action="/05/login.do" method = "post">
    <div class="input-field">
        <input type="text" placeholder="Enter your email" name="memberId" required>
        <i class="uil uil-envelope icon"></i>
    </div>
    <div class="input-field">
        <input type="password" class="password" placeholder="Enter your password" name="password" required>
        <i class="uil uil-lock icon"></i>
        <i class="uil uil-eye-slash showHidePw"></i>
    </div>

    <div class="checkbox-text">
        <div class="checkbox-content">
            <input type="checkbox" id="logCheck">
            <label for="logCheck" class="text">Remember me</label>
        </div>

        <a href="#" class="text">Forgot password?</a>
    </div>

    <div class="input-field button">
        <input type="submit" value="Login">
    </div>
</form>

JoinService

로그인을 해볼까요?

지금은 DB에 없는 사용자를 입력하면 500에러가 발생할테니 있는 사용자를 넣어 로그인 해보겠습니다

로그인을 해보니 위처럼 값이 나오는데요 값을 자세히봐보면 서로 다른것을 알 수 있습니다

같은 값을 넣었는데 왜 다르게 나올까요??

우리가 암호화 모듈을 만들어 암호화를 했으면 똑같방식으로 비교를 할 수 있는데
우리는 만든게 아닌 기존에 만들어 놓은걸 사용(ex.BCrypt)하는데 똑같은 값을 입력해도 값이 다르게 나옵니다
(유출될 수 있기에 같은 값을 넣어도 암호화된 값은 할때마다 달라야 합니다)
그렇기에 이것은 값을 비교하는 메서드가 따로 있습니다.

BCrypt.Result result = BCrypt.verifyer().verify(passwd.toCharArray(), dbpasswd);

System.out.println("-------------result : " + result.verified);

이러한 비교 메서드를 사용하는데
앞에는 사용자가 입력한 값, 뒤에는 DB에서 가져온 값을 입력합니다
위에 result.verified은 자체가 boolean값을 가지고 있어 출력을해보면 true,false가 나옵니다

이렇게 비밀번호 값이 맞다면 암호화된 값이 달라로 같아도 true를 반환해줍니다

< 비밀번호 비교 >

  • 입력한 값을 암호화하고 DB에 암호화된 값과 직접 비교
  • 비교 메서드를 사용하여 true, false로 비교

이제 return값을 true면 memberDto를 false면 null을 반환하게 작성해줍니다

그리고 검증은 끝났으니 sysout은 다 주석처리해 주세요

public MemberDto login(HashMap<String, String> params) {
     //사용자 찾기
        MemberDto memberDto = memberDao.getMember(params.get("memberId"));
        String passwd = params.get("passwd"); 
         //사용자가 입력한 비밀번호
        String dbpasswd = memberDto.getPasswd(); 
         //암호화해서 DB에 저장된 비밀번호
        String encPasswd = BCrypt.withDefaults().hashToString(12, passwd.toCharArray());
        //사용자가 입력한 비밀번호 암호화
    //비밀번호 비교
//        System.out.println("--------------dbpasswd : " + dbpasswd);
//        System.out.println("--------------encPasswd : " + encPasswd);
        //유출될 수 있기에 같은 값을 넣어도 암호화된 값은 할때마다 달라야 한다

        //BCrypt 같은 값을 넣어도 결과값이 다르기 나옴
        //그래서 비교 메서드 따로 해줘야한다
        BCrypt.Result result = BCrypt.verifyer().verify(passwd.toCharArray(), dbpasswd);
//        System.out.println("-------------result : " + result.verified);
//        System.exit(0);
        return result.verified ? memberDto : null ;
    }

LoginController

login메서드에 HttpServletRequest를 받아주고
로그인을 했으면 memberDto가 반환되었을테니 if문을 만들어 값이 있을 경우에만 세션값을 넣어주도록 코드를 작성해줍니다

@RequestMapping("login.do")
public ModelAndView login(
        HttpServletRequest request,
        @RequestParam HashMap<String, String> params) {
    ModelAndView mv = new ModelAndView();
    MemberDto memberDto = joinService.login(params);

    if(!ObjectUtils.isEmpty(memberDto)) {    
        //로그인을 했으면 null값이 아닐테니
    //세션 처리
        //세션을 불러와서
        HttpSession session = request.getSession();
        //memberId 넣어줌
        session.setAttribute("memberId", memberDto.getMemberId());
    }
    mv.setViewName("main");

    return mv;
}

HttpServletRequest import 안될경우

Alt + Enter를 해서 Java Build Path을 가서 Classpath에 serverRuntime이 있는지 확인합니다
없을 경우 죄측 아래 Targeted Runtimes에서 서버를 선택하고 applay를 해줍니다
그래도 안될경우 서버를 새로 만들어 연결해보세요