개발(합니다)/Java&Spring

[spring boot 설정하기-9] oauth2 설정 및 테스트 소스

otrodevym 2021. 4. 23. 00:00
반응형

초기에는 오픈아이디 규격이 제각각이었으나

oauth2에서는 표준 규격에 맞게 로그인 API를 제공하고 있습니다.

기존 가입했던 플랫폼에서 인증을 대신하고 사용자의 정보를 가입하려는 플랫폼에 전달하는 방식입니다.

페이코 인증절차

X라는 서비스에서 가입 및 로그인을 간편로그인으로 했을 경우입니다.

X 서비스에서 인증 요청을 페이코에 하고 페이코는 사용자의 정보를 기반으로 인증 코드와 토큰을 발행합니다.

인증이 완료 되면 사용자는 기존에 페이코에 가입 했던 정보를 X 서비에서 그대로 사용할 수 있습니다.


1. 간편 로그인 플랫폼 생성(+google)

console.cloud.google.com/ 사이트에 접속합니다.

1-1. API 플랫폼 사이트에 접속

1-2. 인증 동의 작성

1-3. 사용자 인증 정보 만들기 - OAuth 클라이언트 ID 선택

1-4. 승인 원본과 리디렉션 URI 작성

테스트용이라 localhost를 사용하고 운영에서는 개별의 도메인을 사용합니다.

승인된 자바스크립트 원본 : http://localhost:9090
승인된 리디렉션 : http://localhost:9090/login/oauth2/code/google

1-5. 클라이언트 ID와 클라이언트 보안 비밀번호 확인

API를 사용하기 위한 클라이언트ID와 보안 비밀번호를 발급받고 노출되지 않도록 잘 보관합니다.

2. 의존성 추가

    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    implementation ('org.springframework.boot:spring-boot-starter-oauth2-client')

3. 패키지 구성 및 테스트 소스

패키지 구성은 아래 사진과 같습니다.

3-1. application.yml

구글의 경우에는 기본적으로 redirectUri가 login/oauth2/code/google로 잡혀있으니 주석처리했습니다.
카카오, 네이버와 같은 플랫폼에서는 redirectUri를 지정해주어야 합니다.

  security:
    oauth2:
      client:
        registration:
          google:
            client-id: {발급 받은 클라이언트 ID}
            client-secret: {발급 받은 보안 비밀번호}
#            redirectUri: /oauth2/callback/
            scope:
              - email
              - profile

3-2. WebSecurityConfig.java

@EnableWebSecurity : Spring Security 설정 활성
authorizeRequests : URL별 권한 관리를 설정을 시작
antMatchers : 권한 관리 대상 지정 URL, HTTP 메소드별로 관리 가능
anyRequest : 설정된 값들 이외 나머지 URL의 설정 지정
oauth2Login : OAuth 2 로그인 기능 설정을 시작
userInfoEndpoint : OAuth 2 로그인 성공 이후 사용자 정보를 가져올 시점의 설정
userService : 소셜 로그인 성공 시 후속 처리 시 UserService 인터페이스의 구현체를 등록

package com.otrodevym.spring.base.common.security.config;

import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthRole;
import com.otrodevym.spring.base.common.security.oauth.base.service.BaseCustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // security
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // security
    // 정적 자원에 대해서는 Security 설정을 적용하지 않음.
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    // oauth base

    private final BaseCustomOAuth2UserService baseCustomOAuth2UserService;


    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // security
    // oauth
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // ---------------  oauth2 base ------------------

        http.csrf().disable().headers().frameOptions().disable()
                .and()
                .authorizeRequests()
                .antMatchers("/", "css/**", "/images/**", "/js/**", "/h2/**", "/h2-console/**").permitAll()
                .antMatchers("/api/v1/**").hasRole(BaseAuthRole.USER.name())
                .anyRequest().authenticated()
                .and()
                .logout().logoutSuccessUrl("/")
                .and()
                .oauth2Login().userInfoEndpoint().userService(baseCustomOAuth2UserService);

    }


}

3-3. 테이블 생성

create table base_auth_user (
    id int not null auto_increment,
    name char(50) not null,
    email char(50) not null,
    picture varchar(3000) not null,
    created_date timestamp,
    modified_date timestamp,
    role char(50) not null,
    primary key (id)
    );

4. Oauth2 테스트 소스

4-1. BaseAuthRole.java

스프링 시큐리티의 권한 코드에는 항상 ROLE이 앞에 있어야합니다.

package com.otrodevym.spring.base.common.security.oauth.base.model;

