[스프링부트+JPA+타임리프+스프링 시큐리티] 비밀번호 확인 후 회원 정보 수정

jyleever·2022년 5월 14일
0

원래는 아이디, 닉네임, 비밀번호 각각 수정하려고 했지만 어째선지 별 노력을 해도 각각 수정했을 때 DB에는 회원 정보가 변경됐지만 변경된 세션은 등록되지 않았다 ^-^.. 자꾸 로그아웃이 됐다....하.......
그래서 일단 닉네임과 비밀번호를 한꺼번에 수정하도록 구현했다.

회원 정보 수정 과정

  1. 회원 수정을 클릭한다.
  2. 비밀번호를 입력하여 일치해야만 회원 수정 페이지로 진입한다.
  3. 닉네임과 비밀번호를 수정할 수 있다. 이 때 유효성 검사를 진행한다.
    • 중복 검사는 서버를 통해 검사한다.
  4. put메소드로 ajax 통신을 통해 회원 수정 컨트롤러에 진입한다.
  5. 회원 수정이 완료되면 세션이 변경되고 내 정보 페이지로 반환된다.

비밀번호 일치 확인

MemberController - checkPwd

    /** 회원 수정하기 전 비밀번호 확인 **/
    @GetMapping("/checkPwd")
    public String checkPwdView(){
        return "member/check-pwd";
    }

check-pwd.html

...
<div class="card-body">
	<div class="text-start">
		<input type="hidden" th:name="_csrf" th:value="${_csrf.token}"/>
			<div class="input-group input-group-outline my-3">
				<label class="form-label">비밀번호 확인</label>
				<input type="password" id="password" name="password" class="form-control">
			</div>
	</div>
<div class="text-center">
<button class="btn bg-gradient-primary w-100 my-4 mb-2" id="checkPwd"> 비밀번호 확인</button>
...
<script>
    $('#checkPwd').click(function() {
        const checkPassword = $('#password').val();
        if(!checkPassword || checkPassword.trim() === ""){
            alert("비밀번호를 입력하세요.");
        } else{
            $.ajax({
                type: 'GET',
                url: '/rest/checkPwd',
                data: {'checkPassword': checkPassword},
                datatype: "text"
            }).done(function(result){
                console.log(result);
                if(result){
                    console.log("비밀번호 일치");
                    window.location.href="/settings/update";
                } else if(!result){
                    console.log("비밀번호 틀림");
                    // 비밀번호가 일치하지 않으면
                    alert("비밀번호가 맞지 않습니다.");
                    window.location.reload();
                }
            }).fail(function(error){
                alert(JSON.stringify(error));
            })
        }
    });
</script>
  • 입력받은 비밀번호를 rest/checkPwd 컨트롤러에 GET 방식으로 전송하여 결괏값을 반환받는다.
  • 결과가 true이면 비밀번호가 일치하므로 회원 수정 페이지로 이동한다
  • 결과가 false이면 비밀번호가 틀렸다는 메시지를 전달하고 페이지를 reload 한다.

MemberRestController - checkPwd

   /** 회원 수정 전 비밀번호 확인 **/
    @GetMapping("/checkPwd")
    public boolean checkPassword(@AuthenticationPrincipal UserAdapter user,
                                @RequestParam String checkPassword,
                                Model model){

        log.info("checkPwd 진입");
        Long member_id = user.getMemberDto().getId();

        return memberService.checkPassword(member_id, checkPassword);
    }
  • 비밀번호 확인을 요청한 유저(로그인 유저)의 비밀번호와 입력 받은 비밀번호를 memberService에 파라미터로 넘긴다.

MemberService - checkPassword

    /** 비밀번호 일치 확인 **/
    @Override
    public boolean checkPassword(Long member_id, String checkPassword) {
        Member member = memberRepository.findById(member_id).orElseThrow(() ->
                new IllegalArgumentException("해당 회원이 존재하지 않습니다."));
        String realPassword = member.getPassword();
        boolean matches = encoder.matches(checkPassword, realPassword);
        return matches;
    }
  • BCryptPasswordEncoder의 matches를 이용하여 비밀번호가 일치하는지 확인한다.

