BE/Spring

[Spring Security] oAuth2.0 - 구글로 로그인하기

멍목 2022. 6. 2. 23:02
반응형

이 포스팅은 아래의 강의를 참고하였으니 여기에서 공부하시는 것을 추천드립니다.

https://inf.run/tcLk

 

[무료] 스프링부트 시큐리티 & JWT 강의 - 인프런 | 강의

스프링부트 시큐리티에 대한 개념이 잡힙니다., - 강의 소개 | 인프런...

www.inflearn.com

 


 

1. 구글 로그인 준비

더보기

1) Google에서 '구글 api 콘솔' 검색 후 APIs Console... 클릭

 

2) 프로젝트 선택 - 새 프로젝트 클릭 후 이름 아무렇게나 짓고 만들기

 

3) 방금 생성한 프로젝트로 선택

 

4) OAuth 동의 화면 - 외부 - 만들기 에서 앱 이름과 사용자 지정 이메일, 개발자 연락처 정보

를 기입 후 동의 후 저장 계속 눌러서 완료한다.

 

5) 사용자 인증 정보 - OAuth 클라이언트 ID 만들기

 

6) 아래 캡쳐처럼 애플리케이션 유형, 이름, URI를 기재한다. (필자는 공부목적으로 아래의 주소처럼 입력함)

 

 

 

2. Spring Boot 설정

1) pom.xml에 아래의 의존성(OAuth2 Client) 추가 및 application.yml에 아래처럼 설정

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

 

- application.yml

spring:  
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ID입력
            client-secret: 시크릿코드입력
            scope:
            - email
            - profile

 

2) SecurityConfig 설정 클래스 파일의 configure 메소드를 아래 표시된 소스 추가

@Override
protected void configure(HttpSecurity http) throws Exception{       // Spring Security 설정
    http.csrf().disable();      // csrf 비활성화

    // URL에 따른 접근 제한 처리
    http.authorizeRequests()
            .antMatchers("/user/**").authenticated()        // URL user : 인증이 되어야 함
            .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")      // URL manager : 권한 'ROLE_ADMIN', 'ROLE_MANAGER'가 있어야함
            .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")         // URL admin : 권한 'ROLE_ADMIN'이 있어야함
            .anyRequest().permitAll()      // 위에 명시되지 않은 URL 은 로그인 및 권한 검사 X
            .and()
            .formLogin()
            .loginPage("/loginForm")       // formLogin이 필요한 경우, /loginForm 으로 보낸다.
            .loginProcessingUrl("/login")   // login 주소가 호출이 되면 시큐리티가 낚이채서 대신 로그인을 진행
            .defaultSuccessUrl("/")        // login 성공 시, 보내줄 기본 url
            // OAuth2 로그인 설정(시작)
            .and()
            .oauth2Login()
            .loginPage("/loginForm")    // OAuth2의 로그인 페이지 URL.
            .userInfoEndpoint()
            .userService(principalOauth2UserSerivce)// 구글 로그인이 완료된 이후에 후 처리가 필요. (코드가 아닌 액세스토큰+사용자프로필의정보)를 가져옴
            // OAuth2 로그인 설정(끝)
    ;
}

 

3) PrincipalDetails 클래스를 OAuth2User를 상속받는다.

* 상속받는 이유? 

1. Spring Security를 이용한 일반 로그인 시, 유저 정보가 UserDetail 클래스에 담기며,

2. OAuth2 로그인 시, 유저 정보가 OAuth2User 클래스에 담겨온다.

3. 그리하여 하나의 클래스가 이 두 가지를 모두 상속받아서 혼용하여 사용할 수 있도록 하기 위함이다.

 

package com.cos.security1.config.auth;

// 시큐리티가 /login url을 낚아채서 로그인을 진행시킴.
// 로그인을 하면 security 전용 session을 만들어준다. (Security ContextHolder)
// 오브젝트 타입 => Authentication 타입 객체
// Authentication 안에 User 정보가 있어야 한다.
// User 오브젝트 타입 => UserDetails 타입 객체

import com.cos.security1.model.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

// Security Session 안에 들어갈 수 있는 객체: Authentication
// Authentication 안에 들어갈 수 있는 객체: UserDetails, OAuth2User
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private User user;
    private Map<String, Object> attibutes;

    // 일반 로그인
    public PrincipalDetails(User user){
        this.user = user;
    }

    // OAuth2 로그인
    public PrincipalDetails(User user, Map<String, Object> attibutes){
        this.user = user;
        this.attibutes = attibutes;
    }
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // 해당 유저의 권한을 리턴하는 method
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        String role = user.getRole();

        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return role;
            }
        });

        return collect;
    }

    @Override
    // 만료 여부
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    // 잠금 여부
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    // 만료 여부
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    // 회원 사용 여부
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return attibutes;
    }

    @Override
    public <A> A getAttribute(String name) {
        return OAuth2User.super.getAttribute(name);
    }

    @Override
    public String getName() {
        return null;
    }
}

 

 

4) oauth2라는 패키지 명을 생성 후, PrincipalOauth2UserService 클래스 파일 생성

OAuth2를 통해 로그인을 하면, 해당 정보를 이용해서 아이디가 있는 지 체크 후 아이디가 없으면 자동가입을 진행한다.

package com.cos.security1.config.oauth2;

import com.cos.security1.Repository.UserRepository;
import com.cos.security1.config.auth.PrincipalDetails;
import com.cos.security1.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
public class PrincipalOauth2UserSerivce extends DefaultOAuth2UserService {

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private UserRepository userRepository;

