-
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 에러가 나오지 않는다.