반응형

들어가기 전 

처음 프로젝트 진행 시, 아무것도 모르던 상태에서 처음으로 만들어야겠다고 생각이 든 기능이

로그인 기능이었습니다.

처음부터 자체 Login과 OAuth2 Login(소셜 로그인)을 같이 구현해보고 싶었지만, 

그 당시에는 지식이 많이 없었기 때문에(지금도 없지만...)

먼저 자체 Login을 구현해보고 OAuth2 Login을 구현해봐야겠다고 생각을 했었습니다.

 

구글링을 정말 많이 해봤었는데, 자체 Login과 OAuth2 Login을 함께 서술한 블로그 글은 거의 없었습니다.

그래서 구현하기가 정말 힘들었었는데 여러 삽질을 통해 구현하게 되어 기록하고자 합니다!

물론 더 나은 방법이 있을 수도 있지만, 일단 스프링 시큐리티와 JWT를 이용하여

자체 Login과 OAuth2 Login을 함께 구현한 제 코드들을 설명드리고자 합니다!

 

저는 스프링 시큐리티, JWT를 이용한 로그인 원리 같은 부분은 간략하게 넘어가고

코드 위주로 설명할 계획입니다.

원리 부분은 밑의 블로그를 거의 80~90% 참고하여 구현했기 때문에 이해가 안 되시는 부분이 있으면

다음 블로그의 게시판 만들기 로그인 과정과 스프링 시큐리티 로그인 과정을 참고하시면

더 이해가 잘 되실 것 같습니다!

덧붙여 처음 프로젝트 진행 시 막막했었는데,

해당 블로그에서 많이 배웠기 때문에 주인분한테 너무 감사하단 말씀드리고 싶습니다!

https://ttl-blog.tistory.com/267

 

스프링부트 게시판 API 만들기 (2) - JSON 로그인 (1) - 회원 엔티티 설계

사실 어떻게 진행해야 할 지 고민을 조금 했습니다. 로그인을 먼저 하는게 맞는건지, 기능 구현이 먼저일지... 그런데 최근에 시큐리티를 공부하기도 했고, JWT 관련해서 공부를 조금 해보고 싶은

ttl-blog.tistory.com

https://ttl-blog.tistory.com/97

 

[Security] Spring Security - OAuht2 로그인 작동원리

요즘은 OAuth2를 사용하여 소셜 로그인을 통해 가입이 가능한 서비스들이 굉장히 많다. 스프링 시큐리티에서도 OAuth2를 이용한 소셜 로그인 방식을 지원하는데, 필자는 맨 처음 이를 사용할때 너

ttl-blog.tistory.com

 


회원 (User) 관련 클래스 생성

User 관련 패키지 구조

User 관련 패키지 구조

 

1. User Entity 생성


      
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Builder
@Table(name = "USERS")
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String email; // 이메일
private String password; // 비밀번호
private String nickname; // 닉네임
private String imageUrl; // 프로필 이미지
private int age;
private String city; // 사는 도시
@Enumerated(EnumType.STRING)
private Role role;
@Enumerated(EnumType.STRING)
private SocialType socialType; // KAKAO, NAVER, GOOGLE
private String socialId; // 로그인한 소셜 타입의 식별자 값 (일반 로그인인 경우 null)
private String refreshToken; // 리프레시 토큰
// 유저 권한 설정 메소드
public void authorizeUser() {
this.role = Role.USER;
}
// 비밀번호 암호화 메소드
public void passwordEncode(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(this.password);
}
public void updateRefreshToken(String updateRefreshToken) {
this.refreshToken = updateRefreshToken;
}

 ✅ @Table(name = "USERS")

현재 프로젝트 DB로는 간단하게 사용할 수 있는 H2를 사용하는데,

H2 상위 버전에서는 'user' 키워드가 예약어로 지정되어 있어서

'user' 엔티티를 테이블로 JPA가 맵핑할 때 에러가 발생할 수 있습니다.

따라서 @Table로 예약어가 아닌 이름을 지정해줬습니다.

 

✅ User 정보 관련 필드

  • email : 이메일
  • password : 비밀번호
  • nickname : 닉네임
  • imageUrl : 프로필 이미지 URL
  • age : 나이 (추가 정보)
  • city : 사는 도시 (추가 정보)

