[Aribnb] SpringSecurity와 OAuth2.0으로 로그인 기능 구현하기 3

장수현·2021년 11월 2일
0

스프링 시큐리티 설정

spring-boot-starter-oauth2-client 의존성 추가

  • 클라이언트 입장에서 소셜 기능 구현시 필요한 의존성
  • spring-security-oauth-client, spring-security-oauth-jose를 기본적으로 관리
    - JOSE(JavaScript Object Signing and Encryption
    JWT(JSON Web Tokens)의 권한을 안전하게 전송하는 프레임워크

/config/auth/ 패키지에 시큐리티 관련 클래스를 담는다.

SecurityConfig

@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/**").permitAll()
                        .antMatchers("/api/v1/**","/art/save").hasRole(Role.GUEST.name())
                        .anyRequest().authenticated()
                    .and()
                        .logout()
                            .logoutSuccessUrl("/")
                    .and()
                        .oauth2Login()
                            .userInfoEndpoint()
                                .userService(customOAuth2UserService);
    }
}

- @EnableWebSecurity
Spring Security 설정들을 활성화
- csrf().disable().headers().frameOptions().disable()
h2-console 화면을 사용하기 위해 해당 옵션들을 disable 한다.

  • csrf : Cross-Site Request Forgery

- .authorizeRequests()
URL별 권한 관리를 설정하는 옵션의 시작점. 선언되야만 antMatchers 사용 가능
- .antMatchers()~.permitAll(), .antMatchers()~.hasRole()
권한 관리 대상을 지정하는 옵션, URL/HTTP 메소드 별로 관리 가능
- .anyRequest()
설정된 값들 이외에 나머지 URL
- .authenticated()
인증된(로그인된) 사용자들에게만 허용
- .oauth2Login()
OAuth2 로그인 기능 설정 진입점
- .userService(customOAuth2UserService);
로그인 성공시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록
리소스 서버(소셜 서비스)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시

CustomOAuth2UserService 클래스 생성

구글 로그인 이후 가져온 사용자의 정보(email, name, picture)들을 기반으로 가입 및 정보수정, 세션 저장 등의 기능일 지원한다.

@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);

        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());

        //회원가입을 해야 Cart Entity에 userId를 넣을 수 있어서 먼저 User save
        User returnUser = userRepository.save(user);

        return returnUser;
    }

    public User findById(Long id) {
        return userRepository.findById(id).get();
    }

    public Boolean emptyCheck(Object obj) {
        if (obj instanceof String) return obj == null || "".equals(obj.toString().trim());
        else if (obj instanceof List) return obj == null || ((List) obj).isEmpty();
        else if (obj instanceof Map) return obj == null || ((Map) obj).isEmpty();
        else if (obj instanceof Object[]) return obj == null || Array.getLength(obj) == 0;
        else return obj == null;
    }
}

- HttpSession
둘 이상의 page request에서 사용자를 식별, 정보를 저장하는 방법을 제공
- registrationId
현재 로그인 중인 서비스를 구분하는 코드 (구글 또는 네이버등을 구분하)
- userNameAttributeName
OAuth2 로그인 진행 시 키가 되는 필드값이다. (=PK)
구글(sub)만 기본 지원, 이후에 네이버로그인과 구글로그인을 동시 지원할 때 사용
- SessionUser
세션에 사용자 정보를 저장하기 위한 DTO
- saveOrUpdate()
구글 사용자 정보가 업데이트 되었을 때 호출

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();
    }

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

- of
OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 하나하나 반환
- toEntity()
User 엔티티를 생성, OAuthAttributes에서 엔티티를 생성하는 시점은 처음가입 할 때 이다.

SessionUser에는 인증된 사용자 정보만 필요

@Getter
public class SessionUser implements Serializable {
    private Long id;
    private String name;
    private String email;
    private String picture;

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

0개의 댓글