코딩공작소

회원 도메인 추가/회원가입 및 Todo 도메인과 연결 본문

어플리케이션개발/토이프로젝트(취미)

회원 도메인 추가/회원가입 및 Todo 도메인과 연결

안잡아모찌 2024. 11. 19. 20:32

. 전체 구조 설계

  1. 도메인 설계
    • Member (회원): 회원 정보를 저장
    • Todo (할 일): 회원과 연관된 Todo 데이터를 저장
  2. Spring Security 설정
    • 회원가입 (Signup)과 로그인 (Login) 기능 구현
    • 인증된 회원만 자신의 Todo를 CRUD 가능하도록 보호
  3. 회원-할 일 연관
    • Member와 Todo 간의 연관관계를 매핑 (1)
  4. API 구성
    • 회원가입: /api/members/signup
    • 로그인: /api/login
    • Todo CRUD: /api/todos/**

 


Member 추가 및 스프링 시큐리티 로직 추가

일단 대표적으로 로그인 보안기능을 사용하려면,

  1. 스프링 시큐리티
  2. JWT 
  3. OAuth

방식의 인증방법이 있다.
각각의 특징들이 있어서, 상황 별로 잘 선택해서 쓰면 된다.

 

나는 개인적으로, 자체적으로 관리해보고 싶어서 스프링 시큐리티를 사용해서 Member 도메인을 만들었다.

일단 그레이들에 시큐리티 의존성이 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-security'

 

Member 클래스

package toyPJT.toypjt_v100.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@Entity
@Setter @Getter
@Table(name = "members")
public class Member implements UserDetails {

    @Id
    @Column(name = "member_id")
    @GeneratedValue
    private Long id;

    private String username;

    private String password;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        return UserDetails.super.isAccountNonExpired();
    }

    @Override
    public boolean isAccountNonLocked() {
        return UserDetails.super.isAccountNonLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return UserDetails.super.isCredentialsNonExpired();
    }

    @Override
    public boolean isEnabled() {
        return UserDetails.super.isEnabled();
    }
}

스프링 시큐리이테 필요한 UserDetails , 회원 인증 및 인가 정보를 조회할 수 있는 기능들을 제공해주는 구현체

 

레포지토리

package toyPJT.toypjt_v100.repository;


import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import toyPJT.toypjt_v100.domain.Member;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

    // 저장 또는 업데이트 (ID 유무에 따른 persist/merge 사용)
    public void save(Member member){
        if(member.getId() == null){
            em.persist(member);
        }else{
            em.merge(member);
        }
    }

    // 삭제 함수 추가
    public void delete(Long id) {
        Member member = em.find(Member.class, id); // ID로 엔티티를 조회
        if (member != null) {
            em.remove(member); // 엔티티가 존재하면 삭제
        }
    }

    public List<Member> findAll(){
        return em.createQuery("select i from Member i", Member.class)
                .getResultList();
    }

    public Member findOne(Long id){
        return em.find(Member.class, id);
    }

    public Member findByUsername(String username){
        return em.find(Member.class, username);
    }

}

일단 레포지토리는 Todo 도메인과는 동일하게 구성해주었다. 회원이름으로 조회하는 함수도 하나 추가했다.

 

 

서비스

package toyPJT.toypjt_v100.service;

import lombok.RequiredArgsConstructor;
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 org.springframework.transaction.annotation.Transactional;
import toyPJT.toypjt_v100.domain.Member;
import toyPJT.toypjt_v100.repository.MemberRepository;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    public void signup(String username, String password){
        // 비밀번호 암호화
        String encodedPassword = passwordEncoder.encode(password);

        // 회원가입
        Member member = new Member();
        member.setUsername(username);
        member.setPassword(encodedPassword);

        memberRepository.save(member);
    }

    // 사용자 이름으로 사용자의 정보를 가져오는 메서드
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByUsername(username);
        if(member == null) {
            throw new IllegalArgumentException(username);
        }

        return member;
    }
}

서비스 단에 UserDetailsService 구현체를 사용하여 loadUserByUsername함수를 구현해준다.
사용자의 인증 정보를 가져오는 메소드이다. 로그인시에 활용되는 기능인듯!!

그리고는 비밀번호 암호화를 하여 회원가입을 시켜주는 함수를 추가한다.

 

시큐리티컨피그 추가해주는 중요한 부분

package toyPJT.toypjt_v100.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig{

    // 빈 비밀번호 암호화를 위한 빈
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    // SecurityFilterChain을 사용한 보안 설정
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 (필요에 따라 활성화 가능)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/members/signup", "/api/login", "/").permitAll() // 회원가입 및 로그인 URL은 인증 없이 허용
                        .anyRequest().authenticated() // 나머지 요청은 인증 필요
                )
                .formLogin(form -> form
                        .loginPage("/api/login") // 로그인 페이지 URL
                        .defaultSuccessUrl("/api/todos", true) // 로그인 성공 시 리다이렉트 URL
                )
                .logout(logout -> logout
                        .logoutUrl("/api/logout") // 로그아웃 URL
                        .logoutSuccessUrl("/api/login") // 로그아웃 성공 후 리다이렉트 URL
                );
        return http.build();
    }
}

스프링 시큐리티의 중요한 부분으로 보안필터체인을 통해 어떤 부분에 보안을 둘지, 예외로 할지, 로그인 성공 시 , 로그아웃 시
어디로 리다이렉트할 지 정해주는 필터 기능을 한다.

비밀번호 암호화를 해주는 디코더도 빈으로 생성해준다.

 

 

 

일차적으로, 회원가입 API를 호출하고 성공적으로 멤버 테이블에 데이터가 저장된 것을 확인 할 수 있다.

 

추 후, 로그인 페이지를 구현하고 필터를 조금 더 수정하여 회원 별로 이용할 수 있도록 수정보완이 필요하다. 그리고 멤버와 투두를 연관관계 매핑을 통해 사용할 것 인지, QueryDSL로 조인을 하여 사용할 것인지에 대한 의사결정이 필요하다.

 

나는 개인적으로 실무에서 queryDSL를 다루고 있기 때문에 해당 방식으로 해 볼 예정!!

 

 

스프링부트는 기본적으로 타임리프와의 조합을 추천하며 jsp는 사용하지 않기를 권장한다.
jsp가 익숙해서 세팅했다가 뷰리졸버가 다 망가져서 한 며칠을 해멨다 ㅡㅡ 열받는다!!!!
자꾸 리졸버가 해당 뷰를 못찾아서 며칠을 해멨는데 결국, jsp 리졸버가 활성화되어있었다.
그래서, 설정파일에서 jsp사용 여부에 대한 값을 강제로 없앴다.

그리고 jpa에서 한번 컬럼이 만들어지면 재시작했을때도 계속 컬럼이 남아있기 때문에, 엔티티에서 지워도 소용이 없었다.
아예 설정값을 바꾸던가, 그런 컬럼들이 생성되는 것에 대해서는 수동으로 잘 지워줘야한다.!!!!!