반응형

로그인 방법 정리

  • 클릭 시 해당 로그인 구현 예제 페이지로 이동
  1. 쿠키 로그인
  2. 세션 로그인
  3. Spring Security 로그인(Form Login)
  4. JWT Token API 로그인
  5. JWT + Cookie를 사용한 화면 로그인
  6. 구글 로그인
  7. 카카오, 네이버, 페이스북 로그인

전체 코드

공통 부분

  • 공통되는 부분은 이 페이지에서 정리
  • 공통되는 부분
    • 구현할 기능
    • 공통으로 사용하는 라이브러리
    • User Entity, UserRole, DTO(JoinRequest, LoginRequest)
    • UserRepository, UserService
    • Thymeleaf를 사용하여 생성한 화면

로그인 예제 기능 정리

  1. 회원가입
    • loginId, password, passwordCheck, nickname 입력
    • loginId나 nickname이 중복되면 회원가입 불가
    • password와 passwordCheck가 다르면 회원가입 불가
    • Thymeleaf Validation을 사용하여 에러 메세지 출력 (Field Error)
    • 회원가입 성공 시 권한은 USER로 설정
  2. 로그인
    • loginId나 password가 틀리면 로그인 불가
    • Thymeleaf Validation을 사용하여 에러 메세지 출력 (Global Error)
  3. 로그아웃
  4. 유저 정보 출력(인증)
    • 로그인한 유저의 loginId, nickname, role(권한) 출력
    • 로그인하지 않은 유저가 접근 시 로그인 페이지로 이동
  5. 관리자 페이지(인가)
    • 로그인한 유저의 권한이 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>
반응형

↓ 클릭시 이동

복사했습니다!