회원 수정

member-update.html

  • 비밀번호가 일치했을 때 보여줄 회원 수정 페이지
<div class="col-lg-6">
	<div class="row justify-content-start">
		<form style="margin: auto" th:object="${member}">
		<input type="hidden" th:name="_csrf" th:value="${_csrf.token}"/>
		<input type="hidden" th:id="userId" th:value="*{id}">
		<h3>아이디</h3>
		<div class="input-group input-group-static mb-4">
			<label style="font-weight: bold" th:for="username">아이디</label>
			<input type="text" th:field="*{username}" th:value="*{username}" class="form-control"
               			   th:id="username" readonly>
		</div>
		<h3>이메일</h3>
		<div class="input-group input-group-static mb-4">
			<label style="font-weight: bold" th:for="email">아이디</label>
			<input type="text" th:field="*{email}" th:value="*{email}" class="form-control"
							th:id="email" readonly>
		</div>
		<h3>닉네임 변경</h3>
		<div class="input-group input-group-static mb-4">
			<label style="font-weight: bold" th:for="username">기존 닉네임</label>
			<input type="text" th:field="*{nickname}" th:value="*{nickname}" class="form-control"
                                           th:id="nickname" readonly>
		</div>
                                <div class="input-group input-group-static mb-4">
                                    <label style="font-weight: bold" th:for="nickname">변경할 닉네임 입력</label>
                                    <input type="text" th:id="newNickname" th:name="newNickname"
                                           placeholder="변경할 닉네임을 입력하세요." class="form-control" >
                                </div>
		<h3>비밀번호 변경</h3>
		<div class="input-group input-group-static mb-4">
			<label style="font-weight: bold" th:for="password">변경할 비밀번호 입력</label>
			<input type="password" th:id="password" th:name="password"
					placeholder="변경할 비밀번호를 입력하세요." class="form-control" >
		</div>
		</form>
		<button id="memberUpdate" class="btn bg-gradient-primary" value="회원 정보 수정"
		th:onclick="memberUpdate()">회원 정보 수정</button>

	</div>
</div>

memberUpdate - js

<script>
    function memberUpdate(){
        const data = {
            id: $('#userId').val(),
            username: $('#username').val(),
            nickname: $('#newNickname').val(),
            password: $('#password').val()
        };

        // 공백 및 빈 문자열 체크
        if(!data.nickname || data.nickname.trim() === "" || !data.password || data.password.trim() === ""){
            alert("공백 또는 입력하지 않은 부분이 있습니다.");
            return false;
        }
        // 유효성 검사
        else if(!/^[가-힣a-zA-Z0-9]{2,10}$/.test(data.nickname)){
            alert("닉네임은 특수문자를 포함하지 않은 2~10자리여야 합니다.");
            $('#newNickname').focus();
            return false;
        }
        else if(!/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,16}/.test(data.password)){
            alert("비밀번호는 8~16자리수여야 합니다. 영문 대소문자, 숫자, 특수문자를 1개 이상 포함해야 합니다.");
            $('#password').focus();
            return false;
        }

        const confirmCheck = confirm("수정하시겠습니까?");

        if(confirmCheck == true){
            $.ajax({
                type: 'PUT',
                url: '/rest/member',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify(data)
            }).done(function(result){
                if(result){
                    alert("회원 수정이 완료되었습니다.");
                    window.location.href="/mypage";
                } else{
                    alert("이미 사용 중인 닉네임입니다. 다시 입력해주세요.");
                    $('#newNickname').focus();
                }
            }).fail(function(error){
                alert(JSON.stringify(error));
            });
        }
    }
</script>
  • 유효성 검사를 통과했을 때 rest/member 컨트롤러에 put 메소드로 ajax 통신을 진행한다.
  • 컨트롤러가 반환한 값이 true인 경우 회원 수정이 완료됐으므로 내 정보 페이지로 이동시킨다.
    컨트롤러가 반환한 결괏값이 false인 경우는 닉네임 중복 검사를 통과하지 못 한 (닉네임이 중복된 경우) 경우 이므로 현재 페이지에 머물고 닉네임 임력 칸에 focus를 둔다.

