[리팩토링] 간편로그인

윤재환·2025년 1월 22일

간편로그인 리팩토링 이유

기존 프로젝트에서는 간편로그인 기능의 주요 구현을 다른 인원이 담당하였고, 저는 해당 기능에서 발생하는 버그를 수정하고, 사용자 정보를 데이터베이스에 저장하는 로직을 추가로 구현했습니다.

그러나 작업 중 OAuth 2.0 표준과 비교했을 때 개선할 점을 발견했고, 이를 통해 간편로그인 기능을 보다 안정적이고 표준화된 방식으로 개선할 필요성을 느꼈습니다.

리팩토링의 주요 목표는 다음과 같습니다:
1. OAuth 2.0 표준 준수: Spring Security OAuth2 Client를 도입하여 인증 플로우를 보다 체계적이고 보안에 강한 방식으로 개선.
2. 유지보수성 향상: 기존의 수동 구현을 표준화된 라이브러리로 대체해 코드의 간결성과 확장성을 확보.
3. 학습과 성장: OAuth 2.0 표준을 깊이 이해하고 이를 실무에 적용해 개인적으로 성장할 기회로 삼음.


OAuth2.0 이란

OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준입니다.

OAuth 2.0은 1.0에서 알려진 보안 문제 등을 개선한 버젼입니다.

OAuth의 필요성

OAuth의 사용이유는 매 사이트마다 회원가입을 할 경우
사이트의 개수만큼 내 개인정보는 많이 뿌려지고 노출되게 됩니다.
개인정보를 지키기에 한 곳에서 관리하는게 보안에 더 좋기 때문입니다.

네이버나 카카오 같은 대형 포털 사이트들이 개인정보를 관리하고
이 2개의 사이트에서 로그인 처리를 대신해 줍니다.

정보 출처 : https://daramgda.tistory.com/80

webconfig 추가

OAuth2를 적용하기 위해서 경로 또한 수정할려고합니다.
HandlerInterceptor에서 인터셉터를 제외시켜 OAuth2 인증 플로우와 관련된 요청을 받기 위해서입니다.

OAuth2UserServiceImplement 클래스 생성

@Service
@RequiredArgsConstructor
public class OAuth2UserServiceImplement extends DefaultOAuth2UserService{
	
	private UserRepository userRepository;

	@Override
	public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2AuthenticationException{
		
		OAuth2User oAuth2User = super.loadUser(request);
		
		String oauthclientName = request.getClientRegistration().getClientName();
		
		try {
			System.out.println(new ObjectMapper().writeValueAsString(oAuth2User.getAttributes()));
			
		} catch (Exception exception) {
			exception.printStackTrace();
		}
		
		return oAuth2User;
	}
}

OAuth2인증 공급자(카카오 구글 네이버) 로부터 사용자 정보를 가져오는 과정을 처리합니다.
Spring Security의 기본 구현체인 DefaultOAuth2UserService를 확장하여 사용자 정보를 출력하거나 추가 로직을 추가하기 위해서 생성했습니다.

우선 loadUser메서드를 생성해서 공급자로부터 어떻게 정보가 들어오는지 확인이는 코드를 작성했습니다.

WebSecurityConfig 수정

이후 WebSecurityConfig에서 애플리케이션의 보안 정책을 정의를 합니다

.oauth2Login(oauth2 -> oauth2
                		.redirectionEndpoint(endpoint -> endpoint.baseUri("/oauth2/callback/*"))
                		.userInfoEndpoint(endpoint -> endpoint.userService(oAuth2UserService))
                		)

을 추가시켜줘서 OAuth2.0로그인 플로우를 처리하도록 설정합니다.

사용자 엔티티와 세션 관리 추가

OAuth2 인증 공급자로부터 사용자 정보를 가져오는 데 성공했다면, 이제 해당 정보를 데이터베이스에 저장하고 세션에 값을 추가하여 사용자가 애플리케이션을 이용할 수 있도록 구현해야 합니다.

이를 위해, OAuth2UserServiceImplement 클래스에 다음과 같은 의존성을 추가했습니다:

private final UserRepository userRepository;
	
	@Autowired
	private HttpServletRequest session;
  1. UserRepository
  • 사용자 정보를 데이터베이스에 저장하거나 조회하는데 사용합니다.
  1. HttpServletRequest
  • 인증 후 사용자 정보를 세션에 저장하여 상태를 유지합니다.

