[Spring] Social Login ②프로젝트 구현

박이레·2022년 10월 18일
0

Spring

목록 보기
3/12

 OAuth에서 가장 어려웠던 부분이 프로젝트 구현입니다. Spring Security를 완벽하게 알지 못해서 그랬습니다. 그래서 무작정 공책에 베껴썼습니다. 계속 베껴쓰다보니 이해가 됐습니다. 대신 어깨에 담이 왔습니다. 프로젝트 구현과 맞바꿈 셈입니다. 어깨에 걸린 담은 며칠 이내로 사라질 겁니다. 그러나 힘겹게 이해한 개념은 사라지지 않을 겁니다.

OAuth 프로젝트 구현을 하기 위해서는 준비물이 필요합니다. Spring Web Layer에 대해 알고 있다면 크게 어렵지 않습니다.



Entity

@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder
    public User(String name, String email, String picture, Role role) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }

    public User update(String name, String picture) {
        this.name = name;
        this.picture = picture;

        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}

Enum

Security 사용 시 ROLE_xx로 작성해야 합니다.

@Getter
@RequiredArgsConstructor
public enum Role {
	GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER", "일반 사용자");
    
    private final String key;
    private final String title;

Repository

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

SecurityConfig

SecurityConfig는 Spring Security 설정 파일입니다.

.csrf().disable().headers().frameOptions().disable은 h2-console 화면을 사용하기 위해 작성했습니다.

antMatchers()는 권한 관리 대상을 지정합니다.
antMatcher("/").permitAll()로 하게 되면 "/" 경로에 대해 전체 열람 권한을 부여합니다.

antMatchers("/user/**").hasRole(Role.User.name())은 "/user/**" 경로에 대해 Role이 user인 사용자만 열람할 수 있습니다.

anyRequest.authenticated()는 .antMatchers()에서 설정된 값 외의 나머지 경로에 대해 인증된 사용자만 열람할 수 있습니다.

oauth2Login().userInfoEndpoint()는 OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 사용합니다.


@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomOAuth2UserService customOAuth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .authorizeRequests()
                .antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
                .antMatchers("/api/v1/**").hasRole(Role.USER.name())
                .anyRequest().authenticated()
                .and()
                .logout()
                .logoutSuccessUrl("/")
                .and()
                .oauth2Login()
                .userInfoEndpoint()
                .userService(customOAuth2UserService);
    }
}

DTO

OAuthAttributes

@Getter
public class OAuthAttributes {

    private Map<String, Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;

    @Builder
    public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture) {
        this.attributes = attributes;
        this.nameAttributeKey = nameAttributeKey;
        this.name = name;
        this.email = email;
        this.picture = picture;
    }

    public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
        if ("naver".equals(registrationId)) {
            return ofNaver("id", attributes);
        }
        return ofGoogle(userNameAttributeName, attributes);
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
        return OAuthAttributes.builder()
                .name((String) attributes.get("name"))
                .email((String) attributes.get("email"))
                .picture((String) attributes.get("picture"))
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");

        return OAuthAttributes.builder()
                .name((String) response.get("name"))
                .email((String) response.get("email"))
                .picture((String) response.get("profile_image"))
                .attributes(response)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    public User toEntity() {
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }
}

SessionUser

@Getter
public class SessionUser implements Serializable {

    private String name;
    private String email;
    private String picture;

    public SessionUser(User user) {
        this.name = user.getName();
        this.email = user.getEmail();
        this.picture = user.getPicture();
    }
}

CustomOAuth2UserService

registrationId는 현재 로그인을 진행 중인 서비스(구글, 네이버 등)를 구분합니다.

userNameAttributeName은 테이블의 PK와 같은 개념입니다.

OAuthAttributes는 OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담은 클래스입니다.


@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        User user = saveOrUpdate(attributes);
        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
                attributes.getAttributes(),
                attributes.getNameAttributeKey());
    }

    private User saveOrUpdate(OAuthAttributes attributes) {
        User user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
                .orElse(attributes.toEntity());

        return userRepository.save(user);
    }
}


완성 코드

💁‍♂️reference

스프링 부트와 AWS로 혼자 구현하는 웹 서비스

이동욱 지음ㅣ프리렉ㅣ2019ㅣ도서 정보

EOD.

profile
혜화동 사는 Architect

0개의 댓글