'Spring Security' 를 이용하여 회원 로그인 / 회원가입 기능을 구현하고자 한다.
- 0. 프로젝트 트리
* config 디렉토리
스프링 시큐리티를 통한 회원 로그인을 구현하기 위한 설정 파일이 포함된다.
* controller 디렉토리
* dto 디렉토리
* entity 디렉토리
* exception 디렉토리
발생할 각종 예외를 처리하기 위한 exception 파일이 포함된다.
* repository 디렉토리
* service 디렉토리
- 1. Entity 구성
+) MemberDto 구성
public class MemberDto {
@Data
public static class Request {
private Long id;
private String username;
private String password;
private String nickname;
private String email;
private String phone;
private MemberRole role;
private String password2;
//DTO -> Entity
public Member toEntity() {
return new Member(
this.id,
this.username,
this.password,
this.nickname,
this.email,
this.phone,
this.role
);
}
}
@Data
public static class Response {
private Long id;
private String username;
private String password;
private String nickname;
private String email;
private String phone;
private MemberRole role;
//Entity -> DTO
public Response(Member member) {
this.id = member.getId();
this.username = member.getUsername();
this.password = member.getPassword();
this.nickname = member.getNickname();
this.email = member.getEmail();
this.phone = member.getPhone();
this.role = member.getRole();
}
}
}
- 2. Repository 구성
package project.board_service.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import project.board_service.entity.Member;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByUsername(String username);
}
Spring Data Jpa 를 이용하여 Repository 를 구성한다.
- !. Spring Security 설정 파일 - (config 디렉토리)
이 부분이 DB의 Member 테이블에 저장된 '회원 데이터'로 로그인하기 위해 가장 중요하다!
1. CustomUserDetailsService.java
: 이 파일의 목적은 회원 데이터로 로그인 할것이라는 것을 Spring Security 에게 알려주기 위함이다.
"loadUserByUsername" 메서드의 return 타입을 보면,
Member 객체의 username, password, 권한 을 이용하여 "User" (Spring Security의 로그인 타입) 을 생성하여 반환하는 것을 볼 수 있다.
즉, 회원 데이터 -> "User" (Spring Security의 로그인 타입) 으로 변환하는 과정이다.
"getAuthorities" 메서드는 Member 객체의 권한명 앞에 "ROLE_" 을 붙여서, Spring Security 의 권한 타입으로 변환한다.
CF) "ROLE_권한명" 은 Spring Security 에서 권한을 표현하는 일반적인 표현법이다.
[CustomUserDetailsService.java]
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired private MemberRepository memberRepository;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with name: " + username));
return new User(member.getUsername(), member.getPassword(), getAuthorities(member));
}
private Collection<? extends GrantedAuthority> getAuthorities(Member member) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + member.getRole().name()));
return authorities;
}
}
2. WebSecurityConfig.java
: 이 파일의 목적은 웹 서비스 접근에 관한 설정을 하는 것이다.
[WebSecurityConfig.java]
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
/*PasswordEncoder Bean 등록 - password 암호화 (방식 - BCryptPasswordEncoder)*/
@Bean
public static PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
/*WebSecurityCustomizer Bean 등록 - 정적 resources 접근을 위함*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web -> web.ignoring()
.requestMatchers("/img/**", "/css/**")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()));
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorizeRequests) ->
authorizeRequests
.requestMatchers("/", "/members/login", "/members/join").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) ->
form
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/members/login")
.loginProcessingUrl("/login")
.failureUrl("/members/login?error=true")
.defaultSuccessUrl("/", true)
.permitAll()
)
.userDetailsService(customUserDetailsService)
.logout(logout ->
logout
.logoutUrl("/logout") //로그아웃 처리 URL
.logoutSuccessUrl("/") //로그아웃 성공 후 리다이렉트 할 URL
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
)
.csrf(csrf ->
csrf
.ignoringRequestMatchers("/api/**") // /api/** 경로에 대한 CSRF 보호를 비활성화
);
return http.build();
}
}
- 3. Service 구성
@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;
/** Create Member **/
@Transactional
public Member createMember(MemberDto.Request dto) {
//1. 중복 검사 (username, email, phone)
duplicateValidation(dto);
//2. 'password' 이중 검사
passwordDoubleCheck(dto);
//3. create member
return memberRepository.save(dto.toEntity());
}
/* 1. 중복 검사 (username, email, phone) */
public void duplicateValidation(MemberDto.Request dto) {
//username 중복 확인
if (memberRepository.findByUsername(dto.getUsername()).isPresent()) {
throw new DataAlreadyExistsException("이미 존재하는 'username' 입니다.");
}
//email 중복 확인
if (memberRepository.findByUsername(dto.getEmail()).isPresent()) {
throw new DataAlreadyExistsException("이미 존재하는 'email' 입니다.");
}
//phone 중복 확인
if (memberRepository.findByUsername(dto.getPhone()).isPresent()) {
throw new DataAlreadyExistsException("이미 존재하는 'phone' 입니다.");
}
}
/* 2. 'password' 이중 검사 */
public void passwordDoubleCheck(MemberDto.Request dto) {
if (!dto.getPassword().equals(dto.getPassword2())) {
throw new PasswordCheckFailedException("비밀번호가 동일하지 않습니다.");
}
}
}
Member 를 생성하고 저장하기 위한 메서드 createMember 를 구성하였다.
'username', 'email', 'phone' 의 중복 검사 기능과 password 를 정확하게 입력하였는지 이중 검사 기능을 포함한다.
- 4. Controller 구성
@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
/**
* 로그인(Login) - "/members/login"
*/
@GetMapping("/login")
public String loginForm(Model model) {
model.addAttribute("loginForm", new MemberDto.Request());
return "members/login";
}
}
- 5. VIEW
- login.html
<!DOCTYPE html>
<html lang="kr" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login Page</title>
</head>
<body>
<h2>로그인</h2>
<form th:action="@{/login}" th:object="${loginForm}" method="post">
<div>
<input type="text" id="floatingInput" placeholder="username"
name="username" th:field="*{username}" required="">
<label for="floatingInput">Username</label>
</div>
<div>
<input type="password" id="floatingPassword" placeholder="Password" autocomplete="off"
name="password" th:field="*{password}" required="">
<label for="floatingPassword">Password</label>
</div>
<button type="submit">Login</button>
<!-- Error message -->
<div th:if="${errorMessage}">
<p th:text="${errorMessage}">Error message here</p>
</div>
</form>
</body>
</html>
+) Test Data 입력
// Test Data 생성
@Profile("local")
@Component
@RequiredArgsConstructor
public class InitData {
private final InitUserService initUserService;
@PostConstruct
public void init() {
initUserService.init();
}
@Component
static class InitUserService {
@PersistenceContext
private EntityManager em;
@Autowired
private PasswordEncoder passwordEncoder;
@Transactional
public void init() {
String username = "member1";
String password = "1234";
MemberDto.Request dto = new MemberDto.Request();
dto.setUsername(username);
dto.setPassword(passwordEncoder.encode(password));
dto.setRole(MemberRole.USER);
em.persist(dto.toEntity());
}
}
}
username : "member1"
password : "1234"
회원 정보를 저장한다.
- 6. 실행 화면
- DB에 저장된 회원 데이터
로그인 페이지 - "/members/login"
username : "member1"
password : "1234"
회원 ID / PW 를 입력하여 로그인 시도한다.
로그인 성공시 이동 URL로 설정된 "/" 로 이동하였다.
즉, 로그인에 성공한 것을 볼 수 있다.
'PROJECT > [SpringBoot] 게시판 서비스' 카테고리의 다른 글
[Spring Boot - 게시판 서비스] +) 게시글 줄바꿈 구현하기 (2) | 2024.10.30 |
---|---|
[Spring Boot - 게시판 서비스] #0. 프로젝트 생성 및 환경설정 (+Github 동기화) (1) | 2024.10.11 |
[게시판 서비스] 회원 탈퇴시, 게시글 / 댓글 처리 (0) | 2024.06.02 |
[게시판 서비스] 게시글 키워드 검색 + 정렬 + 페이징 기능 구현 (0) | 2024.06.02 |
[게시판 서비스] 게시글 페이징 처리 구현 (0) | 2024.06.02 |