실제 회사에서 사용할 웹 애플리케이션을 제작하고 있다 가정하고 우리는 비밀번호를 데이터베이스에 저장하고 있습니다. 데이터베이스 자체에 보안이 되어 있을 것이지만 만약 뚫리게 된다면 비밀번호가 무방비하게 노출된다는 문제점이 있습니다. 이러한 문제점을 보완하기 위해 비밀번호가 노출이 되더라도 알아볼 수 없도록 암호화하는 방법이 있을 것입니다. 이 방법이 비밀번호를 인코딩(암호화) 해주고 사용자가 제출한 비밀번호와 데이터베이스에 저장된 인코딩 된 비밀번호가 일치하는지 확인해주는 메서드를 제공해주는 클래스가 바로 BCryptPasswordEncoder 클래스입니다.
pom.xml에 아래와 같이 3가지를 추가해줍니다. 버전은 5.4.2로 설정하였습니다.
<!-- BCryptPasswordEncoder Security 라이브러리 추가 -->
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.4.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.4.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.4.2</version>
</dependency>
Spring Security와 관련된 빈(bean)과 설정을 따로 관리하기 위해서 security-context.xml 파일을 새로 만들어서 설정하였습니다.
먼저 servlet-context.xml 파일이 있는 경로에 'Spring Bean Configuration File'을 통해서 'security-context' 이름의 파일을 'beans'와 'security' namespaces를 추가 해준 뒤 생성합니다.
* security namespace의 경우 버전이 적히지 않은 것을 선택해줍니다.
생성한 파일에 BCryptPasswordEncoder를 빈(bean)으로 등록하기 위해 아래의 코드를 추가해줍니다.
<!-- BCryptPasswordEncoder -->
<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
web.xml에서 servlet-context.xml의 경로가 적힌 태그 안에 줄 바꿈을 하여 동일하게 security-context.xml파일의 위치를 작성합니다. 해당 과정을 통해서 스프링이 security-context.xml을 인식할 수 있도록 합니다.
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/appServlet/servlet-context.xml
/WEB-INF/spring/appServlet/security-context.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
BCryptPasswordEncoder를 @Autowired 어노테이션을 이용해서 의존성 주입을 해줍니다.
@Autowired
private BCryptPasswordEncoder pwEncoder;
기존 코드의 흐름은 사용자가 작성한 회원가입 정보를 MemberVO 클래스 타입의 member를 통해 데이터를 받아옵니다. 회원의 정보가 담긴 member를 매개변수로 한 memberJoin()메서드를 호출하게 되고 이를 통해 BOOK_MEMBER 테이블에 데이터를 insert하는 쿼리문이 실행됩니다.
회원정보가 담긴 member => memberJoin()을 호출하여 회원가입 쿼리 실행
변경된 코드의 흐름은 회원의 정보가 등록될 때 회원의 비밀번호를 그대로 등록하는 것이 아니라 비밀번호를 사람이 알아볼 수 없는 형태의 문자열로 인코딩을 한 후 저장하고자 합니다. 따라서 우리는 member객체에서 비밀번호를 꺼낸 뒤, 비밀번호를 BCryptPasswordEncoder클래스의 encode() 메서드를 사용하여 인코딩을 합니다. 인코딩을 한 비밀번호를 member객체에 다시 저장 한 뒤 memberJoin() 메서드를 실행하여 회원정보를 등록하는 쿼리문을 실행합니다.
회원정보가 담긴 member => member에 저장된 비밀번호를 꺼냄 => encode() 메서드를 통해 꺼낸 비밀번호 인코딩 => 인코딩 된 비밀번호를 member 객체에 다시 저장 => memberJoin()을 호출하여 회원가입 쿼리 실행
회원가입 쿼리는 그대로 사용할 것이기 때문에 MemberController클래스에 있는 코드만 수정해주면 됩니다.
/* 회원가입 */
@RequestMapping(value="/join", method=RequestMethod.POST)
public String joinPOST(MemberVO member) throws Exception {
/*
logger.info("join 진입");
//회원가입 서비스 실행
memberservice.memberJoin(member);
logger.info("join Service 성공");
*/
String rawPw = "";
String encodePw = "";
rawPw = member.getMemberPw();
encodePw = pwEncoder.encode(rawPw);
member.setMemberPw(encodePw);
memberservice.memberJoin(member);
return "redirect:/main";
}
회원가입 메서드와 같이 기존의 메서드 흐름부터 먼저 알아보겠습니다.
전체적인 흐름은 다음과 같습니다.
memberLogin(member)호출하여 반환받은 정보를 lvo 변수에 저장
=> (lvo == null) 작성
- true(lvo가 null인 경우) - 로그인 실패
- 리플렉션에 실패를 의미하는 데이터 저장
- 로그인 페이지 리다이렉트
- false(lvo가 null이 아닌 경우) - 로그인 성공
- 세션에 사용자의 정보 저장
- 메인 페이지로 리다이렉트
여기서 핵심은 memberLogin() 메서드입니다. 해당 메서드를 통해 select 쿼리가 실행이 되는데, 사용자로부터 제출받은 아이디와 비밀번호가 일치하는 사용자를 찾게 되면 해당 사용자의 정보를 반화하게 되고, 일치하는 사용자를 찾지 못하면 null을 반환하게 됩니다.
현재 데이터베이스에 저장된 비밀번호는 인코딩 된 데이터입니다. 따라서 아무리 사용자가 올바른 아이디와 비밀번호를 제출하더라도 memberLogin() 메서드는 null을 반환할 것입니다.
데이터베이스 자체에서 인코딩된 비밀번호와 사용자가 제출한 비밀번호가 일치하는지를 판단해준다면 좋겠지만 이는 불가능합니다. 그렇기 때문에 데이터베이스로부터 사용자의 비밀번호를 꺼내와서 BCryptPasswordEncoder 클래스의 matchers() 메서드를 활용하여 사용자가 제출한 비밀번호와 일치 여부를 확인해야 합니다. 일치 여부에 따라 로그인 성공 시 실행되어야 할 코드를 적용하던지, 로그인 실패 시 실행되어야 할 코드를 적용해야할 것 입니다.
회원의 정보를 반환하는 쿼리문 실행(사용자의 아이디를 조건으로 함)
=> 회원의 정보가 null인지 판단
- null인 경우 - 로그인 실패
- 로그인 실패 실행 코드
- null아닌 경우
- matchers()문을 통해 데이터베이스 저장된 비밀번호와 제출된 비밀번호 일치 여부 확인
- true인 경우 - 비밀번호 일치
로그인 성공 실행 코드- false인 경우 - 비밀번호 불일치
로그인 실패 실행 코드
새로운 쿼리를 만드는 것이 아니라 기존 작성해둔 로그인 쿼리를 활용 하기 위해서 일부 수정해주겠습니다. MemberMapper.xml에 있는 로그인 쿼리를 아래와 같이 수정합니다.
where문에 아이디만 확인하도록 하였고, 반환받는 데이터에 비밀번호도 추가하였습니다.
<select id="memberLogin" resultType="com.test.model.MemberVO">
select memberId, memberPw, memberName, adminck, money, point from book_member where memberId = #{memberId}
<!-- 기존코드 -->
<!-- select memberId, memberName, adminck, money, point from book_member where memberId = #{memberId} and memberPw = #{memberPw} -->
</select>
기존의 로그인 메서드의 구현부를 주석 처리하거나 지웁니다.
HttpSession session = request.getSession();
String rawPw = "";
String encodePw = "";
MemberVO lvo = memberservice.memberLogin(member); // 제출한 아이디와 일하는 아이디가 있는지
if (lvo != null) { // 일치하는 아이디 존재시
rawPw = member.getMemberPw(); // 사용자가 제출한 비밀번호
encodePw = lvo.getMemberPw(); // 데이터베이스에 저장한 인코딩된 비밀번호
if (true == pwEncoder.matches(rawPw, encodePw)) { // 비밀번호 일치여부 판단
lvo.setMemberPw(""); // 인코딩된 비밀번호 정보 지움
session.setAttribute("member", lvo); // session에 사용자의 정보 저장
return "redirect:/main"; // 메인페이지 이동
} else {
rttr.addFlashAttribute("result", 0);
return "redirect:/member/login"; // 로그인 페이지 이동
}
} else { // 일치하는 아이디가 존재하지 않을 시 (로그인 실패)
rttr.addFlashAttribute("result", 0);
return "redirect:/member/login"; // 로그인 페이지 이동
}
- 회원가입 데이터
- 올바른 데이터 입력시
- 올바르지 않은 데이터 입력시