종우의 삶 (전체 공개)

Spring security - 6 // 프로젝트 설명 - 회원 가입 본문

개발/Spring

Spring security - 6 // 프로젝트 설명 - 회원 가입

jonggae 2024. 1. 30. 19:19

대상 Repository : https://github.com/Jonggae/security

 

코드에 대한 코멘트는 commentary 브랜치에서도 확인할 수 있습니다.

https://github.com/Jonggae/security/tree/feature/6/commentary

 

참고자료:

인프런 Jwt 관련 영상 (github 코드 포함)

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt


Security는 추상화가 아주 많이.. 되어있어 나같은 초보 개발자는 한번에 파악하기 힘들다. SpringSecurity 내의 클래스들을 확인하고, 어떻게 구현되는지 살펴볼 필요가 있을것이다. 쉽지는 않겠지만, 여러 정보들이 많으니 직접 코드를 뜯어보고 조금만 집중하여 확인해보자. ^_^


개요

Spring Security를 사용하여 개발한 이번 프로젝트의 설명을 진행한다.

단순한 회원가입, 로그인 과정을 구현하였으며 Spring Security와 JWT를 사용하였다.

프로젝트 구성

더보기

프레임워크 버전

SpringFramework.Boot  '3.2.1'

 

추가 의존성

Lombok

JWT

Spring Data JPA

Spring Security

Spring Web

Validation

Thymeleaf (추후 개발에 필요)

 

데이터베이스

H2 Database

 

테스트

Security Test, Juint, AssertJ

 

■ Spring Security에서 JWT를 이용한 로그인 ■

 

0. 로그인 이전 회원가입

로그인을 하기 위해서는 서버 DB에 회원의 정보가 저장되어있어야 한다. 

 

register 메서드로 대표되는 회원가입 로직을 이용해 서버 DB에 데이터를 저장하게 된다.

 

회원가입 시 User 객체를 만들어 repository를 이용해 저장하게 되며, 테이블 이름은 [users]로 지정한다.

※ 테이블 명을 변경하지 않고 user로 사용하면 sql 에러가 발생한다. user는 sql 예약어로 존재하기때문.


User.java

package com.example.securitydemo.user.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.Set;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")

public class User {

