본문 바로가기

개발(합니다)/Java&Spring

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

반응형

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

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. 실행 결과

반응형