import lombok.Getter;
import lombok.RequiredArgsConstructor;


@Getter
@RequiredArgsConstructor
public enum BaseAuthRole {

    GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER", "일반 사용자");

    private final String key;
    private final String title;


}

 

4-2. BaseTimeEntity.java

package com.otrodevym.spring.base.common.security.oauth.base.model;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDate;
import java.time.LocalDateTime;



@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // 감시 엔티티 등록
public class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;

}

4-3. BaseAuthUser.java

package com.otrodevym.spring.base.common.security.oauth.base.model;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;


@Getter
@NoArgsConstructor
@Entity
public class BaseAuthUser 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 BaseAuthRole role;

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

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

        return this;
    }

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

4-4. SessionUser.java

package com.otrodevym.spring.base.common.security.oauth.base.dto;

import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthUser;
import lombok.Getter;

import java.io.Serializable;


@Getter
public class SessionUser implements Serializable {

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

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

4-5. OAuthAttributes.java

package com.otrodevym.spring.base.common.security.oauth.base.dto;

import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthRole;
import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthUser;
import lombok.Builder;
import lombok.Getter;

import java.util.Map;


@Getter
public class OAuthAttributes {

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

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

    public static OAuthAttributes of(String registratioId, String userNameAttributeName,
                                     Map<String, Object> attribute) {
        return ofGoogle(userNameAttributeName, attribute);
    }

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

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

4-6. BaseAuthUserRepository.java

package com.otrodevym.spring.base.common.security.oauth.base.dao;

import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthUser;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;


public interface BaseAuthUserRepository extends JpaRepository<BaseAuthUser, Long> {

    Optional<BaseAuthUser> findByEmail(String email);
}

4-7. BaseCustomOAuth2UserService.java

registrationId : 간편 로그인 진행하는 플랫폼 코드
userNameAttributeName : OAuth2 로그인 진행 시 키가 되는 필드값으로 Primary Key와 같은 의미
- 구글 : "sub"
- 네이버, 카카오 : 지원안함
OAuthAttributes : OAuth2UserService를 통해 가져온 OAuth2User의 attribute 저장용도의 클래스
SessionUser : 세선 저장용 dto 클래스

package com.otrodevym.spring.base.common.security.oauth.base.service;

import com.otrodevym.spring.base.common.security.oauth.base.dao.BaseAuthUserRepository;
import com.otrodevym.spring.base.common.security.oauth.base.dto.OAuthAttributes;
import com.otrodevym.spring.base.common.security.oauth.base.dto.SessionUser;
import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthUser;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;
import java.util.Collections;



@RequiredArgsConstructor // 초기화 되지 않은 final 필드나, @NonNull 필드에 생성자를 생성해준다.
@Service
public class BaseCustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final BaseAuthUserRepository baseAuthUserRepository;
    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();

        // oauth2 로그인 시 키가 되는 필드
        String userNameAttributeName =
                userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();

        // OAuthAttributes attributes를 담을 클래스
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        BaseAuthUser authUser = saveOrUpdate(attributes);

        httpSession.setAttribute("user", new SessionUser(authUser));

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(authUser.getRoleKey())), attributes.getAttributes(),
                        attributes.getNameAttributKey()
        );
    }

    private BaseAuthUser saveOrUpdate(OAuthAttributes attributes) {
        BaseAuthUser authUser = baseAuthUserRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getPicture())).orElse(attributes.toEntity());
        return baseAuthUserRepository.save(authUser);
    }
}

4-8. BaseAuthController.java

package com.otrodevym.spring.base.common.security.oauth.base.contoller;

import com.otrodevym.spring.base.common.security.oauth.base.dto.SessionUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.HttpSession;


@Controller
@RequiredArgsConstructor
public class BaseAuthController {


    private final HttpSession httpSession;

    @GetMapping("/oauth/base")
    public String index(Model model) {

        SessionUser user = (SessionUser) httpSession.getAttribute("user");

        if (user != null) {
            model.addAttribute("email", user.getEmail());
            model.addAttribute("userName", user.getName());
            model.addAttribute("userImg", user.getPicture());
        }

        return "oauth/base/index";
    }
}

4-9. home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>home</title>
</head>
<body>
<a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>


<p th:text="${new java.util.Date().toString()}"></p>
<p th:text="${userName}"></p>
<p th:text="${email}"></p>

</body>
</html>

4-10. index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
      <a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>


      <p th:text="${userName}"></p>
      <p th:text="${email}"></p>
</body>
</html>

5. 실행 결과

반응형