    // 구글 로그인 작동 방식
    // 구글로그인버튼 클릭 > 구글로그인 > code 리턴(OAuth-Client Library) > AccessToken 요청
    // userRequest 정보 > loadUser함수 호출 > 구글로부터 회원 프로필을 받아줌

    // 구글로부터 받은 userRequest 데이터에 대한 후처리 함수
    // 함수 종료 시, @AuthenticationPrincipal 어노테이션이 만들어진다.
    // OAuth2 로그인 시 사용
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
        System.out.println("getClientRegistration: "+userRequest.getClientRegistration());  // registration: 어떤 매체에서 로그인했는지 확인 가능
        System.out.println("getAccessToken: "+userRequest.getAccessToken().getTokenValue());    // access token
        System.out.println("getAttributes: "+super.loadUser(userRequest).getAttributes());      // 프로필 정보

        OAuth2User oAuth2User = super.loadUser(userRequest);
        System.out.println("getAttributes: "+oAuth2User.getAttributes());

        String provider = userRequest.getClientRegistration().getRegistrationId();        // google, facebook, etc
        String providerId = oAuth2User.getAttribute("sub");

        // OAuth2 로그인 시, username과 password는 필요없지만 형식상 넣어줌
        String username = provider + "_" + providerId;
        System.out.println("username: "+username);
        String password = bCryptPasswordEncoder.encode("provider");
        String role = "ROLE_USER";
        String email = oAuth2User.getAttribute("email");

        User findUser = userRepository.findByUsername(username);

        if(findUser == null){
            findUser = User.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();

            userRepository.save(findUser);
        }

        return new PrincipalDetails(findUser, oAuth2User.getAttributes());
    }
}

 

 

5) IndexController 클래스에서 아래와 같이 PrincipalDetails를 이용하여 유저 정보를 받을 수 있다.

일반 로그인, OAuth2 로그인 상관없이 혼용 가능

 

package com.cos.security1.controller;

import com.cos.security1.Repository.UserRepository;
import com.cos.security1.config.auth.PrincipalDetails;
import com.cos.security1.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {

    @Autowired
    UserRepository userRepository;

    @Autowired
    BCryptPasswordEncoder encoder;

    @GetMapping({"", "/"})
    public String index(){
        // mustache default folder : src/main/resources/
        // view resolver setting : templates (prefix), .mustache (suffix)
        return "index"; // src/main/resources/templates/index.mustache
    }

    // 일반로그인을 하던, OAuth2로 로그인을 하던 PrincipalDetails로 받기 때문에 혼용 가능
    @GetMapping({"/user"})
    @ResponseBody
    public String user(@AuthenticationPrincipal PrincipalDetails principalDetails)
    {
        System.out.println("principalDetails: "+principalDetails.getUser());
        return "user";
    }

    @GetMapping({"/admin"})
    @ResponseBody
    public String admin(){
        return "admin";
    }

    @GetMapping({"/manager"})
    @ResponseBody
    public String manager(){
        return "manager";
    }

    // /login SpringSecurity 에서 사용하지만, SecurityConfig 에서 설정 가능
    // 로그인 폼으로 이동
    @GetMapping({"/loginForm"})
    public String loginForm(){
        return "loginForm";
    }

    // 회원가입 폼으로 이동
    @GetMapping({"/joinForm"})
    public String joinForm(){
        return "joinForm";
    }

    // 회원가입 로직
    @PostMapping({"/join"})
    public String join(User user){
        user.setRole("ROLE_ADMIN");

        // 패스워드 암호화를 진행하지 않으면 시큐리티 로그인이 불가능
        String encPw = encoder.encode(user.getPassword());
        user.setPassword(encPw);

        userRepository.save(user);

        return "redirect:/loginForm";
    }

    @GetMapping({"/joinProc"})
    @ResponseBody
    public String joinProc(){
        return "회원가입 완료";
    }

    @Secured("ROLE_ADMIN")          // 이 메소드에 대해서만 특정 권한이 필요할 때 사용 가능
    @GetMapping("/info")
    @ResponseBody
    public String info(){
        return "개인정보";
    }

    @PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")       // 메소드 접근 이전에 확인하는 로직
    //@PostAuthorize()                                                      // 메소드 접근 이후에 확인하는 로직
    @GetMapping("/data")
    @ResponseBody
    public String data(){
        return "데이터정보";
    }

    // Authentication 사용방법
    // 일반 로그인의 경우: PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal(); 쓰면 되지만
    // OAuth2의 경우: OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();을 써야함.

    // @AuthenticationPrincipal 사용방법
    // 일반 로그인의 경우: @AuthenticationPrincipal UserDetails userDetails
    // OAuth2의 경우: @AuthenticationPrincipal OAuth2User oAuth2

    // 즉, Authentication 에 UserDetails or OAuth2User가 있음.
    @GetMapping("/test/login")
    @ResponseBody
    public String testLogin(Authentication authentication, @AuthenticationPrincipal UserDetails userDetails){
        System.out.println("/test/login ================");
        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        System.out.println("authentication: "+principalDetails);
        System.out.println("userDetails: "+userDetails.getUsername());
        return "세션정보확인";
    }

    @GetMapping("/test/oauthlogin")
    @ResponseBody
    public String oauthlogin(Authentication authentication, @AuthenticationPrincipal OAuth2User oAuth2){
        System.out.println("/test/oauthlogin ================");
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();

        System.out.println("authenticaiton: "+oAuth2User.getAttributes());
        System.out.println("oauth2User: "+oAuth2.getAttributes());
        return "oAuth2 세션정보확인";
    }
}
반응형