본문 바로가기

Server Programming/Spring Boot Full-Stack Programming

[스프링 풀스택 클론 코딩 - 계정 설정] (2-11) 패스워드 찾기

반응형

1. AccountController의 emailLoginForm메서드, sendEmailLoginLink 메서드, loginByEmail 메서드 추가

   //패스워드 변경을 위한 이메일 로그인 연결
    @GetMapping("/email-login")
    public String emailLoginForm() {
        return "account/email-login";
    }

    //패스워드 변경을 위한 이메일 로그인
    @PostMapping("/email-login")
    public String sendEmailLoginLink(String email, Model model, RedirectAttributes attributes) {
        Account account = accountRepository.findByEmail(email);
        if (account == null) {
            model.addAttribute("error", "유효한 이메일 주소가 아닙니다.");
            return "account/email-login";
        }

        if (!account.canSendConfirmEmail()) {
            model.addAttribute("error", "이메일로 메일 발송은 1시간마다 한 번 사용 가능합니다.");
            //return "account/email-login";
        }
        accountService.sendLoginLink(account);
        attributes.addFlashAttribute("message", "이메일 인증 메일을 발송했습니다.");
        return "redirect:/email-login";
    }

    //이메일을 통한 링크로 패스워드 변경
    @GetMapping("/login-by-email")
    public String loginByEmail(String token, String email, Model model) {
        Account account = accountRepository.findByEmail(email);
        String view = "account/logged-in-by-email";
        //계정이 null이거나 유효한 토큰이 아닐 경우 -> 로그인 불가
        if (account == null || !account.isValidToken(token)) {
            model.addAttribute("error", "로그인할 수 없습니다.");
            return view;
        }
        //유효한 토큰일 경우 로그인 처리
        accountService.login(account);
        return view;
    }

2. AccountService의 sendLoginLink메서드 추가

public void sendLoginLink(Account account) {
    //토큰을 이메일로 로그인 가능하게 하는 토큰을 만든다.
    account.generateEmailCheckToken();
    SimpleMailMessage mailMessage = new SimpleMailMessage();
    mailMessage.setTo(account.getEmail());
    mailMessage.setSubject("Demo, 로그인 링크");
    mailMessage.setText("/login-by-email?token=" + account.getEmailCheckToken() +
            "&email=" + account.getEmail());
    javaMailSender.send(mailMessage);
}

3. 스프링 시큐리티 예외 추가

SecurityConfig 변경 전

@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		// 오버라이딩을 통해 원하는 요청은 시큐리티 체크를 하지 않도록 설정
		return http.authorizeRequests()
				// 모듈에서 걸러내야 하는 요청들
				.mvcMatchers("/", "/login", "/sign-up", "/check-email-token", "/email-login", "/check-email-login",
						"login-link","/login-by-email")
				// 재전송 기능 만들때에는 "/check-email" permitall에서 제외, 로그인한 사용자만 접근해야하므로
				.permitAll()
//                .mvcMatchers("/", "/login", "/sign-up", "/check-email", "/check-email-token",
//                        "/email-login", "/check-email-login", "login-link", "/profile/*").permitAll()
				// -> 프로필에 붙어있는 모든 요청은 get만 허용
				.mvcMatchers(HttpMethod.GET, "/profile/*").permitAll()

				// 그외에 나머지 요청의 경우 모두 로그인이 필요하다.
				.anyRequest().authenticated()

				// 스프링 시큐리티가 제공하는 로그인 페이지 그대로 사용한다면
				// http.formLogin();

				// 커스텀한 로그인 페이지를 통해 로그인 처리를 한다면
				.and().formLogin().loginPage("/login").permitAll()
				// permitAll을 주는 이유는, 로그인한 사용자도, 로그인 안한 사용자도 접근 가능하도록 하기 위해

				// 로그아웃 처리를 위해 -> 로그아웃 성공시 index 페이지로 이동
				.and().logout().logoutSuccessUrl("/").and()
				// 로그인 기억처리를 위해 -> userDetails에서 토큰을 가져온다. -> 빈을 이용한 의존성 주입
				// 토큰값을 읽어오고 저장하는 인터페이스 필요 -> tokenRepository()
				.rememberMe().userDetailsService(accountService).tokenRepository(tokenRepository()).and().build();
		// 키만을 이용해 인증 (안전하지않음)
		// .rememberMe().key("sdfsfsdfsd");
	}

 

