Spring Security

Spring Security - OAuth2 인증 - 환경 설정

상상날개 2022. 8. 1. 17:03

1. 기본 환경 세팅

https://coding-mid-life.tistory.com/71 을 기본으로 시작한다

 

 2. OAuth2에 필요한 추가 코드

  1) build.gradle 파일에 코드를 추가한다

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // 추가

 2) application.yml 파일을 변경한다

  • clientID와 clientSecret은 구글 API console에서 발급받은 API Key를 입력한다
spring:
	security:
    oauth2:
      client:
        registration:
          google:
            clientId:
            clientSecret:
            scope:
              - email
              - profile

 3) loginForm.html 파일에 코드를 추가한다

</form>
<a href="/oauth2/authorization/google">구글 로그인</a> <!-- 추가 -->
<a href="join">회원가입</a>

 4) SecurityConfig 클래스에 코드를 추가한다

                .defaultSuccessUrl("/") //추가
                .and() // 추가
                .oauth2Login() // 추가
                .loginPage("/login"); // 추가
        return http.build();

 

 

3. 애플리케이션 실행

 1) 코드를 모두 수정한 후 애플리케이션을 실행한다

  • localhost:8080/login 으로 접속하여 '구글로그인'을 선택한다

계정을 클릭하여 로그인을 진행하면 

 

 

4. 구글 회원 프로필 정보 받기

 1) 로그인 후 필요한 후처리 작업을 해 준다

  • 코드 받기(인증)
  • 액세스 토큰(권한)
  • 사용자 프로필 정보를 가져온다
  • 3번에서 가져온 정보를 토대로 자동으로 회원가입 할 수 있다
  • 위 정보만으로 부족할 경우 추가적인 구성 정보를 기입할 수 있다

 2) config 패키지에 oauth 패키지를 만들고 PrincipalOauth2UserService 클래스를 생성한다

  • 구글 로그인 버튼 클릭 → 구글 로그인 창 → 로그인 완료 → code 리턴(OAuth-Client 라이브러리) → AccessToken 요청 userRequest 정보 → loadUser 함수 호출 → 구글로부터 회원프로필 정보를 받아 온다
  • 로그인 후에 필요한 후처리 작업을 담당한다
  • loadUser 메서드를 Override하는데 이 메서드는 구글로부터 받은 userRequest 데이터에 대한 후처리 함수이다
    - userRequest에 담긴 정보를 확인할 수 있는 메서드
    - userRequest.getClientRegistration()
    - userRequest.getAccessToken().getTokenValue()
    - super.loadUser(userRequest).getAttributest()
package com.memberlogin.loginjoin.config.oauth;

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 PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        System.out.println("userRequest : " + userRequest);
        return super.loadUser(userRequest);
    }
}

 

 3) SecurityConfig에 위 코드들을 적용해 주는 코드를 추가한다

  @Autowired
    private PrincipalOauth2UserService principalOauth2UserService; // 추가
......
    	.userInfoEndpoint() // 추가
        .userService(principalOauth2UserService); // 추가

 

 4) Member 클래스에 코드를 추가한다

	private String provider;
    private String providerId;

 

 

5. authentication 정보 확인

 1) PrincipalDetails 클래스에 @Data 애너테이션을 추가한다

@Data // 추가

 

 2) IndexController 클래스에 코드를 추가한다

  • Authentication 객체를 의존성 주입을 통해 다운 캐스팅하여 정보를 가져올 수 있다.
  • 회원가입 → 로그인 후 /loginTest url로 접속해서 로그에 Authentication 정보를 확인합니다.
// 추가
		@GetMapping("/loginTest")
    public @ResponseBody String loginTest(Authentication authentication) {
        System.out.println("============/loginTest===========");
        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        System.out.println("authentication : " + principalDetails.getMember());
        return "세션 정보 확인";
    }

  • 위와 같은 동작을 애노테이션으로 할 수 있도록 코드를 추가한다
    - 애너테이션을 통해 회원정보를 가져올 수 있다.
// 추가
		@GetMapping("/loginTest2")
    public @ResponseBody String loginTest2(@AuthenticationPrincipal PrincipalDetails principalDetails) {
        System.out.println("============/loginTest2===========");
        System.out.println("userDetails : " + principalDetails.getMember());
        return "세션 정보 확인2";
    }

  • OAuth2 로그인 정보를 가져오기 위한 코드를 추가한다
// 추가
		@GetMapping("/loginTest3")
    public @ResponseBody String loginOAuthTest(
					Authentication authentication,
					@AuthenticationPrincipal OAuth2User oauth) {
        System.out.println("============/loginOAuthTest===========");
				OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
        System.out.println("authenticaion : " + oauth2User.getAttributes());
				System.out.println("oauth2User : " + oauth.getAttributes());
        return "세션 정보 확인3";
    }

 

 

6. 스프링 시큐리티 (세션)

 1) Authentication 객체만 가질 수 있다

  • 일반 로그인 → PrincipalDetails( implements UserDeatils)
  • OAuth 로그인 → OAuth2User
  • 위와 같이 진행될 시 로그인 User 처리가 불편하게 된다.
    - OAuth 유저도 PrincipalDetails로 묶으면 된다.

 

 2) PrincipalDetails 클래스에 코드를 추가한다

  •  PrincipalDetails 클래스에 OAuth2User를 추가로 implements 해 준다
  • getAttributes() 와 getName() 메서드를 오버라이드 해 준다
 @Data
    public class PrincipalDetails implements UserDetails, OAuth2User {	
    
    		...
    		@Override
    	  public Map<String, Object> getAttributes() {
    	      return null;
    	  }
    	
    	  @Override
    	  public String getName() {
    	      return null;
    	  }
    }

 3) 애플리케이션 재실행 후 localhost:8080/login 접속 후 구글 로그인 출력화면이다

 

 

