3 minute read

들어가는 글

Spring Security를 사용하면서, 항상 이해가 부족한 느낌이었고, 이번에 자체 Login과 OAuth2 Login을 토큰 방식으로 구현하면서 삽질을 많이해서 부족한 부분을 공부하면서 정리하기로 했다.

목차 (후에 링크로 대체)

  1. 프로젝트 설정
  2. JWT란?
  3. Spring Security란?
  4. Spring security oauth2 client란? …….

초기 프로젝트 세팅

Spring Security 이해가 주 목적이므로 필수 Dependency로만 구성했다.

Dependencies 스크린샷 2023-02-24 오전 10 51 22

build.gradle

plugins {
  id 'java'
  id 'org.springframework.boot' version '2.7.9'
  id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.M0o0o0o'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
}

repositories {
  mavenCentral()
}

configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
  implementation 'org.springframework.boot:spring-boot-starter-security'
  implementation 'org.springframework.boot:spring-boot-starter-validation'
  implementation 'org.springframework.boot:spring-boot-starter-web'
  runtimeOnly 'com.h2database:h2'
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'org.springframework.security:spring-security-test'

  compileOnly 'org.projectlombok:lombok'
  testCompileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
  testAnnotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
  useJUnitPlatform()
}

User 엔티티 클래스 생성


@Entity
@Table(name = "USERS")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User {

    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "uuid2")
    @Column(columnDefinition = "BINARY(16)")
    private UUID id;
    private String password;
    private String email;
    private String nickname;
    private String socialId;

    @Enumerated(EnumType.STRING)
    private Provider provider;

    @Enumerated(EnumType.STRING)
    private Role role;

}
  • 유저 테이블의 pk는 UUID로 저장할 수 있도록 했다.
  • socialId는 social 로그인을 하면 각 Provider(ex: kakao)에서 고유한 ID값을 리턴해준다.
    • email은 추후에 해당 social 서비스에서 사용자가 이메일을 변경할 수 있기 때문에 사용자를 식별하는 값으로 사용하는 것은 좋지 않다.
    • 참고

Role

@AllArgsConstructor
public enum Role {
    ROLE_USER("ROLE_USER"),
    ;

    private String role;
}

Spring Security는 권한 관리를 해주는데, 예를 들면 사용자 인증(로그인) 후 요청에 대한 권한이 있는지 확인한다.

  • 권한이 있다면 계속해서 진행하고
  • 해당 자원에 대한 접근 권한이 없다면 접근을 거부한다.

Provider

@AllArgsConstructor
public enum Provider {
    LOCAL("local"), KAKAO("kakao"), GOOGLE("google"), NAVER("naver"),
    ;
    private String name;
}
  • Provider enum Class는 사용자의 가입 경로를 구분하기 위한 Enum 클래스다.
    • LOCAL : 자체 로그인
    • KAKAO : 카카오 로그인
    • NAVER : 네이버 로그인
    • GOOGLE : 구글 로그인

회원가입 로직

우선 자체, OAuth2 로그인을 진행하기 전에 자체 회원가입 로직이 필요하기에 작성했다.

User DTO


public class UserDTO {
  /**
   * user 회원가입 등록 DTO
   */
  @Data
    @NoArgsConstructor
    public static class UserSingUpDTO {
        @NotNull
        private String email;
        @NotNull
        private String password;
        @NotNull
        private String nickname;
    }

  /**
   * 유저 반환 DTO
   */
  @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class UserRes {
        private String email;
        private String nickname;
    }
}

User Controller


@Slf4j
@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @ExceptionHandler(IllegalArgumentException.class)
    public String handleIllegalArgumentEx(IllegalArgumentException e) {
        return e.getMessage();
    }

    @PostMapping("/user/signup")
    public UserRes signUp(@RequestBody @Validated UserSingUpDTO signUpDto, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            throw new IllegalArgumentException("입력 값을 확인해주세요.");
        }

        User savedUser = userService.save(signUpDto);

        return UserRes
                .builder()
                .email(savedUser.getEmail())
                .nickname(savedUser.getNickname())
                .build();
    }
}

원래는 이메일 형식과 길이 등 유효성 검증을 더 자세하게 해야 하지만, 필수 입력만 하면 이메일 검증만 진행한 후에 회원가입이 가능하게 작성했다.

User Service


@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public User save(UserSingUpDTO dto) {
        if (checkDeplicateEmail(dto.getEmail())) {
            throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
        }

        return userRepository.save(
                User
                        .builder()
                        .email(dto.getEmail())
                        .password(passwordEncoder.encode(dto.getPassword()))
                        .nickname(dto.getNickname())
                        .provider(Provider.LOCAL) // 자체 회원가입이기 때문에 Provider.LOCAL로 저장한다.
                        .role(Role.ROLE_USER) // ROLE_USER 권한 부여
                        .build()
        );
    }

    private boolean checkDeplicateEmail(String email) {
        Optional<User> user = userRepository.findByEmail(email);
        if(user.isPresent()) return true;
        return false;
    }

}

유저 서비스 로직은 입력받은 이메일의 중복만 체크한 후 이상히 없다면 회원가입이 가능하게 작성했다.

User Repository

public interface UserRepository extends JpaRepository<User, UUID> {

    Optional<User> findByEmail(String email); // 입력받은 이메일을 갖는 유저 조회
}

초기 Spring Security Config 작성

스프링 시큐리티 의존성을 추가한 상태에서 스프링 부트를 시작하면 스프링 시큐리티의 초기 설정이 적용된다. ex) Form Login
그래서 우선은 모든 요청을 허가해두었다.


@Configuration
/**
 * 스프링 시큐리티를 활성화한다.
 * debug = true로 설정하면 현재 등록된 스프링 시큐리티의 필터 목록을 터미널을 통해 확인할 수 있다.
 */
@EnableWebSecurity(debug = true) 
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .formLogin().disable()
                .httpBasic().disable()
                .csrf().disable();

        http
                .authorizeHttpRequests()
                .anyRequest().permitAll(); // 들어오는 어떤 요청도 모두 허가한다는 뜻

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

다음 시간에는 JWT에 대해 알아보자! To be continued…