본문 바로가기

Server Programming/Spring Boot Full-Stack Programming

[스프링 풀스택 클론 코딩 - 회원가입] (1-18) 로그인 로그아웃

반응형

로그인 처리는 스프링 시큐리티를 이용


login

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head th:replace="fragments.html :: head"></head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>

<body>
	<nav th:replace="fragments.html :: main-nav"></nav>
	<div class="container">
		<!--패딩 1~5 -->
		<div class="py-5 text-center">
			<p class="lead"> 데모</p>
			<h2>로그인</h2>

			

		</div>
		<!-- 폼을 감쌀 영역-->
		<!-- 로그인을 처리하는 핸들러를 직접 작성할 필요는 없지만, 유저정보를 전달하는 인터페이스는 구현해야한다.-->
<div class="row justify-content-center">
			<!--파라미터에 에러가 존재한다면, 출력 -->
            <div th:if="${param.error}" class="alert alert-danger" role="alert">
				<p>이메일(또는 닉네임)과 패스워드가 정확하지 않습니다.</p>
				<p>또는 확인되지 않은 이메일을 사용했습니다. 이메일을 확인해 주세요.</p>
				<p>
					확인 후 다시 입력하시거나, <a href="#" th:href="@{/find-password}"> 패스워드 찾기</a>를 이용하세요.
				</p>
			</div>
			<!--form 작성 -->
			<form class="needs-validation col-sm-6" action="#" th:action="@{/login}" method="post" novalidate>
				<div class="form-group">
					<label for="username">이메일 또는 닉네임</label>
					<input id="username" type="text" name="username" 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">
					<label for="password">패스워드</label>
					<input id="password" type="password" name="password" class="form-control"
						aria-describedby="passwordHelp" required>
					<small id="passwordHelp" class="form-text text-muted">
						패스워드가 기억나지 않는다면, <a href="#" th:href="@{/email-login}">패스워드 없이 로그인하기</a>
					</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="@{/sign-up}">계정을 먼저 만드세요.</a>
                    </small>
                </div>
			</form>
		</div>
		<div th:replace="fragments.html :: footer"></div>
	</div>
	<script th:replace="fragments.html :: form-validation"></script>

</body>

</html>

 

SecurityConfig

package com.demo.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;

import lombok.RequiredArgsConstructor;

//회원가입을 위한 시큐리티 수동 설정
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

//    private final AccountService accountService;
//    private final DataSource dataSource;

	/**
	 * Spring Security 5.7.x 부터 WebSecurityConfigurerAdapter 는 Deprecated. ->
	 * SecurityFilterChain, WebSecurityCustomizer 를 상황에 따라 빈으로 등록해 사용한다.
	 */
	@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")
				//재전송 기능 만들때에는 "/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().build();
//                
//                .and()
//                .rememberMe().userDetailsService(accountService).tokenRepository(tokenRepository())
//		  		  .and().build();
//                
	}


	@Bean
	public WebSecurityCustomizer webSecurityCustomizer() {
		// static리소스도 요청을 거부하지 않는다.
		return (web) -> web.ignoring().mvcMatchers("/node_modules/**")
				.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
	}
}

 

MainController

package com.demo.main;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import com.demo.account.CurrentUser;
import com.demo.domain.Account;

@Controller
//첫 페이지로 가는 요청 핸들러 작성

public class MainController {

	//스프링이 제공하는 @AuthenticationPrincipal를 동적으로 사용하기 위해
	//익명일 경우 null로 익명이 아닐 경우 account 객체로 사용
	
	@GetMapping("/")
	public String home(@CurrentUser Account account, Model model) {
		//인증한 사용자인 경우
		if(account !=null) {
			model.addAttribute(account);
		}
		return "index";
		//login 메서드에는 account라는 프로퍼티가 없기 때문에 중간 매개자 UserAccount 작성
		//도메인의 유저정보와 스프링 시큐리티 유저정보의 연결고리
	}
	
	//로그인의 경우 메인 컨트롤러에 작성 -> 뷰 컨트롤러를 이용한다면 단축 가능
	@GetMapping("/login")
	public String login() {
		return "login";
		//로그인 요청시 로그인 뷰 리턴
	}
}

 

AccountService에서 UserDetailsService에서 상속받아 오버라이딩해서 구현 [따라서 extends가 아닌 implements]

package com.demo.account;

import java.util.List;

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.demo.domain.Account;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AccountService implements UserDetailsService {

	@Override
	public UserDetails loadUserByUsername(String emailOrNickname) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		//이메일 또는 닉네임을 이용한 로그인 처리를 위해 필요한 인터페이스 작성
		Account account= accountRepository.findByEmail(emailOrNickname);
		if(account==null) {
			account= accountRepository.findByNickname(emailOrNickname);
		}
		if(account==null) {
			throw new UsernameNotFoundException(emailOrNickname);
			//닉네임이나 이메일이 잘못되었다.
		}
		//principal에 해당하는 객체 리턴
		return new UserAccount(account);
		//자동으로 빈을 가져다 사용한다.
	}
}

 

Nickname을 이용해 계정을 찾는 메서드 만들기

 

AccountRepository

package com.demo.account;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;

import com.demo.domain.Account;

//해당 클래스를 인터페이스로 만든다. -> Account에서, ID타입으로 조회
//기본적으로 write를 안쓰고 읽기만 하게해서 메모리 사용량을 최적화
@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {

	boolean existsByEmail(String email);

	boolean existsByNickname(String nickname);

	Account findByEmail(String email);

	Account findByNickname(String nickname);

}

 

 

반응형