7. PrincipalDetails

 1) PrincipalDetails

  • UserDetails를 구현하는 객체로 사용되고 있다

 2) 스프링 시큐리티 세션

  • 시프링 시큐리티 세션 정보는 단 1가지 타입인 Authentication 객체만 가지고 있을 수 있다

 3) Authentication 객체를 담는 2가지 필드

  • OAuth2User 와 UserDetails가 있다
    - 일반적으로 회원가입을 하면 UserDetails를 통해 처리를 하게 된다
    - OAuth2로 회원가입 및 로그인을 하면 OAuth2User를 통해 처리를 하게 된다
  • 여기서 문제가 발생한다
    - OAuth2User와 UserDetails에는 Member 객체를 포함하고 있지 않다

  4) 해결 방법

  • UserDetails를 PrincipalDetails에 implements 한 후에 Member 객체를 담게 한다
    - Authentication 객체를 갖는 PrincipalDetails가 만들어진다
    - PrincipalDetails 객체에는 Member가 포함되어 있다
  • OAuth2User 또한 같은 문제점을 가지고 있고 일반 회원가입 & 로그인 처리와 OAuth2 처리가 별도로 되어 문제가 발생한다
    - PrincipalDetails implements UserDetails, OAuth2User를 통해 문제를 해결한다

 

8. 회원가입

 1) PrincipalDetails 클래스에 코드를 추가 및 수정한다

@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private Member member;
    private Map<String, Object> attributes; // 추가

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

		// 추가 & OAuth 로그인
    public PrincipalDetails(Member member, Map<String, Object> attributes) {
        this.member = member;
        this.attributes = attributes;
    }
		...

		@Override
    public Map<String, Object> getAttributes() {
        return attributes; // 수정
    }
}

  • Member 클래스에 코드를 추가한다
@Entity
@Data
@NoArgsConstructor // 추가
public class Member {
	
		@Builder // 추가
    public Member(String username, String email, String role, String provider, String providerId) {
        this.username = username;
        this.email = email;
        this.role = role;
        this.provider = provider;
        this.providerId = providerId;
    }
		
		...
}

  • 후처리 작업이 있는 PrincipalOauth2UserService 클래스의 코드를 수정한다
    - memberEntity
      : null 값일 때는 oauth로 처음 로그인 한 것이므로 회원가입 처리를 해 준다
      : null이 아닌 경우에는 기존에 1번이라도 로그인 한 이력이 있기 때문에 별도 처리를 하지 않는다
package com.memberlogin.loginjoin.config.oauth;

import com.memberlogin.loginjoin.model.Member;
import com.memberlogin.loginjoin.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
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 PrincipalOauth2UserService extends DefaultOAuth2UserService {

    //코드 추가
    @Autowired
    private MemberRepository memberRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        //코드 추가
        OAuth2User oauth2User = super.loadUser(userRequest);

        String provider = userRequest.getClientRegistration().getClientId();
        String providerId = oauth2User.getAttribute("sub");
        String username = oauth2User.getAttribute("name");
        String email = oauth2User.getAttribute("email");
        String role = "ROSE_USER";

        Member memberEntity = memberRepository.findByUsername(username);

        if(memberEntity == null) {
            // OAuth로 처음 로그인한 유저 - 회원가입 처리
            memberEntity = Member.builder()
                    .username(username)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            memberRepository.save(memberEntity);
        }

        return new PrincipalDetails(memberEntity, oauth2User.getAttributes());
    }
//        System.out.println("userRequest : " + userRequest);
//        return super.loadUser(userRequest);
//    }
}

 

 2) 로그인하여 Member 객체 정보 확인

  • IndexController 클래스에서 /user url을 수정한다
    - 일반 회원가입 & 로그인 → /user url 접속
    - 로그아웃 후 구글 로그인 → /user url 접속
public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
        System.out.println(principalDetails.getMember());
        return "user";
    }

  • 일반 회원가입 & 로그인 → /user url 접속

  • 로그아웃 후 구글 로그인 → /user url 접속

  • 일반 회원가입과 OAuth2 로그인으로 회원가입 처리가 정상적으로 되는 것을 확인할 수 있다

 

9. Index 페이지 통한 로그인 정보 확인

  • 지금까지는 로그인이 정상적으로 되었는지 확인하려면 /user (권한이 있는 경우에만 접근 가능)로 접속해서 확인했다
  • 지금부터는 Index 페이지(/)에서 로그인 되었는지 확인할 수 있도록 코드를 수정, 추가해 본다

 1) IndexController 클래스의 코드를 수정 한다

@GetMapping("/")
    public String index(@AuthenticationPrincipal PrincipalDetails principalDetails, Model model) {

        try {
            if(principalDetails.getUsername() != null) {
                model.addAttribute("username", principalDetails.getUsername());
            }
        } catch (NullPointerException e) {}
        return "index";
    }

 2) src > main > resources > templates 패키지에 index.html 파일을 생성한다

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Index Page 입니다.</title>
</head>
<body>
<h1>Index 페이지 입니다.</h1>
{{#username}}
    <h1>{{username}} 사용자입니다.</h1>
    <a href="/user">유저</a>
    <a href="/logout">로그아웃</a>
{{/username}}
{{^username}}
    <h3>로그인되지 않았습니다.</h3>
    <a href="/login">로그인 페이지로 이동</a>
    <a href="/join">회원가입 페이지로 이동</a>
{{/username}}
</body>
</html>

 3) 프로젝트를 재실행 후 애플리케이션에 접속하면 화면 출력이 변경되었음을 확인할 수 있다

로그인하지 않았을 때 로그인 했을 때