자체 로그인에서는 추가 정보인 age, city들을 회원가입 화면에서 입력 받겠지만,

OAuth2 로그인은 추가 정보인 age, city를 로그인 시 따로 받지 않으므로,

이후에 OAuth2 로그인 구현 시에 로그인이 성공하면 추가 정보를 입력하는 폼으로 이동하도록 구현할 것입니다.

 

 ✅ 권한 관련 ENUM - Role 필드

이후에 Role Enum 클래스 생성 부분에서 언급하겠지만, USER의 Role은 GUEST와 USER로 구분합니다.

이는 자체 로그인 시에는 상관없이 회원가입 시 USER로 Role을 정하여 DB에 저장합니다.

OAuth2 로그인 시에는 첫 로그인 시에 Role을 Guest로 설정하고,

추가 정보 입력 시 User로 업데이트 되게 구현할 것입니다.

 

✅ OAuth2 Type 관련 ENUM - SocialType 필드

Social Type이 저장되는 필드입니다. (NAVER, KAKAO, GOOGLE 등)

 

✅ 소셜 타입의 식별자 값 필드

로그인한 소셜 타입의 식별자 값이 저장되는 필드입니다. (자체 로그인의 경우 null)

 

✅ JWT RefreshToken 필드

JWT를 사용하여 로그인 성공 시 AccessToken, RefreshToken을 발행하는데

발행된 RefreshToken을 User Entity에 저장합니다.

 

✅ 여러 메소드

유저 권한 설정 메소드, 비밀번호 암호화 메소드, 리프레시 토큰 재발급 메소드 등이 존재합니다.

 

 

2. 유저 권한 Enum Role 생성


      
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST"), USER("ROLE_USER");
private final String key;
}

앞서 말했던 것 처럼, OAuth2 로그인 시 첫 로그인을 구분하기 위해

Role에 Guest를 추가했습니다.

구현한 프로젝트가 작은 사이드 프로젝트였기 때문에 따로 관리자(ADMIN)는 만들지 않았습니다.

추가하여 구현해도 좋을 것 같습니다.

key 필드를 추가하여 "ROLE_"을 붙인 이유는, 스프링 시큐리티에서는 권한(Role) 코드에

항상 "ROLE_" 접두사가 앞에 붙어야 하기 떄문에 "ROLE_"을 설정해주었습니다.

 

3. 소셜 타입 구분 Enum SocialType 생성


      
public enum SocialType {
KAKAO, NAVER, GOOGLE
}

별도의 로직 없이 단순히 소셜 타입을 구분하기 위한 SocialType ENUM을 생성했습니다.

 

 

 

4. UserRepository 생성


      
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByNickname(String nickname);
Optional<User> findByRefreshToken(String refreshToken);
/**
* 소셜 타입과 소셜의 식별값으로 회원 찾는 메소드
* 정보 제공을 동의한 순간 DB에 저장해야하지만, 아직 추가 정보(사는 도시, 나이 등)를 입력받지 않았으므로
* 유저 객체는 DB에 있지만, 추가 정보가 빠진 상태이다.
* 따라서 추가 정보를 입력받아 회원 가입을 진행할 때 소셜 타입, 식별자로 해당 회원을 찾기 위한 메소드
*/
Optional<User> findBySocialTypeAndSocialId(SocialType socialType, String socialId);
}

스프링 데이터 JPA를 사용하여 UserRepository를 생성하였습니다.

여기서 findBySocialTypeAndSocialId() 메소드는 OAuth2 로그인 구현 시 사용하는 메소드입니다.

User Entity 생성 부분에서 언급했지만,

OAuth2 로그인 시 정보 제공을 동의하여 로그인을 진행하면,

추가 정보를 제외한 정보를 받아 DB에 저장됩니다. 

따라서 이후에 추가 정보를 입력하기 위해 해당 회원을 DB에서 찾는 메소드가 필요합니다.

그때, DB에 소셜 타입(NAVER, KAKAO, GOOGLE, ...)과 소셜 식별값이 저장되기 떄문에

