ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 11월 7일 화요일 TIL 회고록
    카테고리 없음 2023. 11. 8. 02:28

    OAuth2, 일반 로그인 동일하게 처리하기 -2


    간단한 복습

    일반 로그인과 소셜 로그인 시 발생하는 타입 문제 해결법

     

    이 문제는 인터페이스 다중구현(Multi-Implementation)을 통해 해결할 수 있다.

    Security가 관리하는 세션인 Security Session에서 Authentication 타입의 객체는 OAuth2User와 UserDetails 타입의 객체만 받을 수 있다. 하지만 PrincipalDetails가 두 인터페이스를 모두 구현한다면 일반 로그인, 소셜 로그인 구분 없이 같은 타입(PrincipalDetails)의 객체로 받아서 처리할 수 있다.

     

    PrincipalDetailsImpl.class (일반 로그인)

    @Getter
    public class PrincipalDetailsImpl implements UserDetails, OAuth2User {
    	// UserDetails를 구현함으로써 PrincipalDetails는 UserDetails와 같은 타입이 되었다.
    
      private final User user; // 콤포지션 변수
      private Map<String, Object> attributes; 
    
      // 일반 로그인
      public PrincipalDetailsImpl(User user) {
        this.user = user;
      }
    
      @Override
      public String getName() {
        return null;
      }
    
      @Override
      public Map<String, Object> getAttributes() {
        return null;
      }
    
     }

     

    다중 구현을 하기 위해 Map<String,Object> 를 리턴하는 getAttributes()와 getName() 메서드를 오버로드 하면 된다.

     

    PrincipalDetailsImpl.class (일반 로그인 ,OAuth2 로그인)

    @Getter
    public class PrincipalDetailsImpl implements UserDetails, OAuth2User {
    
      private final User user;
      private Map<String, Object> attributes;
    
      // 일반 로그인
      public PrincipalDetailsImpl(User user) {
        this.user = user;
      }
    
      // OAuth2 로그인
      public PrincipalDetailsImpl(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
      }
    
      @Override
      public String getName() {
        return (String) attributes.get("name");
      }
    
    
      @Override
      public Map<String, Object> getAttributes() {
        return attributes;
      }
    
      // 계정이 갖고 있는 권한 목록을 리턴하는 메서드이다.
      @Override
      public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRole role = user.getUserRole();
        String authority = role.getAuthority();
        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);
        return authorities;
      }
    
    }

     

    일반 로그인과 소셜 로그인 시 입력되는 정보가 다르기 때문에 이 차이를 생성자를 다르게 주어 처리하였고,

    Map<String, Object> 타입을 리턴하는 getAttributes() 메서드는 OAuth2(구글,카카오,네이버) 로부터 받은 사용자의 정보들을 

    Map<String, Object> 타입으로 리턴한다. 그리고 getName()은 소셜 로그인한 사용자의 이름을 리턴한다. 

     

     

    PrincipalDetailsImpl.class (최종)

    @Getter
    public class PrincipalDetailsImpl implements UserDetails, OAuth2User {
    
      private final User user;
      private Map<String, Object> attributes;
      private Long userId;
      private String username;
    
      // 일반 로그인
      public PrincipalDetailsImpl(User user, Long userId, String username) {
        this.user = user;
        this.userId = userId;
        this.username = username;
      }
    
      // OAuth2 로그인
      public PrincipalDetailsImpl(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
      }
    
      @Override
      public String getName() {
        return (String) attributes.get("name");
      }
    
      public User getUser() {
        return user;
      }
    
      public Long getUserId() {
        return userId;
      }
    
      public String getSocialId() {
        return user.getSocialId();
      }
    
    
      @Override
      public Map<String, Object> getAttributes() {
        return attributes;
      }
    
      // 계정이 갖고 있는 권한 목록을 리턴하는 메서드이다.
      @Override
      public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRole role = user.getUserRole();
        String authority = role.getAuthority();
        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);
        return authorities;
      }
    
      @Override
      public String getPassword() {
        return user.getPassword();
      }
    
      @Override
      public String getUsername() {
        return user.getUsername();
      }
    
      @Override
      public boolean isAccountNonExpired() {
        return false;
      }
    
      @Override
      public boolean isAccountNonLocked() {
        return false;
      }
    
      @Override
      public boolean isCredentialsNonExpired() {
        return false;
      }
    
      @Override
      public boolean isEnabled() {
        return false;
      }
    }

     

    Long userId, String username 필드를 추가하였다. OAuth2 로그인 유저를 찾을 때 소셜 로그인으로 찾아야 하므로 getSocialId 메서드도 만들었다.

     

    기존에 만들었던 UserDetailsService 클래스도 삭제하고 PrincipalDetailsService 클래스도 새로 만들었다. 이름 리팩토링 말고

    변경점은 없다.

     

    PrincipalDetailsService.class

    @Service
    @AllArgsConstructor
    public class PrincipalDetailsService implements UserDetailsService {
    
      private final UserRepository userRepository;
    
      @Override
      public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
        User user = userRepository.findByLoginId(loginId).orElseThrow(
            () -> new CustomException(ExceptionStatus.NOT_FOUND_USER)
        );
        if (user != null) {
          return new PrincipalDetailsImpl(user, user.getId(), user.getUsername());
        }
        return null;
      }
    }

     

    유저 엔티티 수정

    OAuth2 로그인 유저를 저장하기 위한 소셜 아이디, 네이버 로그인인지 카카오 로그인인지 구글 로그인인지 확인할 수 있게 Enum 타입 SocialType Enum 클래스를 만들었다. 

     

    User.class

    @Entity(name = "users")
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    public class User extends TimeStamp {
    	
      ...
    	
      private String socialId; // 로그인한 소셜 타입의 식별자 값 (일반 로그인의 경우에는 null)
    
      @Enumerated(EnumType.STRING)
      private SocialType socialType; // KAKAO, GOOGLE, KAKAO
      
      // 생성자 수정
      @Builder
      public User(String loginId, String username, String password, String email, String socialId,
          UserRole userRole, SocialType socialType, String imageUrl) {
        this.loginId = loginId;
        this.username = username;
        this.password = password;
        this.email = email;
        this.socialId = socialId;
        this.userRole = userRole;
        this.socialType = socialType;
        this.imageUrl = imageUrl;
      }
      
     
      }

     


    실행 후 나왔던 오류들


    1. principalName cannot be empty 에러

    OAuth2 로그인을 하면 해당 에러가 출력되었다. 

     

    시도했던 방법은

     

    해당 에러를 구글링 해보았더니 이 블로그가 나왔다. 이 블로그에서 적혀있는 해결 방법을 사용해봤다.

     

    UserDetails 인터페이스를 구현한 PrincipalUserDetailsImpl 클래스에 getUsername() 메서드의 내용을 구현하지 않았기 때문에 나오는 오류라고 한다.

      @Override
      public String getUsername() {
        return null;
      }

     

    그래서 아래와 같이 수정해주었다.

      @Override
      public String getUsername() {
        return user.getUsername();
      }

     

    하지만 오류가 고쳐지지 않아 getName() 메서드도 수정했다.

    @Override
    public String getName() {
      return (String) attributes.get("name");
    }

     

     

    getName 메서드를 수정했더니 이 에러는 나오지 않았다. 근데 이 메서드를 수정해서 고쳐진게 아니라 OAuth2 로그인 핸들러를 수정했더니 이 에러가 안나오는 것 같다..

     

    2. OAuth2LoginSuccessHandler 에서 authentication.getPrincipal() null 에러

     

    이 에러때문에 며칠 고생했었다.

     

    OAuth2LoginSuccessHandler.class

    @Override
      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
          Authentication authentication) throws IOException, ServletException {
    
        log.info("OAuth2 Login 성공!");
        try {
    
          User oAuth2User = (User) authentication.getPrincipal();
          
          ...
    }

     

    원래 authentication.getPrincipal()의 정보를 User 타입으로 받아 user.getRole이 GUEST 라면 액세스 토큰, 리프레시 토큰을 발급하고 요청 헤더에 실어서 인덱스 화면으로 리다이렉트 시키는 핸들러인데, OAuthUser가 null 값이 들어가 에러가 출력되는 문제가 발생했다.

    User oAuth2User = (User) authentication.getPrincipal();

     

    어떻게 수정해야할지 고민하다가 autheentication.getPrincipal()의 정보를 User 타입이 아닌 PrincipalUserDetailsImpl 타입으로 받도록 수정해봤다.

    PrincipalDetailsImpl oAuth2User = (PrincipalDetailsImpl) authentication.getPrincipal();

     

    authentication.getPrincipal() 정보가 담긴 oAuth2User을 User 타입으로 변환시켜서 액세스, 리프레시 토큰을 저장해야헸다.

    그래서 PrincipalDetailsImpl 클래스에 getSocialId 메서드를 만들어서 유저의 소셜아이디를 리턴시키게 만들었다.

     

    PrincipalDetailsImpl.class

      public String getSocialId() {
        return user.getSocialId();
      }

     

    authentication.getPrincipal() 정보가 담긴 oAuth2User에 

    userRepository 인터페이스에 만든 findBySocialId 메서드를 PrincipalUserDetailsImpl에 getSocialId 메서드를 사용해서 

    User 타입 oAuthUser에 유저 정보를 담았다.

    User oAuthUser = userRepository.findBySocialId(oAuth2User.getSocialId());

     

    onAuthenticatinSuccess 메서드

     

     @Override
      public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
          Authentication authentication) throws IOException, ServletException {
    
        log.info("OAuth2 Login 성공!");
        try {
    
          PrincipalDetailsImpl oAuth2User = (PrincipalDetailsImpl) authentication.getPrincipal();
    
          User oAuthUser = userRepository.findBySocialId(oAuth2User.getSocialId());
    
          // User의 role이 GUEST일 경우 처음 요청한 회원이므로 회원가입 페이지로 리다이렉트
          if (oAuthUser.getUserRole() == UserRole.GUEST) {
            String accessToken = jwtUtil.createAccessToken(oAuthUser.getUsername(),
                oAuthUser.getUserRole());
            String refreshToken = jwtUtil.createRefreshToken(oAuthUser.getUsername(),
                oAuthUser.getUserRole());
            redisDao.setValues(oAuthUser.getUsername(), refreshToken,
                Duration.ofMillis(JwtUtil.REFRESH_TOKEN_TIME));
            response.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);
            response.sendRedirect("http://127.0.0.1:5500/index.html"); 
            jwtUtil.sendAccessAndRefreshToken(response, accessToken, refreshToken);
            
          } else {
            loginSuccess(response, oAuthUser);
          }
        } catch (Exception e) {
          throw e;
        }

     

    실행 결과

    2023-11-07T21:24:38.541+09:00  INFO 76331 --- [nio-8080-exec-4] c.l.r.c.a.s.CustomOAuth2UserServiceImpl  : CustomOAuth2UserServiceImpl.loadUser() 실행 = OAuth2 로그인 요청 진입
    [Hibernate] select u1_0.id,u1_0.created_date,u1_0.email,u1_0.image_url,u1_0.last_modified_date,u1_0.login_id,u1_0.password,u1_0.social_id,u1_0.social_type,u1_0.user_role,u1_0.username from users u1_0 where u1_0.social_type=? and u1_0.social_id=?
    [Hibernate] insert into users (id, created_date, email, image_url, last_modified_date, login_id, password, social_id, social_type, user_role, username) values (default, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    2023-11-07T21:24:42.406+09:00  INFO 76331 --- [nio-8080-exec-4] c.l.r.c.a.h.OAuth2LoginSuccessHandler    : OAuth2 Login 성공!
    [Hibernate] select u1_0.id,u1_0.created_date,u1_0.email,u1_0.image_url,u1_0.last_modified_date,u1_0.login_id,u1_0.password,u1_0.social_id,u1_0.social_type,u1_0.user_role,u1_0.username from users u1_0 where u1_0.social_id=?

     

     소셜 로그인 시 null 에러가 나오지 않는다.

     

     

     

Designed by Tistory.