4/24/2018

(구버전) 스프링 게시판 만들기 #17. 패스워드 암호화 구현

'(구버전) 스프링 게시판 만들기'는 내용이 부족하다고 판단하여
스프링 게시판 만들기를 새로 작성하였습니다.

링크 및 참조용으로 현재 게시물은 남겨두겠지만,
가급적이면 새로운 스프링 게시판 만들기를 참조해주시기 바랍니다.

지금까지 회원정보(아이디, 패스워드. 닉네임)은 입력한 텍스트 그대로 저장이 되었었습니다. 아무런 일이 안생기면 별문제 없겠지만, 만약이라도 데이터 베이스가 저장된게 노출된다면 큰 문제입니다.

회원정보가 노출되면, 다른 사이트에 같은 정보가 가입한 경우도 있을 수 있으므로 큰 문제가 발생됩니다.

신비로운 기술(?)을 사용하여 절대 노출되지 않는 완벽한 보안 기술이 생긴다면 좋겠지만, 그러기엔 매우 힘드니, 관리자는 최대한 보안에 힘써야하며, 그 방법중 하나가 암호화입니다.

입력한 문자를 특정 공식에 의해 완전히 다른 문자로 바꿔서, 만약이라도 정보가 노출될 경우 확인하기 어렵게 할 수 있습니다. 암호화를 한번만 하는게 아니라, 여러번하게되면 더욱 확인하기 어려워집니다.

먼저 pom.xml에 암호화 관련 라이브러리를 추가합니다.

메이븐 리포지터리에서 스프링 세큐리티를 검색하면 나오는 결과의 core, web, config를 같은 버전으로 추가합니다.

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.0.8.RELEASE</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.0.8.RELEASE</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.0.8.RELEASE</version>
</dependency>

web.xml의 contextConfigLocation에 스프링 세큐리티(spring security)를 설정해주는 xml파일의 경로를 추가합니다.

/WEB-INF/spring/spring-security.xml

이제 web.xml에 적었던 경로(/WEB-INF/spring/)에 spring-security.xml 파일을 생성하고 내부에 코드를 추가합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd">
      
    <beans:bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" /> 
</beans:beans>

이제 맴버 컨트롤러(MemberController)에서 의존성 주입하여, 암호화를 사용할 수 있습니다.

@Autowired
BCryptPasswordEncoder passEncoder;

회원 가입 메서드에서 암호화를 합니다.

회원 가입시 입력하는 MemberVO에서, 패스워드(getUserPass)만 받아와서 encode로 암호화한뒤, 다시 패스워드(setUserPass)에 넣어줍니다.

@Autowired
BCryptPasswordEncoder passEncoder;

이제 회원가입을 합니다. 비밀번호는 1111로 했습니다.

패스워드 부분이 입력한 1111이 그대로 저장되지 않고 암호화되어 저장되었습니다.

암호화되면 문자열의 길이가 상당히 길어지므로, 테이블의 해당 컬럼의 크기가 작다면 변경할 필요가 있습니다.

이제 로그인하려고하면, 로그인에 실패합니다.

왜냐하면 입력한 비밀번호는 1111인데, 데이터 베이스에 저장된 비밀번호는 1111이 아닌 암호화된 1111이니까요. 그러므로 암호화된 1111을 해독하여 원래의 1111로 되돌려야합니다. 그런데 이번에 사용한 BCrypt 암호화 방식은 같은 텍스트를 입력하더라도 시간에 따라서 다른 결과를 출력합니다.

matches 메서드를 사용하면 사용자가 로그인하기 위해 입력한 값과 데이터 베이스에 저장된 값을 비교할 수 있습니다.

로그인하고난 뒤 정보를 가져올 쿼리를 작성합니다.

이전에 사용했던 쿼리는 사용자가 입력한 아이디와 패스워드 두가지가 모두 맞아야 값을 가져올 수 있었는데, 이번엔 아이디만으로 값을 가져옵니다.

쿼리가 정상적으로 작동한다면 매퍼에 추가합니다.

<!-- 로그인 - 암호화 적용 -->
<select id="loginBcrypt" resultType="com.kuzuro.domain.MemberVO">
 select
  userId, userName, userPass
 from
  myMember
 where userId = #{userId}