    @Id
    @Column(name = "user_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "username", length = 50, unique = true, nullable = false)
    private String username;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @ManyToMany
    @JoinTable(name = "user_authority", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
    private Set<Authority> authorities;

}

 

User 엔티티는 id, username, password, email이 저장되며 DB에서 보여질 컬럼의 이름을 설정하고 필요한 옵션들을 지정해준다.

 

@ManyToMany 를 사용하여 User와 Authority(권한) 사이의 관계를 지어준다

User의 권한을 정해놓아야 하기 때문에 user_id 컬럼을 기준으로 연결을 한다.

authority_name 컬럼에는 ROLE_USER, ROLE_ADMIN 따위의 정보가 들어간다.


Authority.java

package com.example.securitydemo.user.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.*;

@Entity
@Table(name = "authority")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Authority {
    @Id
    @Column(name = "authority_name", length = 50)
    private String authorityName;
}

회원가입 시 User의 Authority를 설정하는 클래스이다.

 

UserService에서 기본적으로 회원가입 시 ROLE_USER로 생성된다.

 

다음은 User의 정보를 담아올 Dto를 만들어준다.

 

유저 엔티티를 담아올 UserDto권한 엔티티를 담아올 AuthorityDto를 사용한다.


UserDto.java

package com.example.securitydemo.user.dto;

import com.example.securitydemo.user.domain.User;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;

import java.util.Set;
import java.util.stream.Collectors;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {
    private String username;
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;
    private String email;

    private Set<AuthorityDto> authorityDtoSet;

    public static UserDto from(User user) {
        if (user == null) return null;

        return UserDto.builder()
                .username(user.getUsername())
                .email(user.getEmail())
                .authorityDtoSet(user.getAuthorities().stream()
                        .map(authority -> AuthorityDto.builder().authorityName(authority.getAuthorityName()).build())
                        .collect(Collectors.toSet()))
                .build();
    }
}

 

사용자로 부터 입력받은 User 엔티티 객체를 UserDto로 변환하기 위해 사용한다.AuthorityDto를 set으로 저장하여 권한 정보를 담는다.객체 생성에는 builder 패턴을 사용한 from 메서드를 만들었다.

 

UserDto 객체가 생성될 때 username, email, AuthorityDto set 이 생성되며getAuthorities() 메서드로 User 엔티티와 연결된 Authority 객체들을 AuthorityDto로 변환하고 set으로 수집한다.

 

-> 어쨌든 권한을 설정한다는 뜻


AuthorityDto.java

package com.example.securitydemo.user.dto;

import lombok.*;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthorityDto {
   private String authorityName;
}

 

 

UserService를 구현해준다.


UserService.java

package com.example.securitydemo.user.service;

import com.example.securitydemo.common.exception.NotFoundMemberException;
import com.example.securitydemo.common.security.SecurityUtil;
import com.example.securitydemo.user.domain.Authority;
import com.example.securitydemo.user.domain.User;
import com.example.securitydemo.user.dto.UserDto;
import com.example.securitydemo.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserDto register(UserDto userDto) {
        checkUserinfo(userDto.getUsername(), userDto.getEmail());
       
        Authority authority = Authority.builder()
                .authorityName("ROLE_USER").build();

        User user = User.builder()
                .username(userDto.getUsername())
                .password(passwordEncoder.encode(userDto.getPassword()))
                .email(userDto.getEmail())
                .authorities(Collections.singleton(authority))
                .build();
                
        return UserDto.from(userRepository.save(user));    
    }
    
    public void checkUserinfo(String username, String email) {
        checkUsername(username);
        checkEmail(email);
    }

    public void checkUsername(String username) {
        if (userRepository.findByUsername(username).isPresent()) {
            throw new IllegalArgumentException("이미 사용중인 아이디입니다");
        }
    }

    public void checkEmail(String email) {
        if (userRepository.findByEmail(email).isPresent()) {
            throw new IllegalArgumentException("이미 사용중인 이메일입니다");
        }
    }
}

 

UserService 에서는 회원가입 관련 메서드만 존재하며, 로그인 후에 권한에따라 필요한 메서드들이 추가될 예정이다.

 

register() 메서드를 이용해 회원가입을 진행한다.

 

UserDto에 담겨있는 정보를 기준으로,

checkUserinfo()를 통해 유저의 정보가 이미 들어있는지, 입력 데이터가 중복인지 확인후

 

Authority 권한 객체를 생성하고 (여기서 ROLE_USER 를 지정해준다. ADMIN이 필요하다면 메서드를 추가할 수 있을것이다.)

생성된 권한 객체와 함께 User 객체를 생성한다.

 

password는 공개되어선 안되므로 PassswordEncoder로 암호화 해준다. 

 

User 객체가 생성되면 from으로 User객체를 UserDto로 변환하고 userRepository.save()로 저장한다.


UserRepository.java

package com.example.securitydemo.user.repository;

import com.example.securitydemo.user.domain.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByUsername(String username);
    Optional<User> findByEmail(String email);
    @EntityGraph(attributePaths = "authorities")
    Optional<User> findOneWithAuthoritiesByUsername(String username);
}

 


controller에서 구현을 완료해준다.

추후 프론트 페이지를 만들예정이므로 포스트맨으로 api테스트만 진행한다.

 

ApiTestController.java

package com.example.securitydemo.user.controller;

import com.example.securitydemo.user.dto.UserDto;
import com.example.securitydemo.user.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class ApiTestController {
 
    private final UserService userService;

      @PostMapping("/register")
    public ResponseEntity<UserDto> register(@Valid @RequestBody UserDto requestDto) {
        return ResponseEntity.ok(userService.register(requestDto));
    }
}

 

엔드포인트를 /api/register로 지정하고 postman으로 요청을 보내본다.

 

 

 

요청이 정상적으로 들어왔음을 알 수 있다.

 

H2 DB에도 정보가 업데이트 되었다.

 

 

user_id에 맞추어 user_id = 3 의 권한이 지정되었다. (1, 2번 데이터는 미리 추가해놓은 것)

 

테스트코드도 작성하였으나 우선 api테스트로 진행하였다. 

 

이로써 회원 가입기능은 간단하게 완성되었다.

 

 

시큐리티를 이용한 로그인 과정은 다음 포스트에서 살펴보자.

Comments