MemberRestController

    /** 회원 정보 수정 **/
    @PutMapping("/member")
    public boolean update(@RequestBody MemberDto.RequestDto dto) {

        log.info("MemberRestController 진입");

        if(memberService.checkNickname(dto.getId(), dto.getNickname())){
            log.info("중복 닉네임");
            return false;
        } else{
            log.info("사용 가능한 닉네임");

            // 회원 정보 수정
            memberService.userInfoUpdate(dto);

            /** ========== 변경된 세션 등록 ========== **/
            /* 1. 새로운 UsernamePasswordAuthenticationToken 생성하여 AuthenticationManager 을 이용해 등록 */
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword())
            );

            /* 2. SecurityContextHolder 안에 있는 Context를 호출해 변경된 Authentication으로 설정 */
            SecurityContextHolder.getContext().setAuthentication(authentication);
            return true;
        }
    }
  • memberService의 checkNickname 메소드를 호출하여 중복 닉네임인지 확인
  • 사용 가능한 닉네임인 경우 memberService의 userInfoUpdate메소드를 호출하여 회원 정보 dto를 파라미터로 전달해 DB에서 회원 정보 변경
  • 회원 정보 수정 후 변경된 세션 등록

MemberService

    /** 닉네임 중복 체크 **/
    @Override
    public boolean checkNickname(Long member_id, String nickname) {

        if(memberRepository.existsByNickname(nickname)){
            if(memberRepository.findByNickname(nickname).getId() == member_id){
                // 입력 받은 닉네임의 회원 id와 일치한다면 즉, 현재 닉네임을 그대로 입력한 경우
                return false;
            } else{
                // 다른 사람이 사용하고 있는 닉네임이라면
                return true;
            }
        } else{
            // 중복된 닉네임이 아니라면
            return false;
        }
    }

    /** 회원 수정 **/
    @Override
    public void userInfoUpdate(MemberDto.RequestDto memberDto) {

        /* 회원 찾기 */
        Member member = memberRepository.findById(memberDto.toEntity().getId()).orElseThrow(() ->
                new IllegalArgumentException("해당 회원이 존재하지 않습니다."));

        /* 수정한 비밀번호 암호화 */
        String encryptPassword = encoder.encode(memberDto.getPassword());
        member.update(memberDto.getNickname(), encryptPassword); // 회원 수정

        log.info("회원 수정 성공");
    }
  • 닉네임이 중복됐는지 체크하고 등록된 닉네임이 아니라면 member 엔티티의 update 메소드를 호출하여 회원을 수정한다.
  • 엔티티를 변경하는 기능이므로 member 엔티티에 메소드를 정의해두었다.
    public void update(String nickname, String password){
        this.nickname = nickname;
        this.password = password;
    }
  • 정보 수정을 완료하면 DB에는 데이터가 변경되지만 재로그인을 해야 변경된 회원 정보가 화면에 나타난다.
    따라서 회원 정보를 변경하고 변경된 세션을 등록하여 따로 로그아웃하지 않고도 변경된 정보가 현재 세션에 반영될 수 있도록 한다.

세션 변경

MemberRestController

/** ========== 변경된 세션 등록 ========== **/
/* 1. 새로운 UsernamePasswordAuthenticationToken 생성하여 AuthenticationManager 을 이용해 등록 */
Authentication authentication = authenticationManager.authenticate(
         new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword())
);

/* 2. SecurityContextHolder 안에 있는 Context를 호출해 변경된 Authentication으로 설정 */
SecurityContextHolder.getContext().setAuthentication(authentication);
  • 변경된 세션을 등록하기 위해 새로운 UsernamePasswordAuthenticationToken을 만들고 AuthenticationManager를 사용해 등록해준다.
  • SecurityContextHolder 안에 있는 Context를 호출해 변경된  Authentication을 설정해준다.

이 때, AuthenticationManager 을 사용하기 위해 SecurityConfig에서 bean으로 등록해준다.

SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
    ...

    /** AuthenticationManager 빈 등록 **/
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

0개의 댓글