</select>

기존에 있던 로그인 DAO에서 매퍼 아이디 부분만 수정합니다.

맴버 컨트롤러(MemberController)의 로그인 메서드를 수정합니다.

// 로그인
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(MemberVO vo, HttpServletRequest req, RedirectAttributes rttr) throws Exception {
 logger.info("post login");
 
 HttpSession session = req.getSession();
 MemberVO login = service.login(vo);
 
 boolean passMatch = passEncoder.matches(vo.getUserPass(), login.getUserPass());
 
 if(login != null && passMatch) {
 
  session.setAttribute("member", login);
  
 } else {
  session.setAttribute("member", null);
  rttr.addFlashAttribute("msg", false);
 }  
 
 /* if(login == null) {
  session.setAttribute("member", null);
  rttr.addFlashAttribute("msg", false);
 } else {
  session.setAttribute("member", login);
 } */
   
 return "redirect:/";
}

service.login(vo) 로 인해 입력한 아이디와 매칭되는 값을 데이터에서 찾아 리턴합니다. 그리고 passEncoder.matches() 메서드는 입력한 패스워드와 저장된 패스워드를 비교하고, 같다면 true 틀리면 false를 리턴합니다.

아이디와 매칭되는 데이터가 있으며, 입력한 패스워드가 저장된 패스워드와 일치 될 경우 if문 내부가 실행됩니다. 아이디가 매칭되지 않거나, 입력한 패스워드가 저장된 패스워드가 일치하지 않으면 else문 내부가 실행됩니다.

이제 프로젝트를 다시 실행하고 로그인을 시도합니다.

이제 로그인이 정상적으로 이루어집니다.

회원가입할 때 패스워드가 암호화가 되었으므로, 회원 정보 수정이나 회원 탈퇴에서도 matches 메서드를 사용해서 비교하도록 해야합니다.

