Spring Security 자체 Login 및 OAuth2 Login with Jwt (1)
들어가는 글
Spring Security를 사용하면서, 항상 이해가 부족한 느낌이었고, 이번에 자체 Login과 OAuth2 Login을 토큰 방식으로 구현하면서 삽질을 많이해서 부족한 부분을 공부하면서 정리하기로 했다.
목차 (후에 링크로 대체)
- 프로젝트 설정
- JWT란?
- Spring Security란?
- Spring security oauth2 client란? …….
초기 프로젝트 세팅
Spring Security 이해가 주 목적이므로 필수 Dependency로만 구성했다.
Dependencies
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…