티스토리 뷰





반응형

1. 구글 계정 만들기

먼저 OAuth 동의 화면에서 이름 짓고 밑에 3개 잘 등록되었는지만 확인하고 생성

 

제일 상단처럼 프로젝트 이름을 짓고 왼쪽 햄버거 버튼으로 사용자 인증 정보로 넘어오면 오른쪽 네모칸 처럼 클릭

 

승인된 리디렉션 URI

  • 서비스에서 파라미터로 인증 정보를 주었을 때 인증이 성공하면 구글에서 리다이렉트할 URL입니다.
  • 스프링 부트 security 2.0 부터는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL을 지원하고 있습니다.
  • 사용자가 별도로 리다이렉트할 URL을 지원하는 컨트롤러를 만들 필요가 없습니다. 시큐리티에서 구현됨
  • 현재는 개발 상태이므로 위와 같이 적었고 포트는 알아서 맞춰서 사용하세요(기본 8080)

만들기 버튼으로 생성된 OAuth 2.0 클라이언트 ID 을 클릭하면 클라이언트 ID와 클라이언트 보안 비밀키가 확인이 된다.

 

이제 인텔리제이로 가서 설정을 하자.

 

src/main/resources 하위에 application-oauth.properties 파일을 생성합니다. 그리고 아래와 같이 작성.

spring.security.oauth2.client.registration.google.client-id = 클라이언트 ID
spring.security.oauth2.client.registration.google.client-secret = 클라이언트 보안 비밀
spring.security.oauth2.client.registration.google.scope = profile, email

강제로 profile과 email을 등록한 이유는 openid라는 scope가 있으면 Opne id Provider로 인식하기 때문입니다. 이렇게 되면 OpenId Provider인 서비스(구글)와 그렇지 않은 서비스(네이버/카카오)로 나눠서 각각 OAuth2Service를 만들어야 하기 때문입니다. 무슨 소린지 모르겠죠? 일단 진행합시다 저도 어려워요 ㅋㅋ

 

스프링 부트에서는 properties의 이름을 application-xxx.properties로 만들면 xxx라는 이름의 profile이 생성되어 이를 통해 관리할 수 있습니다. 즉 profile=xxx 라는 식으로 호출하면 해당 프로퍼티의 설정들을 가져올 수 있습니다. 그렇기에 같은 위치에 application.properties을 만들어 아래와 같이 옵션을 추가합시다.

spring.profiles.include = oauth

아그리고 여기까지 진행 했을 경우 application-oauth 파일은 절대 깃허브에 올라가면 안되니 ignore파일에 꼭 추가하시고 혹시라도 실수 해서 올라갔다면 지우고 다시 만들거나 하셔야 합니다. 보안키는 절대 털리면 안됩니다.

(혹시 .gitignore에 추가했는데도 커밋 목록에 노출되면 https://jojoldu.tistory.com/307 을 참고하세요)

 

사용자의 정보를 담당할 도메인인 User 클래스 생성

@Getter
@NoArgsConstructor
@Entity
public class User {

  @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)  //JPA로 데이터베이스로 저장할 때 Enum 값을 어떤 형태로 저장할지 결정
  @Column(nullable = false)     //기본적으로 int로 된 숫자가 저장됨, 숫자로하면 의미를 알 수 없어 문자열로 수정
  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 클래스 Role을 생성

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

  private final String key;
  private final String title;

  //스프링 시큐리티에서는 권한 코드에 항상 ROLE_ 가 앞에 있어야만 합니다.
}

 

마지막으로 User의 CRUD를 책임질 UserRepository 생성

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface User2Repository extends JpaRepository<User, Long> {

  //소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위함
  Optional<User> findByEmail(String email);
}

 

의존성 추가하기

//소셜로그인 등 클라이언트에서 필요한 의존성
compile('org.springframework.boot:spring-boot-starter-oauth2-client') 

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

@EnableWebSecurity

-Spring Security 설정들을 활성화 시켜줍니다.

 

csrf().disable().headers().frameOptions().disable()

-h2-console 화면을 사용하기 위해 해당 옵션들을 disable 합니다.

 

authorizeRequests

-URL별 권한 관리를 설정하는 옵션의 시작점입니다. 이게 선언되야만 antMatchers 옵션 사용가능

 

antMatchers

-권한 관리 대상을 지정, URL, HTTP 메소드별로 관리가 가능

-"/"등 지정된 URL은 permitAll()로 전체 열람 가능하게 했고, /api/v1** 주소들은 USER 권한이 있어야 가능

 

logout().logoutSuccessUrl("/")

-로그아웃 성공시 이동할 주소이며 이는 컨트롤러에서 만들어서 사용해야함

 

oauth2Login

-OAuth2 로그인 기능에 대한 여러 설정의 진입점이며 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록하는 곳이 userService() 입니다. 소셜 서비스들에서 사용자 정보를 가져온 상태에서 추가로 진행하고 싶은 기능을 명시합니다.

 

CustomOAuth2UserService 클래스 생성

