게시판 구현(4.2) - 회원가입 구현

김지민·2023년 7월 3일

spring Boot

목록 보기
9/9

회원 가입 구현

html 코드

<!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>

MemeberController 코드

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";
    }
}



상세 설정

role 상수 정의 - enum

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;
    }
}

ENTITY CLASS

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). -> 회원 탈퇴시 비활성화.
    }
}

repository

package com.itwill.spring4.repository.member;

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

public interface MemberRepository extends JpaRepository<Member, Long> {

    Member findByUsername(String username);
}

service

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(고유키)를 리턴.
    }
    

로그인 service 추가


 // 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에서 구현하게 만든 거임.

profile
한 단계씩 차근차근

0개의 댓글