<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/base_layout}">
<div layout:fragment="main">
<div class="card my-2" >
<div class="card-header text-center">
<h1>회원 가입 페이지</h1>
</div>
<div class="card-body">
<form method="post" th:action="@{ /member/signup }">
<div class="my-2">
<input type="text" class="form-control" name="username" placeholder="사용자 아이디" required autofocus />
</div>
<div class="my-2">
<input type="password" class="form-control" name="password" placeholder="비밀번호" required />
</div>
<div class="my-2">
<input type="email" class="form-control" name="email" placeholder="이메일" required />
</div>
<div class="my-2">
<input type="submit" class="form-control btn btn-outline-success" value="가입 완료" />
</div>
</form>
</div>
</div>
</div>
</html>

package com.itwill.spring4.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.itwill.spring4.dto.member.MemberSignUpDto;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequestMapping("/member")
public class MemeberController {
@GetMapping("/signup") // 주소는 가능하면 전부 다 소문자로 사용하기
public void signUp() {
log.info("signUp() GET");
}
@PostMapping("/signup")
public String signUp(MemberSignUpDto dto) {
log.info("signUp(dto ={}) POST", dto);
// 회원 가입 서비스 호출
Long id = memberService.registerMember(dto);
log.info("회원가입 id ={}", id);
// 회원 가입 이후에 로그인 화면으로 이동(redirect):
return "redirect:/login";
}
}


package com.itwill.spring4.repository.member;
public enum Role {
USER("ROLE_USER", "USER"),
ADMIN("ROLE_ADMIN", "ADMIN");
//-> 해당 객체가 생성자를 통해 생성이 되며, 각 이름(user, admin)은 변수가 됨.
//-> 또한, 상수를 정의하기에 순서에 따라 0~ 번호가 메겨짐.
//-> user: 0, admin: 1
private final String key;
private final String name;
Role(String key, String name) {
this.key = key;
this.name = name;
}
public String getKey() {
return this.key;
}
}
security filter에게 UserDetails 타입을 넘겨 줘야 함.
spring security가 사용하는 타입은 UserDetails이어서 반드시 필
==> 그래서 Member 클래스가 UserDetials을 상속하고 있어야 함.
package com.itwill.spring4.repository.member;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.itwill.spring4.repository.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@NoArgsConstructor
@Getter
@ToString
@Entity
@Table(name = "MEMBERS")
@SequenceGenerator(name = "MEMBERS_SEQ_GEN", sequenceName = "MEMBERS_SEQ", allocationSize = 1)
// Member is-A UserDetails
// 스프링 시큐리티는 로그인 처리를 위해서 UserDetails 객체를 사용하기 때문에
// 회원 정보 엔터티는 UserDetails 인터페이스를 구현해야 함.
public class Member extends BaseTimeEntity implements UserDetails{
@Id // primary key
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBERS_SEQ_GEN")
// GenerationType.IDENTITY: 마리아 디비, mySql
private Long id;
// 제약조건
@Column(nullable = false, unique = true) // NOT NULL, UNIQUE 제약 조건
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private Role role;
@Builder
// 회원 가입을 하는 user는 무조건 USER 권한을 갖는 사용자만 만듦
private Member(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
this.role = Role.USER; // 회원 가입 사용자 권한의 기본값은 USER
}
// 서로 상속관계이기에 Collection리턴은 ArrayList리턴과 같은 말
// GrantedAuthority를 상속받는 타입(?: 모든 타입)이면 원소로 갖아도 됨
// UserDetails 인터페이스의 추상 메서드를 구현:
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// ROLE_USER 권한을 갖음.
// -> 2개 이상의 권한시, 2개 이상의 객체(new SimpleGrantedAuthority)를 만들면 됨.
return Arrays.asList(new SimpleGrantedAuthority(role.getKey()));
}
@Override
public boolean isAccountNonExpired() {
return true; // 계정(account)이 non-expired(만료되지 않음)
}
@Override
public boolean isAccountNonLocked() {
return true; // 계정이 non-lock(잠기지 않음) + 만약 false 면 로그인 안됨.
}
@Override
public boolean isCredentialsNonExpired() {
return true; // 비밀번호가 non-expired.(만료되지 않음)
}
@Override
public boolean isEnabled() {
return true; // 사용자 상세정보(UserDetails)가 활성화(enable). -> 회원 탈퇴시 비활성화.
}
}
package com.itwill.spring4.repository.member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByUsername(String username);
}
sesrvice에서 비밀번호 encoding을 해야 함.
package com.itwill.spring4.service;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.itwill.spring4.dto.member.MemberSignUpDto;
import com.itwill.spring4.repository.member.Member;
import com.itwill.spring4.repository.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
@RequiredArgsConstructor
// Security Filter Chain에서 UserDetailsService 객체를 사용할 수 있도록 하기 위해서.
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
// SecurityConfig에서 설정한 PasswordEncoder 빈(bean)을 주입해줌.
// -> SecurityConfig에서 Bean으로 관리가 되어 있기에 스프링 컨테이너는 다음을 생성하고 있어서 필요한 곳에 넣어줌.
private final PasswordEncoder passwordEncoder;
// 회원 가입
public Long registerMember(MemberSignUpDto dto) {
log.info("registerMember(dto={})", dto);
Member entity = Member.builder().username(dto.getUsername()).password(passwordEncoder.encode(dto.getPassword()))
.email(dto.getEmail()).build();
log.info("save 전: entity= {}", entity);
memberRepository.save(entity);
log.info("save 후: entity={}", entity);
return entity.getId(); // DB에 저장된 ID(고유키)를 리턴.
}
// Security Filter Chain에서는 loadUserByUsername를 호출하여 Db에 존재하는지를 판단.
// -> 해당 메서드가 있어야만 Security Filter Chain가 제대로 동작이 될 수 있음.
// -> 로그인 성공 여부 판단.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername(username = {})", username);
// DB에서 username으로 사용자 정보 검색(select).
UserDetails user = memberRepository.findByUsername(username);
if (user != null) {
return user;
}
// 에러 메시지
throw new UsernameNotFoundException(username + "not found");
}
}
로그 결과




만약 username이 없을 경우
다음 로그를 security Filter Chain이 호출함.
1. username이 존재하는지
2. isAccountNonExpired
3. isAccountNonLocked
4. isCredentialsNonExpired
5. isEnabled

member 클래스의 UserDetails의 @overide된 메서드를 검사함.
==> 그래서 service에서 구현하게 만든 거임.