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가지 타입

https://youtu.be/7hrw03uzQHE

+ Recent posts