0. 상황 설명
- 여러가지 방법을 사용해서 로그인 기능을 구현하려 함
- 공통인 기능과 코드는 이 게시글에서 모두 정리
- 방법마다 다른 코드는 각 게시물에서 따로 정리
* 구현한 로그인 기능 프로젝트 *
- 쿠키 로그인 https://blogan99.tistory.com/84
- 세션 로그인 https://blogan99.tistory.com/86
- 스프링 시큐리티 로그인 https://blogan99.tistory.com/87
- JWT 로그인 https://blogan99.tistory.com/89
- 구글 로그인 (OAuth 2.0) https://blogan99.tistory.com/90
- 카카오 로그인 (OAuth 2.0) https://blogan99.tistory.com/91
- 네이버 로그인 (OAuth 2.0) https://blogan99.tistory.com/92
- 페이스북 로그인 (OAuth 2.0) https://blogan99.tistory.com/93
- 자동 로그인 (Spring Security - Remember Me) https://blogan99.tistory.com/104
1. 프로젝트
- 버전 정보 / DB
- 스프링부트 3.1.5
- gradle
- Java 21
- DB : MySQL
- 구현 기능
- 로그인 이전 화면
- 회원가입
- ID, 비밀번호, 이름 입력해서 회원가입 (하나라도 값이 없다면 에러메시지 출력 => 회원가입 불가)
- ID 중복 시 에러메시지 출력 => 회원가입 불가
- 체크 용 비밀번호가 다를 시 에러메시지 출력 => 회원가입 불가
- 로그인
- ID, 비밀번호 입력해서 로그인
- ID와 비밀번호가 일치하지 않으면 에러메시지 출력 => 로그인 불가
- 회원가입
- 로그인 이후 화면
- 아래의 모든 기능에 로그인 하지 않고 접근 시 로그인 화면으로 이동됨
- 마이페이지
- 로그인 된 계정의 아이디, 이름, memberRole (ADMIN / USER) 가 화면에 출력
- 회원가입 시 기본 memberRole : USER
- 로그인 된 계정의 아이디, 이름, memberRole (ADMIN / 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>
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 로그인 기능 구현 (2) - 세션 로그인 (0) | 2024.01.07 |
---|---|
[Spring Boot] 로그인 기능 구현 (1) - 쿠키 로그인 (0) | 2024.01.07 |
[Spring Boot] 페이징 기능 구현 ( + 페이징, 정렬, 검색, 에러 메시지 포함 예제) (1) | 2024.01.03 |
[SpringBoot] MySQL 연동 (maven, gradle) (0) | 2023.12.12 |
[Spring Boot] 스프링 구동 시에 특정 코드 자동 실행시키기 (Command Line Runner, Application Runner) (1) | 2023.11.22 |