이 두 가지로 추가 정보를 입력하지 않은 회원을 가져오는 메소드를 추가했습니다.

 

 

5. UserSignUpDto 생성


      
@NoArgsConstructor
@Getter
public class UserSignUpDto {
private String email;
private String password;
private String nickname;
private int age;
private String city;
}

자체 로그인 회원 가입 API에 RequestBody로 사용할 UserSignUpDto를 생성했습니다.

자체 로그인이기 때문에, 추가 정보인 age와 city도 포함하여 요청합니다.

 

 

6. UserService 생성


      
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public void signUp(UserSignUpDto userSignUpDto) throws Exception {
if (userRepository.findByEmail(userSignUpDto.getEmail()).isPresent()) {
throw new Exception("이미 존재하는 이메일입니다.");
}
if (userRepository.findByNickname(userSignUpDto.getNickname()).isPresent()) {
throw new Exception("이미 존재하는 닉네임입니다.");
}
User user = User.builder()
.email(userSignUpDto.getEmail())
.password(userSignUpDto.getPassword())
.nickname(userSignUpDto.getNickname())
.age(userSignUpDto.getAge())
.city(userSignUpDto.getCity())
.role(Role.USER)
.build();
user.passwordEncode(passwordEncoder);
userRepository.save(user);
}
}

자체 로그인 회원 가입 시 사용하는 회원 가입 API의 로직입니다.

자체 로그인이기 때문에 클라이언트 요청에서 age, city 등 추가 정보까지 다 받아와서

Builder로 추가 정보를 포함한 User Entity 생성 후 DB에 저장합니다.

 

 

 

7. UserController 생성


      
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/sign-up")
public String signUp(@RequestBody UserSignUpDto userSignUpDto) throws Exception {
userService.signUp(userSignUpDto);
return "회원가입 성공";
}
@GetMapping("/jwt-test")
public String jwtTest() {
return "jwtTest 요청 성공";
}
}

UserController를 생성하여 자체 로그인 회원 가입 API를 구현했습니다.

"/sign-up" URI로 요청이 오게 되면, 자체 회원가입이 되게 됩니다.

 

※ "/jwt-test" URI는 이후의 JWT 서비스 테스트를 위한 API입니다.

(모든 글에서 테스트 코드는 다루지 않을 예정입니다.

테스트 코드가 궁금하시다면 맨 아래의 Reference 블로그를 참고해주세요!)


📖 깃허브 링크 (전체 코드)

https://github.com/KSH-beginner/oauth2WithJwtLogin

 

GitHub - KSH-beginner/oauth2WithJwtLogin

Contribute to KSH-beginner/oauth2WithJwtLogin development by creating an account on GitHub.

github.com


📕 전체 로그인 구현 목차

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (1) - 회원(User) 관련 클래스 생성

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (2) - JWT란?

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (3) - JWT 관련 클래스 생성 / JWT 인증 로직

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (4) - 자체 JSON 로그인 커스텀하기

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (5) - OAuth란? / OAuth 2.0 인증 과정 예시

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (6) - OAuth 2.0 로그인 구현 사전 설정

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (7) - OAuth 2.0 로그인 관련 클래스 생성

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (8) - SecurityConfig 설정 클래스 생성

Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (9)- JWT 자체 로그인 & OAuth2 Login 테스트


Reference

https://ttl-blog.tistory.com/267

 

스프링부트 게시판 API 만들기 (2) - JSON 로그인 (1) - 회원 엔티티 설계

사실 어떻게 진행해야 할 지 고민을 조금 했습니다. 로그인을 먼저 하는게 맞는건지, 기능 구현이 먼저일지... 그런데 최근에 시큐리티를 공부하기도 했고, JWT 관련해서 공부를 조금 해보고 싶은

ttl-blog.tistory.com

https://ttl-blog.tistory.com/97

 

[Security] Spring Security - OAuht2 로그인 작동원리

요즘은 OAuth2를 사용하여 소셜 로그인을 통해 가입이 가능한 서비스들이 굉장히 많다. 스프링 시큐리티에서도 OAuth2를 이용한 소셜 로그인 방식을 지원하는데, 필자는 맨 처음 이를 사용할때 너

ttl-blog.tistory.com

 

반응형
BE_성하