Spring Boot/STUDY

[Spring Boot] JPA 프로젝트 - 로그인, 로그아웃 기능 구현

코맹 2024. 6. 21. 14:49

 

 

 

로그인 기능 구현

 

회원가입 기능을 구현했으니 로그인 기능을 구현해보려고 한다.

 

/security/SecurityConfig.java에 login url 설정
// 로그인 url을 지정 ~/member.login, 로그인 성공하면 루트로 변경
.formLogin((fl) -> fl.loginPage("/member/login").defaultSuccessUrl("/"))
  • 로그인 페이지와 로그인 성공시 이동 페이지를 지정해주었다

 

/repository/MemberRepository.java find* 메서드 추가
package com.eunji.backboard.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.eunji.backboard.entity.Member;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long>{
  Optional<Member> findByUserName(String username);
  Optional<Member> findByEmail(String email);
  
}
  • db에 있는 데이터와 로그인 입력 데이터가 일치하는지 확인하기 위해  repository에 메서드를 추가

 

/controller/MemberController.java에 login() 메서드 추가
@GetMapping("/login")
  public String login() {
      return "member/login";
  }
  • PostMapping은 생성할 필요 ❌
  • SpringSecurity에서 제공해주기 때문.

 

/templates/member/login.html 생성
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
  <div layout:fragment="main-content" class="card container my-3 form-register">
    <div class="my-3 border-bottom">
      <h4>로그인</h4>
    </div>
    <form th:action="@{/member/login}" method="post">
      <div th:if="${param.error}">
        <div class="alert alert-danger">
          사용자 이름, 비밀번호를 확인해주세요.
        </div>
      </div>
      <div class="mb-3">
        <label for="username" class="form-lable">사용자이름</label>
        <input type="text" name="username" class="form-control">
      </div>
      <div class="mb-3">
        <label for="password" class="form-lable">비밀번호</label>
        <input type="password" name="password" class="form-control">
      </div>

      <div class="d-flex justify-content-center">
        <button type="submit" class="btn btn-sm btn-primary mx-2">로그인</button>
        <a href="/" class="btn btn-sm btn-secondary">취소</a>
      </div>
    </form>
  </div>
</html>

 

로그인 화면

 

/security/MemberRole.java 생성
package com.eunji.backboard.security;

import lombok.Getter;

@Getter
public enum MemberRole {
  // 방법1
  // ADMIN("관리자", "ROLE_ADMIN"), USER("사용자","ROLE_USER");

  // MemberRole(String key, String value) {
  //   this.key = key;
  //   this.value = value;
  // }

  // private String key;
  // private String value;

  // 방법2
  ADMIN("ROLE_ADMIN"), USER("ROLE_USER");

  MemberRole(String value) {
    this.value = value;
  }

  private String value;

}
  • 권한으로 처리하기 위해 ENUM 클래스로 만듦

 

/entity/Member.java에 MemberRole 추가
@Enumerated(EnumType.STRING)    // Enum타입이 STRING인 이유: "ROLE_ADMIN", "ROLE_USER"이기 때문
@Column(length = 12)
private MemberRole role;

 

 

DB를 확인하면 ROLE 컬럼이 추가된 것을 확인할 수 있다.

 

 

/service/MemberService.java setMember() 메서드 수정
public Member setMember(String username, String email, String password) {
    Member member = Member.builder().username(username).email(email).regDate(LocalDateTime.now()).build();

    // BCryptPasswordEncoder 매번 새롭게 객체를 생성한다.
    // 이것보다는 Bean 등록해놓고 쓰는 게 유지보수를 위해서 더 좋음
    // BCryptPasswordEncoder pwdEncoder = new BCryptPasswordEncoder();   
    member.setPassword(passwordEncoder.encode(password));    // 암호화한 값을 DB에 저장
    member.setRegDate(LocalDateTime.now());
    member.setRole(MemberRole.USER);    // 일반사용자 권한
    this.memberRepository.save(member);

    return member;

  }
  • member.setRole(MemberRole.USER) 코드 추가

 

  • DB 확인 결과 ROLE 컬럼에 USER가 들어간 것을 확인할 수 있다.

 

/service/MemberSecurityService.java 생성
package com.eunji.backboard.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.eunji.backboard.entity.Member;
import com.eunji.backboard.repository.MemberRepository;
import com.eunji.backboard.security.MemberRole;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class MemberSecurityService implements UserDetailsService{
  private final MemberRepository memberRepository;


  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Optional<Member> _member = this.memberRepository.findByUsername(username);
    if(_member.isEmpty()) {
      throw new UsernameNotFoundException("사용자가 없습니다.");
    }
    Member member = _member.get();

    List<GrantedAuthority> authorities = new ArrayList<>();
    if("admin".equals(username)) {
      authorities.add(new SimpleGrantedAuthority(MemberRole.ADMIN.getValue()));
    } else {
      authorities.add(new SimpleGrantedAuthority(MemberRole.USER.getValue()));
    }

    return new User(member.getUsername(), member.getPassword(), authorities);

  }
  
}
  • Spring Security가 로그인 유효성 검사를 해줌

 

/security/SecurityConfig.java 계정 관리자 빈 추가
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception{
  return authenticationConfiguration.getAuthenticationManager();
}

 

 

/templates/layout.html 로그인/로그아웃 토글 메뉴 추가
<li class="nav-item">
	<!-- 로그인을 안했을 때 -->
	<a th:href="@{/member/login}" sec:authorize="isAnonymous()" class="nav-link">로그인</a>
	<!-- 로그인된 이후에 표시 -->
	<a th:href="@{/member/logout}" sec:authorize="isAuthenticated()" class="nav-link">로그아웃</a>
</li>
<li class="nav-item">
	<a th:href="@{/member/register}" sec:authorize="isAnonymous()" class="nav-link">회원가입</a>
</li>

 

 

  • 로그인 성공시 nav바에 로그아웃으로 바뀌는 것을 확인할 수 있다.

 


 

로그아웃 기능 구현

 

/security/SecurityConfig.java 로그아웃 코드 추가

.logout((logout) -> logout.logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
);