의존성 주입후 이제 받은 데이터를 데이터베이스에 저장하고 세션에 값을 추가하는 코드를 작성하겠습니다.

@Override
public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2AuthenticationException {
    OAuth2User oAuth2User = super.loadUser(request); // 기본 사용자 정보 로드
    String oauthClientName = request.getClientRegistration().getClientName(); // 인증 공급자 이름
    String userId = null;
    UserEntity userEntity = null;

    try {
        // 카카오 인증 처리
        if ("kakao".equals(oauthClientName)) {
            userId = "kakao_" + oAuth2User.getAttributes().get("id");

            // 데이터베이스에서 사용자 정보 조회
            userEntity = userRepository.findByUId(userId);

            if (userEntity == null) {
                // 새로운 사용자 생성 및 저장
                UserEntity newUser = new UserEntity();
                newUser.setUMail((String) oAuth2User.getAttributes().getOrDefault("kakao_account.email", ""));
                newUser.setUName((String) oAuth2User.getAttributes().getOrDefault("properties.nickname", "Unknown"));
                newUser.setURegister(LocalDate.now());
                newUser.setUPw(""); // 비밀번호는 빈 값으로 설정
                newUser.setUPofile((String) oAuth2User.getAttributes().getOrDefault("properties.profile_image", ""));
                newUser.setUBirth(""); // 생일 정보는 비워둠
                newUser.setUTel(""); // 전화번호 정보도 비워둠
                newUser.setUId(userId);

                // 데이터베이스에 사용자 저장
                userEntity = userRepository.save(newUser);
            }

            // 세션에 사용자 ID 저장
            request.getSession().setAttribute("UId", userEntity.getUId());
        }

    } catch (Exception e) {
        OAuth2Error error = new OAuth2Error("invalid_token", "Failed to process OAuth2 login", null);
        throw new OAuth2AuthenticationException(error, e.getMessage());
    }

    // 사용자 정보와 권한을 포함한 CustomOAuth2User 반환
    return new CustomOAuth2User(userEntity, oAuth2User.getAttributes());
}

작성한 코드를 조금 설명하겠습니다.

사용자 엔티티 저장
1. 사용자 식별:

  • 인증 공급자로부터 받은 사용자 ID를 기반으로 데이터베이스에서 해당 사용자를 조회합니다.
  • 카카오 사용자의 경우, kakao_<사용자 ID>형식으로 고유 식별자를 생성합니다.

신규 사용자 생성

  • 데이터베이스에 사용자가 없을 경우, 새로 생성하여 저장.
  • 이메일, 이름, 프로필 이미지 등의 정보를 공급자로부터 받아와 엔티티에 매핑.
  • 간편로그인에 필요없는 비밀번호(Upw), 생일(UBirth) 등은 빈 값으로 처리하고 이후 유저가 수정을 통해 값을 넣을수 있습니다.

세션에 값 저장

  • 세션에 사용자 고유 ID(UId)를 저장하여 로그인 상태를 유지합니다.
  • 세션 저장 방식:
request.getSession().setAttribute("UId", userEntity.getUId());

예외 처리

  • 인증 공급자로부터 사용자 정보를 가져오는 중 문제가 발생할 경우, 예외 처리하여 Spring Security가 인증 실패로 인식하게 합니다.
  • OAuth2AuthenticationException을 통해 인증 실패 상태로 반환합니다.

로그인 완료시 세션에 안들어가는 오류

HttpSession으로 구현했을때는

session.setAttribute("UId", existingUser.getUId());

이렇게 세션에 등록이 되었지만
HttpServletRequest을 사용했을때는

session.setAttribute("UId", existingUser.getUId());

로 실행이 되지 않았습니다.

찾아 보니 HttpSessionHttpServletRequest는 차이있어 그렇습니다.

HttpServletRequest는 세션값이 리다렉트후에 유지 되지않습니다

원인

  • HttpServletRequest.setAttribute()는 요청(Request) 범위에 데이터를 저장하며, 리다이렉트가 발생하면 새로운 요청이 생성되므로 데이터가 사라집니다.
  • 반면, 세션(session) 범위에 데이터를 저장하려면
    HttpServletRequest.getSession().setAttribute()를 사용해야 합니다.

해결 방법

  • HttpServletRequest에서 세션에 데이터를 저장하려면 getSession()을 호출해 야 합니다
request.getSession().setAttribute("UId", userEntity.getUId());
profile
백엔드 개발에 관심있는 1인

0개의 댓글