반응형
로그인 방법 정리
- 클릭 시 해당 로그인 구현 예제 페이지로 이동
- 쿠키 로그인
- 세션 로그인
- Spring Security 로그인(Form Login)
- JWT Token API 로그인
- JWT + Cookie를 사용한 화면 로그인
- 구글 로그인
- 카카오, 네이버, 페이스북 로그인
전체 코드
공통 부분
- 공통되는 부분은 이 페이지에서 정리
- 공통되는 부분
- 구현할 기능
- 공통으로 사용하는 라이브러리
- User Entity, UserRole, DTO(JoinRequest, LoginRequest)
- UserRepository, UserService
- Thymeleaf를 사용하여 생성한 화면
로그인 예제 기능 정리
- 회원가입
- loginId, password, passwordCheck, nickname 입력
- loginId나 nickname이 중복되면 회원가입 불가
- password와 passwordCheck가 다르면 회원가입 불가
- Thymeleaf Validation을 사용하여 에러 메세지 출력 (Field Error)
- 회원가입 성공 시 권한은 USER로 설정
- 로그인
- loginId나 password가 틀리면 로그인 불가
- Thymeleaf Validation을 사용하여 에러 메세지 출력 (Global Error)
- 로그아웃
- 유저 정보 출력(인증)
- 로그인한 유저의 loginId, nickname, role(권한) 출력
- 로그인하지 않은 유저가 접근 시 로그인 페이지로 이동
- 관리자 페이지(인가)
- 로그인한 유저의 권한이 ADMIN이면 접속 가능
- 로그인한 유저의 권한이 USER이면 홈으로 이동
- 로그인하지 않은 유저가 접근 시 로그인 페이지로 이동
라이브러리
- 예제마다 추가되는 라이브러리들은 따로 정리
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
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'
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'
}
UserEntity
@Entity
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String loginId;
private String password;
private String nickname;
private UserRole role;
}
UserRole
public enum UserRole {
USER, ADMIN;
}
DTO(JoinRequest, LoginRequest)
JoinRequest
@Getter
@Setter
@NoArgsConstructor
public class JoinRequest {
@NotBlank(message = "로그인 아이디가 비어있습니다.")
private String loginId;
@NotBlank(message = "비밀번호가 비어있습니다.")
private String password;
private String passwordCheck;
@NotBlank(message = "닉네임이 비어있습니다.")
private String nickname;
// 비밀번호 암호화 X
public User toEntity() {
return User.builder()
.loginId(this.loginId)
.password(this.password)
.nickname(this.nickname)
.role(UserRole.USER)
.build();
}
// 비밀번호 암호화
public User toEntity(String encodedPassword) {
return User.builder()
.loginId(this.loginId)
.password(encodedPassword)
.nickname(this.nickname)
.role(UserRole.USER)
.build();
}
}
LoginRequest
@Getter
@Setter
@NoArgsConstructor
public class LoginRequest {
private String loginId;
private String password;
}
UserRepository
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByLoginId(String loginId);
boolean existsByNickname(String nickname);
Optional<User> findByLoginId(String loginId);
}
UserService
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// Spring Security를 사용한 로그인 구현 시 사용
// private final BCryptPasswordEncoder encoder;
/**
* loginId 중복 체크
* 회원가입 기능 구현 시 사용
* 중복되면 true return
*/
public boolean checkLoginIdDuplicate(String loginId) {
return userRepository.existsByLoginId(loginId);
}
/**
* nickname 중복 체크
* 회원가입 기능 구현 시 사용
* 중복되면 true return
*/
public boolean checkNicknameDuplicate(String nickname) {
return userRepository.existsByNickname(nickname);
}
/**
* 회원가입 기능 1
* 화면에서 JoinRequest(loginId, password, nickname)을 입력받아 User로 변환 후 저장
* loginId, nickname 중복 체크는 Controller에서 진행 => 에러 메세지 출력을 위해
*/
public void join(JoinRequest req) {
userRepository.save(req.toEntity());
}
/**
* 회원가입 기능 2
* 화면에서 JoinRequest(loginId, password, nickname)을 입력받아 User로 변환 후 저장
* 회원가입 1과는 달리 비밀번호를 암호화해서 저장
* loginId, nickname 중복 체크는 Controller에서 진행 => 에러 메세지 출력을 위해
*/
public void join2(JoinRequest req) {
userRepository.save(req.toEntity(encoder.encode(req.getPassword())));
}
/**
* 로그인 기능
* 화면에서 LoginRequest(loginId, password)을 입력받아 loginId와 password가 일치하면 User return
* loginId가 존재하지 않거나 password가 일치하지 않으면 null return
*/
public User login(LoginRequest req) {
Optional<User> optionalUser = userRepository.findByLoginId(req.getLoginId());
// loginId와 일치하는 User가 없으면 null return
if(optionalUser.isEmpty()) {
return null;
}
User user = optionalUser.get();
// 찾아온 User의 password와 입력된 password가 다르면 null return
if(!user.getPassword().equals(req.getPassword())) {
return null;
}
return user;
}
/**
* userId(Long)를 입력받아 User을 return 해주는 기능
* 인증, 인가 시 사용
* userId가 null이거나(로그인 X) userId로 찾아온 User가 없으면 null return
* userId로 찾아온 User가 존재하면 User return
*/
public User getLoginUserById(Long userId) {
if(userId == null) return null;
Optional<User> optionalUser = userRepository.findById(userId);
if(optionalUser.isEmpty()) return null;
return optionalUser.get();
}
/**
* loginId(String)를 입력받아 User을 return 해주는 기능
* 인증, 인가 시 사용
* loginId가 null이거나(로그인 X) userId로 찾아온 User가 없으면 null return
* loginId로 찾아온 User가 존재하면 User return
*/
public User getLoginUserByLoginId(String loginId) {
if(loginId == null) return null;
Optional<User> optionalUser = userRepository.findByLoginId(loginId);
if(optionalUser.isEmpty()) return null;
return optionalUser.get();
}
}
화면
- 공통으로 사용할 화면들 사용한 화면들
- 로그인 예제마다 URL이 다른데 이를 Path Variable을 통해 구분
- Controller에서 화면에 model을 통해 "loginType", "pageName" 전송
- URL에는 loginType을 넣어주면 되고, 화면 페이지에는 pageName을 출력하면 각각의 예제에서 Controller은 다르지만 화면은 한 번씩만 생성하여 사용 가능
home.html
- 로그인이 되어있으면 Controller에서 nickname을 보내줌
- nickname이 null이면 로그인하지 않은 상태
- 로그인한 사용자에게는 회원가입, 로그인 버튼 출력 및 화면에 nickname 출력
- 로그인하지 않은 사용자에게는 유저 정보, 관리자 페이지, 로그아웃 버튼 출력
<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="${nickname == 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="${nickname == null}">
<h3>[[${nickname}]]님 환영합니다!</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
- loginId, password, nickname을 입력받음
- th:object로 joinRequest와 연결하여 사용
- validation을 통한 Field Error 출력을 위해 th:errorclass, th:error 사용
<!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">로그인 아이디 : </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="nickname">닉네임 : </label>
<input type="text" th:field="*{nickname}" th:errorclass="error-input"/>
<div class="error-class" th:errors="*{nickname}"></div>
</div>
<br/>
<button type="submit">회원 가입</button>
</form>
</div>
</body>
</html>
<style>
.error-class {
color: red;
}
.error-input {
border-color: red;
}
</style>
login.html
- loginId, password를 입력받음
- th:object로 loginRequest와 연결하여 사용
- Field Error을 출력한 join과는 달리 validation을 통한 Global Error 출력
- 로그인 페이지에서 회원가입 페이지로 이동 가능
<!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">로그인 아이디 : </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
- 유저 정보 출력 페이지
- 로그인을 했으면 Controller에서 User을 보내줌
- 화면에서는 User을 받아 loginId, nickname, 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="|loginId : ${user.loginId}|"/>
<div th:text="|nickname : ${user.nickname}|"/>
<div th:text="|role : ${user.role}|"/>
</div>
</div>
</body>
</html>
admin.html
- 이 페이지는 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] Session을 사용한 로그인 구현 (7) | 2023.01.04 |
---|---|
[Spring Boot] Cookie를 사용한 로그인 구현 (1) | 2023.01.02 |
[Spring Boot] 연관관계 매핑 - N:M 관계 예제 (0) | 2023.01.01 |
[Spring Boot] 연관관계 매핑 - 1:1 관계 예제 (+ @PostConstruct를 사용한 초기 데이터 생성) (0) | 2023.01.01 |
[Spring Boot] 연관관계 매핑 - 1:N 관계 예제 (+FetchType.LAZY) (0) | 2022.12.31 |