@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 delegate = new DefaultOAuth2UserService();
    OAuth2User oAuth2User = delegate.loadUser(userRequest);

    //현재 로그인 진행중인 서비스를 구분하는 코드, 지금은 구글만 사용하는 불필요한 기능이지만 이후 네이버 로그인 연동시 필요
    String registrationId = userRequest.getClientRegistration().getRegistrationId() ;
    
    //OAuth2 로그인 진행 시 키가되는 필드값을 이야기 합니다. PK와 같은 의미, 구글만 기본 코드 sub를 지원
    String userNameAttributedName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName() ;
    
    //OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
    OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributedName, oAuth2User.getAttributes()) ;

    User user = saveOrUpdate(attributes) ;
    //SessionUser : 세션에 사용자 정보를 담기위한 Dto, User 클래스보다 좋음
    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) ;
  }
}

saveOrUpdate 메서드는 우리 테이블에 없는 정보면 insert을 하고 이미 있는 유저면 update을 한다. update하는 이유는 사용자의 정보가 바뀔 경우가 존재할까봐. (회원사진이나 이름을 바꿀 경우)

 

OAuthAttributes 클래스 생성

package com.bahngFamily.jihoon.springboot.config.auth.dto;

import com.bahngFamily.jihoon.springboot.domain.user.Role;
import com.bahngFamily.jihoon.springboot.domain.user.User;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;

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

  //of() : OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야만 합니다.
  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() ;
  }

  //toEntity() : User엔티티 생성, 엔티티를 생성하는 시점은 처음 가입때 입니다.
  //가입할 때 기본권한을 GUEST로 주기 위해 role 빌더값에는 Role.GUEST를 사용함
  //OAuthAttribute클래스 생성이 끝났으면 같은 패키지에 SessionUser 클래스를 생성
  public User toEntity() {
    return User.builder()
        .name(name)
        .email(email)
        .picture(picture)
        .role(Role.GUEST)
        .build();
  }
}

 

SessionUser 클래스 생성

package com.bahngFamily.jihoon.springboot.config.auth.dto;

import com.bahngFamily.jihoon.springboot.domain.user.User;
import java.io.Serializable;
import lombok.Getter;

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

 

로그인 테스트

index.mustache에 로그인 버튼과 로그인 성공 시 사용자 이름을 보여주는 코드를 작성합니다.

{{>layout/header}}
  <h1>스프링 부트로 시작하는 웹 서비스 Ver.2</h1>
  <div class = "col-md-12">
    <div class = "row">
      <div class = "col-md-6">
        <a href="/posts/save" role="button" class = "btn btn-primary">글 등록</a>
        {{#userName}}
          Logged in as: <span id="user">{{userName}}</span>
          <a href="/logout" class="btn btn-info active" role="button">Logout</a>
        {{/userName}}
        {{^userName}}
          <a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
          <a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
<!--          <a href="/sighup" class="btn btn-info" role="button">회원가입</a>-->
        {{/userName}}
      </div>
    </div>
    <br>
    <!-- 목록 출력 영역 -->
    <table class="table table-horizontal table-bordered">
      <thead class="thead-strong">
        <tr>
          <th>게시글 번호</th>
          <th>제목</th>
          <th>작성자</th>
          <th>최종수정일</th>
        </tr>
      </thead>
      <tbody id="tbody">
        {{#posts}}
          <tr>
            <td>{{id}}</td>
            <td><a href="/posts/update/{{id}}">{{title}}</a></td>
            <td>{{author}}</td>
            <td>{{modifiedDate}}</td>
          </tr>
        {{/posts}}
      </tbody>
    </table>
  </div>
{{>layout/footer}}

a href = "/logout"

-스프링 시큐리티에서 기본적으로 제공하는 로그아웃 URL입니다.

-즉, 개발자가 별도로 저 URL에 해당하는 컨트롤러를 만들 필요가 없습니다.

 

a href="/oauth2/authorization/google

-스프링 시큐리티에서 기본적으로 제공하는 로그인 URL입니다.

-로그아웃과 마찬가지로 따로 컨트롤러를 만들 필요가 없습니다.

 

Controller

@RequiredArgsConstructor
@Controller
public class IndexController {
  private final PostsService postsService;
  private final HttpSession httpSession;

  @GetMapping("/")
  public String index(Model model, @LoginUser SessionUser user){
    model.addAttribute("posts", postsService.findAllDesc());
	
    SessionUser user = (SessionUser) httpSession.getAttribute("user");
    
    if(user != null)
      model.addAttribute("userName", user.getName());

    return "index";
  }
}

 

스프링 시큐리티에서 간편 로그인과 관련하여 제공해주는게 아주 많기 때문에 설정만 잘하고 필요한 클래스만 상속 잘 받아서 오버라이딩만 하면 잘 진행된다. 이에 대해 자세히 알고 싶다면 아래의 사이트를 참고하면 좋을 것 같다.

 

www.baeldung.com/spring-security-5-oauth2-login

 

Spring Security 5 - OAuth2 Login | Baeldung

Learn how to authenticate users with Facebook, Google or other credentials using OAuth2 in Spring Security 5.

www.baeldung.com

 

 

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

 

 

 

 

 

 

 

반응형
댓글
반응형
최근에 달린 댓글
글 보관함
Total
Today
Yesterday
최근에 올라온 글
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31