게시물 수정
  1. 예제 그대로 라이브러리 추가하고 빈 생성하고 객체주입까지 했는데 어노테이션 걸어 놓은 bcrypt 객체를 찾을 수 없다면서 작동을 안 하네요 ㅠㅠㅠ 어디서 꼬인 걸까요.. 프로젝트 프로퍼티 보니까 디펜던시에 넣어놓은 4개 다 들어가 있고, 웹xml도 제대로 넣은 것 같은데 말이죠 ㅠㅠ

    답글삭제
    답글
    1. 안녕하세요?

      오타가 있는게 아니라면 문제없이 적용될텐데 이상하네요. 혹시 오타가 있는지 확인해보시거나, 본문에 있는 코드를 그대로 복사해서 사용해보시기 바랍니다.

      삭제
  2. 익명7/10/2019

    1번부터 정독하고 소스 받아서 해보고있는데
    No mapping found for HTTP request with URI [/board/listSearch] in DispatcherServlet with name 'appServlet'이런 오류가뜨는데
    어떻게 잡나요...?

    답글삭제
    답글
    1. 안녕하세요?

      에러 메시지를 보니 /board/listSearch가 보이는데요, 컨트롤러에 제대로 매핑이 되었는지 확인해보시기 바랍니다. 또, 서버의 컨텍스트가 /controller 처럼 처음 지정한 패키지로 되어있다면 /로 바꿔주시기 바랍니다.

      코드를 처음부터 직접 작성할 때는 특별한 문제가 없겠지만, 다른 코드를 받아서 하는 경우 설정등의 차이가 있어서 에러가 발생하기 쉽습니다.

      삭제
  3. spring-security.xml 에
    <benas:beans 부분에서 계속 빨간밑줄이있네요...
    3일정도 여기저기 찾아보다가 해결책을 못찾아서 댓글달아봅니다.... ㅜㅜ

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      spring-security.xml을 더블클릭해서 열어보면 하단에 namespace라는게 있을텐데
      거기에서 beans가 체크 해제 되어있나 확인해보셔야할 것 같습니다.

      그래도 안된다면, 빨간줄에 마우스를 올려놓고 기다리면 어떤게 문제인지 나옵니다. 그걸로도 찾아보시고..

      상단 메뉴에서 [ prorject - clean ], 패키지 익스플로러(package explorer)에서 지금 작업하시는 프로젝트를 우클릭하시고 [ maven - update project ] 를 해보시면... 아마 되지 않을까나 예상해봅니다.

      삭제
  4. 여기까지 전부 완성한뒤 암호화 이후 회원수정과 회원탈퇴까지 추가해서 만들었습니다.
    궁금한 점이 있어서 질문 드립니다.

    회원가입에서 만들어진 'asd' 계정은 로그인과 비밀번호 변경, 회원탈퇴까지 가능한 상황인데
    정작 db에 없는 계정인 'qwe' 계정에 비밀번호 아무거나 입력하면 NullPointerException이 발생합니다.
    boolean passMatch = passEncoder.matches(vo.getUserPw(), login.getUserPw());
    이 코드에서 발생하는데 db에 없는 계정으로 로그인시에 '존재하지 않는 계정이거나 비밀번호가 틀립니다'라는 알림이 나오게 하려면 어떻게 해야할까요?

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      본문에 있는 로그인 방식은
      첫번째로 사용자가 입력한 아이디와 매칭되는 행이 db에 있나 확인 한 뒤,
      두번째로 존재할 경우 비밀번호를 매칭하는 방식입니다.

      본문에서는 첫번째 단계의 값이 null, 즉 매칭되는게 없고 두번째 단계의 비밀번호 매칭이 안될 경우
      rttr.addFlashAttribute("msg", false);값을 프론트로 전달함으로서 말씀하신 기능을 구현하고 있습니다.


      말씀하신대로 존재하지 않는 계정을 입력했을 때 에러가 발생했다면, 로직의 순서나 조건문이 잘못되었을걸로 예상됩니다.

      삭제
    2. 익명7/12/2022

      저도 같은 에러가 뜨네요 해결하셨으면 방법좀 부탁드립니다 ㅠㅠ

      삭제
    3. 많이 늦었지만, 저도 같은 문제를 겪었는데 login이 null 아니라는 부분에서 조건문을 한번 더 작성하면 해결이 됐습니다.

      // 로그인 POST
      @RequestMapping(value = "/login", method = RequestMethod.POST)
      public String login(User user, HttpServletRequest req, RedirectAttributes rttr) throws Exception {
      logger.info("post login");
      HttpSession session = req.getSession();
      User login = userDAOImpl.login(user);

      if (login != null) {
      // 기존 암호와 맞는지
      boolean passMatch = passwordEncoder.matches(user.getUserPass(), login.getUserPass());

      System.out.println(passMatch);

      if (passMatch) {
      session.setAttribute("user", login);
      return "redirect:/";
      } else {
      session.setAttribute("user", null);
      rttr.addFlashAttribute("msg", false);
      return "redirect:/login";
      }
      } else {
      session.setAttribute("user", null);
      rttr.addFlashAttribute("msg", false);
      return "redirect:/login";
      }
      }

      삭제
  5. 쇼핑몰 구게시판 신게시판 완주했습니다 에러인부분도 있었찌만 그럭저럭 완주했습니다 감사합니다

    답글삭제
  6. 감사합니다 덕분에 한달 정도 걸렸지만 완주 했네요 ~ 코드가 초보자가 이해하기 직관적이라 저한테 더 도움이 되었어요 ~~~

    혹시 스프링 시큐리티를 스프링 부트 open JDK11 환경에서 사용하시는 분들은 스프링 자바 폴더에 config 폴더를 아래와 같이 만들고 이렇게 설정을 하면 위 userController는 손댈 필요가 없더라구요.

    package com.study.practice.config;

    import com.study.practice.service.UserDAOImpl;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {



    // 비밀번호 암호화
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http
    .csrf().disable()
    .formLogin().disable()
    .authorizeRequests()
    .antMatchers("/**", "/login", "/register").permitAll()
    .anyRequest().authenticated()
    .and();
    }
    }

    답글삭제
    답글
    1. 해당 코드는 spring-boot-starter-security 디팬던시를 기준으로 작성했습니다.

      삭제