SecurityContext
세션 방식으로 로그인 구현시, 스프링 시큐리티는
세션 공간 안에
시큐리티 세션이라는 자신만의 공간을 갖는다.
SecurityContext 객체를 Http Session 에 저장해서
스프링 시큐리티 전용 세션 쓰는듯.
JWT 토큰 방식으로 구현시
SecurityContext를 세션에 저장하지 않는다. 즉,
하나의 요청을 처리할 때 SecurityContext 하나가 생성되고
응답이 끝나면 버려진다.
SecurityContextHolder 클래스에는
SecurityContextHolderStrategy 타입 객체가 들어있고
SecurityContextHolderStrategy 타입 객체에는
SecurityContext 가 들어있다.
strategy 라는게
SecurityContext를 HttpSession에 저장할건지, 아니면 뭐 다른방식으로 저장할건지 그런것같다.
(SecurityContext 저장 방식에 대한 전략인 것 같다는 소리임)
Authentication 객체와 UserDetails & OAuth2User
SecurityContext 에는
들어갈 수 있는 객체 타입이 정해져있다.
그것은 Authentication 타입이다.
Authentication 타입 객체에는 User 정보를 저장한다.
Authentication 안에도 저장할 수 있는 객체의 타입이 정해져있다.
그것은 UserDetails 타입과 OAuth2User 타입이다.
이 둘 중 하나여야, Authentication 객체 안에 저장할 수 있다.
서드파티 없이 회원가입-로그인 프로세스를 직접 구현한 경우 UserDetails 타입 객체를 저장하게 된다.
소셜로그인 구현시에는 OAuth2User 타입 객체를 저장하게 된다.
UserDetails & OAuth2User
세션 방식에서 사용자가 로그인 할 경우
소셜로그인이 아닌 방식으로 로그인한 사용자 정보는 UserDetails 타입 객체로,
소셜로그인으로 로그인한 사용자 정보는 OAuth2User 타입 객체로
세션에 저장하게 된다.
그리고 이후에, 만약 내정보 조회 API 를 만든다면
소셜로그인 사용자 정보 조회 기능은
세션에서 OAuth2User 타입 객체를 꺼내서 써야하고
일반로그인 사용자 정보 조회 기능에서는
세션에서 UserDetails 타입 객체를 꺼내서 써야한다.
이렇게 구현하려면
똑같은 "사용자 정보 조회 API"를
로그인 방식에 따라서 두개 마련해야 하는데,
이건 좀 너무 코드가 별로다.
근데 방법이 있다. 바로!!
UserDetails와 OAuth2User를 둘 다 implement 하는 클래스를 만드는 것이다!
사실 JWT 방식에서는
소셜로그인한 사용자도 필터에서 UserDetails 로 저장하거나,
일반로그인한 사용자를 필터에서 OAuth2User 로 저장하거나
이런식으로도 가능할 것 같다.
하지만 일반로그인한 사용자를 OAuth2User로 저장하는건 의미상 이상하고
소셜로그인한 사용자를 UserDetails 로 저장하는것도
registrationId (google인지 naver인지 이런거) 같은 일부정보가 누락될 수도 있을 것 같아서 그다지 좋아보이지 않는다.
따라서 나는 JWT 방식으로 구현하고 있지만 이렇게 진행할 것이다.
구현
package com.yelim.security.domain;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
public class UserPrincipal implements OAuth2User, UserDetails {
private User user;
private List<GrantedAuthority> authorities;
private Map<String, Object> oauthUserAttributes;
private UserPrincipal(User user, List<GrantedAuthority> authorities,
Map<String, Object> oauthUserAttributes) {
this.user = user;
this.authorities = authorities;
this.oauthUserAttributes = oauthUserAttributes;
}
/**
* OAuth2 로그인시 사용
*/
public static UserPrincipal create(User user, Map<String, Object> oauthUserAttributes) {
return new UserPrincipal(user, List.of(() -> "ROLE_USER"), oauthUserAttributes);
}
public static UserPrincipal create(User user) {
return new UserPrincipal(user, List.of(() -> "ROLE_USER"), new HashMap<>());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return String.valueOf(user.getEmail());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Map<String, Object> getAttributes() {
return Collections.unmodifiableMap(oauthUserAttributes);
}
@Override
@Nullable
@SuppressWarnings("unchecked")
public <A> A getAttribute(String name) {
return (A) oauthUserAttributes.get(name);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.unmodifiableList(authorities);
}
@Override
public String getName() {
return String.valueOf(user.getEmail());
}
}
- getUsername(), getName()
User를 authentication 할 때 사용할 username을 return.
만약 이메일로 authentication 할 경우 username 으로 email을 리턴하게하면 된다. - getAttributes()
우리 서버에서 구글 access token 을 얻은 다음
이 access token 으로 구글한테 사용자 정보를 알려달라고 요청하면
구글에서 응답을 하는데
이 때 사용자 정보가 attributes 안에 담아져서 온다.
attribues가 Map 형식이니까 { "email" : "yelim@gmail.com" } 이런식으로!
package com.yelim.security.domain;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.GrantedAuthority;
class UserPrincipalTest {
private User user;
@BeforeEach
void setUp() {
user = new User(1L, "Kim98", "testPassword");
}
@Test
void create() {
UserPrincipal userPrincipal = UserPrincipal.create(user);
assertThat(userPrincipal.getName()).isEqualTo(user.getEmail());
}
/**
* OAuth2 로그인시 사용
*/
@Test
void createWithAttributes() {
// given
Map<String, Object> oauthUserAttributes = new HashMap<>();
oauthUserAttributes.put("email", "test@email.com");
UserPrincipal userPrincipal = UserPrincipal.create(user, oauthUserAttributes);
// when
Map<String, Object> attributes = userPrincipal.getAttributes();
// then
assertThat(userPrincipal.getName()).isEqualTo(user.getEmail());
assertThat(attributes).hasSize(1);
assertThat(attributes.get("email")).isEqualTo(oauthUserAttributes.get("email"));
}
@Test
void getAttribute() {
// given
Map<String, Object> oauthUserAttributes = new HashMap<>();
oauthUserAttributes.put("email", "test@email.com");
UserPrincipal userPrincipal = UserPrincipal.create(user, oauthUserAttributes);
// when
Object email = userPrincipal.getAttribute("email");
// then
assertThat((String) email).isEqualTo("test@email.com");
}
@Test
void getPassword() {
UserPrincipal userPrincipal = UserPrincipal.create(user);
assertThat(userPrincipal.getPassword()).isEqualTo(user.getPassword());
}
@Test
void getUsername() {
UserPrincipal userPrincipal = UserPrincipal.create(user);
assertThat(userPrincipal.getUsername()).isEqualTo(user.getEmail());
}
@Test
void getAuthorities() {
// given : UserPrincipal 객체가 만들어져 있다.
UserPrincipal userPrincipal = UserPrincipal.create(user);
// when : authorities 를 조회한다.
Collection<? extends GrantedAuthority> authorities = userPrincipal.getAuthorities();
Optional<? extends GrantedAuthority> first = authorities.stream().findFirst();
// then : authorities 가 조회된다.
assertThat(first.isPresent()).isTrue();
assertThat(first.get()).isInstanceOf(GrantedAuthority.class);
assertThat(first.get().getAuthority()).isEqualTo("ROLE_USER");
}
@Test
void getAuthorities_whenOAuth2Login() {
// given
Map<String, Object> oauthUserAttributes = new HashMap<>();
oauthUserAttributes.put("email", "test@email.com");
UserPrincipal userPrincipal = UserPrincipal.create(user, oauthUserAttributes);
// when : authorities 를 조회한다.
Collection<? extends GrantedAuthority> authorities = userPrincipal.getAuthorities();
Optional<? extends GrantedAuthority> first = authorities.stream().findFirst();
// then : authorities 가 조회된다.
assertThat(first.isPresent()).isTrue();
assertThat(first.get()).isInstanceOf(GrantedAuthority.class);
assertThat(first.get().getAuthority()).isEqualTo("ROLE_USER");
}
@Test
void getName() {
// given
Map<String, Object> oauthUserAttributes = new HashMap<>();
oauthUserAttributes.put("email", "test@email.com");
UserPrincipal userPrincipal = UserPrincipal.create(user, oauthUserAttributes);
// when & then
assertThat(userPrincipal.getName()).isEqualTo(user.getEmail());
}
@Test
void getAttributes() {
// given
Map<String, Object> oauthUserAttributes = new HashMap<>();
oauthUserAttributes.put("email", "test@email.com");
UserPrincipal userPrincipal = UserPrincipal.create(user, oauthUserAttributes);
// when
Map<String, Object> attributes = userPrincipal.getAttributes();
// then
assertThat(attributes).hasSize(1);
assertThat(attributes.get("email")).isEqualTo(oauthUserAttributes.get("email"));
}
@Test
void isAccountNonExpired() {
}
@Test
void isAccountNonLocked() {
}
@Test
void isCredentialsNonExpired() {
}
@Test
void isEnabled() {
}
}
참고
Authentication 객체가 가질 수 있는 2가지 타입
'Spring Security' 카테고리의 다른 글
소셜로그인 7. OAuth2AuthenticationSuccessHandler (0) | 2021.09.06 |
---|---|
소셜로그인 6. 커스텀 OAuth2UserService 만들기 (1) | 2021.09.06 |
소셜로그인 4. 토큰 정보 정의 (0) | 2021.09.05 |
소셜로그인 3. Spring Security 의존성 및 yml (0) | 2021.09.05 |
소셜로그인 2. 구글 콘솔에서 Oauth 앱 생성하기 - 승인된 리다이렉션 URI란 (1) | 2021.09.05 |