Spring Boot

[Spring Boot] 로그인 기능 구현 (0) - 공통 기능, 코드 구현

공대생안씨 2024. 1. 7. 00:19

0. 상황 설명

  • 여러가지 방법을 사용해서 로그인 기능을 구현하려 함
  • 공통인 기능과 코드는 이 게시글에서 모두 정리
  • 방법마다 다른 코드는 각 게시물에서 따로 정리

 

* 구현한 로그인 기능 프로젝트 *

 

1. 프로젝트

  • 버전 정보 / DB
    • 스프링부트 3.1.5
    • gradle
    • Java 21
    • DB : MySQL
  • 구현 기능
    • 로그인 이전 화면 
      • 회원가입
        • ID, 비밀번호, 이름 입력해서 회원가입 (하나라도 값이 없다면 에러메시지 출력 => 회원가입 불가)
        • ID 중복 시 에러메시지 출력 => 회원가입 불가
        • 체크 용 비밀번호가 다를 시 에러메시지 출력 => 회원가입 불가
      • 로그인
        • ID, 비밀번호 입력해서 로그인
        • ID와 비밀번호가 일치하지 않으면 에러메시지 출력 => 로그인 불가
    • 로그인 이후 화면
      • 아래의 모든 기능에 로그인 하지 않고 접근 시 로그인 화면으로 이동됨 
      • 마이페이지 
        • 로그인 된 계정의 아이디, 이름, memberRole (ADMIN / USER) 가 화면에 출력
          • 회원가입 시 기본 memberRole : USER
      • 관리자 전용
        • memberRole : ADMIN인 계정으로 접근 시 접근 가능
        • memberRole : USER인 계정으로 접근 시 홈 화면으로 돌아감 => 접근 불가
      • 로그아웃

 

2. 공통 의존관계

  • build.gradle
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'

	runtimeOnly "com.mysql:mysql-connector-j"

	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

  • application.yml
spring:
  # DB 접속 정보 설정 => localhost:3306
  datasource:
    url: jdbc:mysql://localhost:3306/[DB이름]?serverTimezone=Asia/Seoul
    username: [username]
    password: [password]
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  # JPA 설정
  jpa:
    hibernate:
      ddl-auto: create # application 첫 실행 시에만 create, 이후에는 none 혹은 주석처리
    show-sql: true

 

3. domain

  • Member (엔티티)
@Entity
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="member_id")
    private Long id;

    private String loginId;
    private String password;
    private String name;

    private MemberRole role;
}

 

  • MemberRole (Enum)
public enum MemberRole {
    USER, ADMIN
}

 

  • dto > JoinRequest
    • 회원가입 시 데이터 받는 폼
    • @NotBlank(message="메세지") : 빈칸 허용 x -> 빈칸일 때 에러메시지 출력
@Getter @Setter
@NoArgsConstructor
public class JoinRequest {

    @NotBlank(message = "ID를 입력하세요.")
    private String loginId;

    @NotBlank(message = "비밀번호를 입력하세요.")
    private String password;
    private String passwordCheck;

    @NotBlank(message = "이름을 입력하세요.")
    private String name;

    public Member toEntity(){
        return Member.builder()
                .loginId(this.loginId)
                .password(this.password)
                .name(this.name)
                .role(MemberRole.USER)
                .build();
    }
}

 

  • dto > LoginRequest
    • 로그인 시 데이터 받는 폼
@Getter @Setter
@NoArgsConstructor
public class LoginRequest {

    private String loginId;
    private String password;
}

 

4. repository

  • MemberRepository
    • JpaRepository를 extends 하여 구현
public interface MemberRepository extends JpaRepository<Member, Long> {

	// 로그인 ID를 갖는 객체가 존재하는지 => 존재하면 true 리턴 (ID 중복 검사 시 필요)
    boolean existsByLoginId(String loginId);

	// 로그인 ID를 갖는 객체 반환
    Member findByLoginId(String loginId);
}

 

5. service

  • MemberService
    • 로그인 ID 중복 검사 메서드
    • 회원가입 메서드
    • 로그인 메서드
    • 로그인한 Member 반환 메서드
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public boolean checkLoginIdDuplicate(String loginId){
        return memberRepository.existsByLoginId(loginId);
    }


    public void join(JoinRequest joinRequest) {
        memberRepository.save(joinRequest.toEntity());
    }

    public Member login(LoginRequest loginRequest) {
        Member findMember = memberRepository.findByLoginId(loginRequest.getLoginId());

        if(findMember == null){
            return null;
        }

        if (!findMember.getPassword().equals(loginRequest.getPassword())) {
            return null;
        }

        return findMember;
    }

    public Member getLoginMemberById(Long memberId){
        if(memberId == null) return null;

        Optional<Member> findMember = memberRepository.findById(memberId);
        return findMember.orElse(null);

    }
}

 

6. controller

  • @RequestMapping 을 통해서 로그인 방법을 주소의 끝에 붙여줘야 함 ( ex : localhost:8080/cookie-login)
  • 로그인 구현 방법마다 [ "loginType" : 로그인 방법 , "pageName" : 로그인 방법을 포함한 페이지 이름 ] 을 model에 담아서 넘김
  • 로그인, 회원가입, 홈화면, 회원정보, 관리자화면, 로그아웃에 대응하는 메서드 작성

 