변경 후 (/check-email-login 추가)

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		// 오버라이딩을 통해 원하는 요청은 시큐리티 체크를 하지 않도록 설정
		return http.authorizeRequests()
				// 모듈에서 걸러내야 하는 요청들
				.mvcMatchers("/", "/login", "/sign-up", "/check-email-token",
						"/email-login", "/check-email-login", "/login-link", "/login-by-email")
				// 재전송 기능 만들때에는 "/check-email" permitall에서 제외, 로그인한 사용자만 접근해야하므로
				.permitAll()
//                .mvcMatchers("/", "/login", "/sign-up", "/check-email", "/check-email-token",
//                        "/email-login", "/check-email-login", "login-link", "/profile/*").permitAll()
				// -> 프로필에 붙어있는 모든 요청은 get만 허용
				.mvcMatchers(HttpMethod.GET, "/profile/*").permitAll()

				// 그외에 나머지 요청의 경우 모두 로그인이 필요하다.
				.anyRequest().authenticated()

				// 스프링 시큐리티가 제공하는 로그인 페이지 그대로 사용한다면
				// http.formLogin();

				// 커스텀한 로그인 페이지를 통해 로그인 처리를 한다면
				.and().formLogin().loginPage("/login").permitAll()
				// permitAll을 주는 이유는, 로그인한 사용자도, 로그인 안한 사용자도 접근 가능하도록 하기 위해

				// 로그아웃 처리를 위해 -> 로그아웃 성공시 index 페이지로 이동
				.and().logout().logoutSuccessUrl("/").and()
				// 로그인 기억처리를 위해 -> userDetails에서 토큰을 가져온다. -> 빈을 이용한 의존성 주입
				// 토큰값을 읽어오고 저장하는 인터페이스 필요 -> tokenRepository()
				.rememberMe().userDetailsService(accountService).tokenRepository(tokenRepository()).and().build();
		// 키만을 이용해 인증 (안전하지않음)
		// .rememberMe().key("sdfsfsdfsd");
	}

 

4. email-login 페이지, logged-in-by-email 페이지 추가

 

email-login

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments.html :: head"></head>
<body class="bg-light">
    <div th:replace="fragments.html :: main-nav"></div>
    <div class="container">
        <div class="py-5 text-center">
            <p class="lead">데모</p>
            <h2>패스워드 재설정</h2>
        </div>
        <div class="row justify-content-center">
            <div th:if="${error}" class="alert alert-danger alert-dismissible fade show mt-3" role="alert">
                <span th:text="${error}">완료</span>
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>

            <div th:if="${message}" class="alert alert-info alert-dismissible fade show mt-3" role="alert">
                <span th:text="${message}">완료</span>
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>

            <form class="needs-validation col-sm-6" action="#" th:action="@{/email-login}" method="post" novalidate>
                <div class="form-group">
                    <label for="email">가입 할 때 사용한 이메일</label>
                    <input id="email" type="email" name="email" class="form-control"
                           placeholder="your@email.com" aria-describedby="emailHelp" required>
                    <small id="emailHelp" class="form-text text-muted">
                        가입할 때 사용한 이메일을 입력하세요.
                    </small>
                    <small class="invalid-feedback">이메일을 입력하세요.</small>
                </div>

                <div class="form-group">
                    <button class="btn btn-success btn-block" type="submit"
                            aria-describedby="submitHelp">로그인 링크 보내기</button>
                    <small id="submitHelp" class="form-text text-muted">
                        데모에 처음 오신거라면 <a href="#" th:href="@{/findpassword}">계정을 먼저 만드세요.</a>
                    </small>
                </div>
            </form>
        </div>

        <div th:replace="fragments.html :: footer"></div>
    </div>
    <script th:replace="fragments.html :: form-validation"></script>
</body>
</html>

 

logged-in-by-email

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments.html :: head"></head>
<body class="bg-light">
    <nav th:replace="fragments.html :: main-nav"></nav>

    <div class="container">
        <div class="py-5 text-center" th:if="${error}">
            <p class="lead">데모 패스워드 찾기</p>
            <div  class="alert alert-danger" role="alert" th:text="${error}">
                로그인 할 수 없습니다.
            </div>
        </div>

        <div class="py-5 text-center" th:if="${error == null}">
            <p class="lead">데모 패스워드 찾기</p>
            <h2>이메일을 통해 임시 로그인 했습니다. <a th:href="@{/settings/password}">패스워드를 변경</a>하세요.</h2>
        </div>
    </div>
</body>
</html>
반응형