7. resources > templates

  • 모든 화면의 <head> title은 model로 넘겨받은 pageName로 설정
  • 모든 화면의 <body> 에는 model로 넘겨받은 pageName 출력 => 클릭 시 홈 화면으로 이동
  • home.html
    • 로그인 x : 회원가입, 로그인 버튼 출력
    • 로그인 o
      • "컨트롤러에서 넘겨받은 이름" + 님 환영합니다! 출력
      • 마이 페이지, 관리자 페이지, 로그아웃 버튼 출력
<html lang="ko">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="|${pageName}|"></title>
</head>
<body>
<div>
    <h1><a th:href="|/${loginType}|">[[${pageName}]]</a></h1> <hr/>
    <div th:if="${name == null}">
        <h3>로그인 되어있지 않습니다!</h3>
        <button th:onclick="|location.href='@{/{loginType}/join (loginType=${loginType})}'|">회원 가입</button> <br/><br/>
        <button th:onclick="|location.href='@{/{loginType}/login (loginType=${loginType})}'|">로그인</button>
    </div>
    <div th:unless="${name == null}">
        <h3>[[${name}]]님 환영합니다!</h3>
        <button th:onclick="|location.href='@{/{loginType}/info (loginType=${loginType})}'|">마이 페이지</button> <br/><br/>
        <button th:onclick="|location.href='@{/{loginType}/admin (loginType=${loginType})}'|">관리자 페이지</button> <br/><br/>
        <button th:onclick="|location.href='@{/{loginType}/logout (loginType=${loginType})}'|">로그아웃</button>
    </div>
</div>
</body>
</html>

 

  • join.html
    • th:errorclass , th:errors 사용해서 FieldError 출력 (Validation)
    • ID, 비밀번호, 비밀번호 체크, 이름 입력 칸
    • 회원 가입 버튼
<!DOCTYPE html>
<html lang="ko">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="|${pageName}|"></title>
</head>
<body>
<div>
    <h1><a th:href="|/${loginType}|">[[${pageName}]]</a></h1> <hr/>
    <h2>회원 가입</h2>
    <form th:method="post" th:action="|@{/{loginType}/join (loginType=${loginType})}|" th:object="${joinRequest}">
        <div>
            <label th:for="loginId">ID : </label>
            <input type="text" th:field="*{loginId}" th:errorclass="error-input"/>
            <div class="error-class" th:errors="*{loginId}"></div>
        </div>
        <br/>
        <div>
            <label th:for="password">비밀번호 : </label>
            <input type="password" th:field="*{password}" th:errorclass="error-input"/>
            <div class="error-class" th:errors="*{password}"></div>
        </div>
        <br/>
        <div>
            <label th:for="passwordCheck">비밀번호 체크 : </label>
            <input type="password" th:field="*{passwordCheck}" th:errorclass="error-input"/>
            <div class="error-class" th:errors="*{passwordCheck}"></div>
        </div>
        <br/>
        <div>
            <label th:for="name">이름 : </label>
            <input type="text" th:field="*{name}" th:errorclass="error-input"/>
            <div class="error-class" th:errors="*{name}"></div>
        </div>
        <br/>
        <button type="submit">회원 가입</button>
    </form>
</div>
</body>
</html>

<style>
    .error-class {
        color: red;
    }
    .error-input {
        border-color: red;
    }
</style>

 

  • login.html
    • #fields.hasGlobalErrors() , #fields.globalErrors() 사용해서 GlobalError 출력 (Validation)
    • ID와 비밀번호 입력 칸
    • 로그인, 회원가입 버튼
<!DOCTYPE html>
<html lang="ko">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="|${pageName}|"></title>
</head>
<body>
<div>
    <h1><a th:href="|/${loginType}|">[[${pageName}]]</a></h1> <hr/>
    <h2>로그인</h2>
    <form th:method="post" th:action="|@{/{loginType}/login (loginType=${loginType})}|" th:object="${loginRequest}">
        <div>
            <label th:for="loginId">ID : </label>
            <input type="text" th:field="*{loginId}"/>
        </div>
        <br/>
        <div>
            <label th:for="password">비밀번호 : </label>
            <input type="password" th:field="*{password}"/>
        </div>
        <div th:if="${#fields.hasGlobalErrors()}">
            <br/>
            <div class="error-class" th:each="error : ${#fields.globalErrors()}" th:text="${error}" />
        </div>
        <br/>
        <button type="submit">로그인</button>
        <button type="button" th:onclick="|location.href='@{/{loginType}/join (loginType=${loginType})}'|">회원 가입</button> <br/><br/>
    </form>
</div>
</body>
</html>

<style>
    .error-class {
        color: red;
    }
    .error-input {
        border-color: red;
    }
</style>

 

  • info.html
    • 현재 로그인 한 계정의 ID, 이름, role 출력
<!DOCTYPE html>
<html lang="ko">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="|${pageName}|"></title>
</head>
<body>
<div>
    <h1><a th:href="|/${loginType}|">[[${pageName}]]</a></h1> <hr/>
    <h2>마이 페이지</h2>
    <div>
        <div th:text="|ID : ${member.loginId}|"/>
        <div th:text="|이름 : ${member.name}|"/>
        <div th:text="|role : ${member.role}|"/>
    </div>
</div>
</body>
</html>

 

  • admin.html
    • role : ADMIN => 인가 성공했다는 뜻
    • 화면에는 관리자 페이지, 인가 성공 출력
<!DOCTYPE html>
<html lang="ko">
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="|${pageName}|"></title>
</head>
<body>
<div>
    <h1><a th:href="|/${loginType}|">[[${pageName}]]</a></h1> <hr/>
    <h2>관리자 페이지</h2>
    <h3>인가 성공</h3>
</div>
</body>
</html>