토이 프로젝트/Spring&Java 갖고놀기

Spring Boot Web Chatting : 스프링 부트로 실시간 채팅 만들기 (5) OAuth2 를 활용한 소셜 로그인

TerianP 2022. 9. 21.
728x90

10.29 추가 : 일반(문자) 채팅만 구현하는 코드는 git 의 master 브렌치를 참고해주시기 바랍니다.  master-Webrtc-jpa 는 화상 채팅과 jpa 를 이용한 DB 연결을 포함하는 브렌치입니다.

 

소셜 로그인 후 실시간 채팅만들기

이제 저번 시간까지해서 실시간 채팅을 하기 위한 기본 요소들은 모두 만들었습니다. 이제는 조금 더 업그래이드 하기 위해서! 로그인 기능을 덧붙여볼까 합니다.

근데 언제나 하던 일반 로그인은 시시하고, 조금 색다르게 소셜 로그인 기능을 먼저 추가해볼까 합니다.

정확히는 소셜 로그인 시 아이디(닉네임)을 채팅명으로 만든 후 해당 닉네임으로 채팅을 할 수 있도록 기능을 추가하겠습니다.

단 이번에는 DB 에 회원 정보를 저장하지 않고 소셜 로그인을 하는 것 까지만! 하는 코드로  DB 저장까지는 추후 회원가입 코드를 짜고 같이 한꺼번에 하도록 하겠습니다

 

역시나 관련 코드는 git 에 업로드 해두었으니 자세한 보다 자세한 코드는 깃 참고 부탁드립니다.

https://github.com/SeJonJ

 

SeJonJ - Overview

https://www.notion.so/ca0c6295bc9644d4b5ad073ed6a75c6d - SeJonJ

github.com

 

또한 아래 내용은 스프링 시큐리티를 활용한 소셜 로그인에 대한 설명이기 때문에 소셜로그인에 사용되는 기본적인 클래스와 인터페이스들에 대해서 알아야합니다

아래에 간단히 정리해둔 글이 있으니 모르시는 분들은 미리 참고부탁드립니다!

2022.09.15 - [Java - Spring] - SpringSecurity 와 소셜 로그인 : OAuth2, 카카오 로그인, 네이버 로그인

 

SpringSecurity 와 소셜 로그인 : OAuth2, 카카오 로그인, 네이버 로그인

SpringSecurity OAuth2 와 소셜 로그인 - 학원에서 진행했던 파이널 프로젝트에서 내가 담당한 파트!! 이전부터 정리하려고 했으나 채팅하고 이것저것 한다고 미루다가 드디어 해보려고한다. - 이번 글

terianp.tistory.com

 

Gradle

- gradle 에 라이브러리 추가!!

// security
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
implementation 'org.springframework.boot:spring-boot-starter-security:2.7.3'

// spring oauth2
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:2.7.1'

 

application.properties - 매우 중요

- 아래 정보가 모두 정확하게!! 넣어주어야 한다. 아니면 미친듯한 에러에 시달릴 것이다

- 각종 api 키가 들어있기 때문에 해당 파일은 git 에 없습니다

# naver
### API id & secret ID
spring.security.oauth2.client.registration.naver.client-id=네이버 로그인 API 아이디
spring.security.oauth2.client.registration.naver.client-secret=네이버 로그인 API 시크릿키
## registration
### redirect uri => 네이버 로그인 API 정보가 있는 페이지의 redirect-uri 와 일치해야함 
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
### api 에서 가져오는 내용
spring.security.oauth2.client.registration.naver.scope=email, nickname
spring.security.oauth2.client.registration.naver.client-name=Naver

## provider ==> 로그인 정보 제공자
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response

# kakao
### restAPI key
spring.security.oauth2.client.registration.kakao.client-id=카카오 로그인 API id
## registeration
### api에서 가져오는 내용
spring.security.oauth2.client.registration.kakao.scope = profile_nickname, account_email
spring.security.oauth2.client.registration.kakao.client-name = Kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type = authorization_code
### 카카오 로그인 api 정보가 있는 페이지의 redirect uri 와 일치해야함
spring.security.oauth2.client.registration.kakao.redirect-uri = http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method = POST
## provider
spring.security.oauth2.client.provider.kakao.authorization-uri = https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri = https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri = https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute = id

 

SecurityConfig

- 소셜 로그인을 위한 가장 중요한 SecurityConfig 클래스 파일

- 소셜 로그인을 위해서는 스프링 시큐리티와  OAuth2 를 사용해야 하는데 이를 위한 설정을 하기위한 클래스 파일이다.

- 해당 파일에서는 스프링 시큐리티의 설정들 - 로그인 시 경로, 요청, 로그인 성공 시 경로 -와 함께 OAuth2 설정 - OAuth2 로그인 사용 여부, 소셜 로그인 url, 후처리를 진행할 클래스파일 - 등을 설정할 수 있다

package webChat.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import webChat.service.social.PrincipalOauth2UserService;

// springSecurity Config
@Configuration
@EnableWebSecurity
public class SecurityConfig  extends WebSecurityConfigurerAdapter {

    @Autowired
    private PrincipalOauth2UserService principalOauth2UserService;

    // Security 를 이용한 각종 권한 접근 경로 등 설정
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                    .authorizeHttpRequests()
                    // "/" 아래로 접근하는 모든 유저에 대해서 허용 => 즉 모든 경로에 대해서 허용
                    // 일단 임시로 모든 경로에 대해서 허용해둠
                    .antMatchers("/**").permitAll()
                .and()
                    // Security 의 기본 login 페이지가 아닌 커스텀 페이지를 사용하기 위한 설정
                    // 로그인 페이지 url
                    .formLogin().loginPage("/chatlogin").permitAll()
                    .loginProcessingUrl("/login") // 로그인 요청 url
                    .defaultSuccessUrl("/") // 로그인 완료 시 요청 url
                .and()
                    .logout().logoutUrl("/logout").permitAll() // 로그인 아웃 시 url
                    .logoutSuccessUrl("/") // 성공적으로 로그아웃 햇을 때 url
                .and()
                    .oauth2Login() // 소셜 로그인 사용 여부
                    .loginPage("/chatlogin") // 소셜 로그인 진행 시 사용할 url
                    .userInfoEndpoint()
                    // SNS 로그인이 완료된 뒤 후처리가 필요함. 엑세스토큰+사용자프로필 정보
                    .userService(principalOauth2UserService);
    }
}

 

ChatUser

- 로그인 시 사용되는 DTO

- 사실 채팅을 위해서 여러 정보가 필요하지 않고, 아주 일반적인 userName, email, provider 3가지만 포함한다. 

- 여기서 provider 은 소셜 로그인 시 '어디서' 정보를 제공하는 소셜 로그인인지 적어두기 위한 변수이다. 정확히는 네이버 로그인인지 카카오 로그인인지 구분하는 용도

- 추후 소셜 로그인과 함께 일반 회원가입을 통해 로그인 기능을 만들게 되면 password 변수가 추가되어야 할 것이다.

package webChat.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatUser {
    private String userName; // 소셜에서 제공받은 유저명 => 유저 닉네임
    private String email; // 소셜에서 제공받은 이메일
    private String provider; // 소셜 제공자 -> ex) 네이버, 카카오 ----
}

 

PrincipalDetails

- 스프링 시큐리티로 로그인 시 스프링 시큐리티의 세션안에는 시큐리티에서 검증한 Authentication 객체를 갖는다. 이때 Authentication 안에는 UserDetails 를 구현한 구현 클래스가 오게 된다.

- 소셜 로그인 역시 일반 로그인과 마찬가지로 로그인 정보가 Authentication 객체 안에 담기게 되는데, 이때 UserDetails 를 구현한 구현 클래스가 아닌 OAuth2User 를 구현한 구현 클래스가 온다는 차이점이 있다.

- 나는 앞으로 일반 로그인과 소셜 로그인 모두 구현할 생각이기 때문에 PrincipalDetails 이라는 클래스가 UserDetails 와 OAuth2User 모두를 구현한 구현 클래스가 되도록 만들었다. 사실 소셜로그인만 먼저 구현하려고 한다면 굳이 UserDetails 은 상속받을 필요가 없고 OAuth2User 만 사용해도 충분하다

package webChat.service.social;

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 webChat.dto.ChatUser;

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

// SpringSecurity 를 이용한 로그인에서 사용되는
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    // ChatUserDTO
    private ChatUser user;

    // 소셜 로그인 유저의 정보 확인을 위한 attributes
    private Map<String, Object> attributes;

    // 소셜 유저 타입 정보 -> 네이버, 카카오, 일반 등
    private String provider;

    // 일반 유저
    public PrincipalDetails(ChatUser user, String provider) {
        this.user = user;
        this.provider = provider;
    }

    // OAuth2User 유저 -> 소셜 로그인 유저
    public PrincipalDetails(ChatUser user, Map<String, Object> attributes, String provider){
        this.user = user;
        this.attributes = attributes;
        this.provider = provider;
    }

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

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

    // 해당 유저의 권한을 return
    // 원래는 회원 가입 시 유저의 권한을 설정해두고 해당 유저의 권한을 return 해야하나
    // 현재는 DB 를 사용해서 회원가입을 하는게 아니라 소셜 로그인을 하는 것! 이 목적이기 때문에
    // 모든 유저의 권한은 "user" 로 return 한다.
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection< GrantedAuthority> role = new ArrayList<>();

        role.add(new GrantedAuthority(){

            @Override
            public String getAuthority() {
                return "user";
            }
        });

        return role;
    }
    
    @Override
    public String getPassword() {
        return "nopwd";
    }

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

 

PrincipalOauth2UserService - 아주아주 중요함

- 제일 중요하고 제일 복잡한...클래스ㅠ.ㅠ

- 가장 제일 중요한 소셜 로그인 후 세션에 로그인 정보를 담는 기능을 담당하는 클래스

-  대략 아래와 같은 순서로 로그인 기능이 동작한다.

  • 네이버 로그인 버튼 클릭 -> 네이버 로그인창 -> 로그인 완료 -> code 리턴(OAuth-client 라이브러리) -> AccessToken 요청 -> userRequest 정보 -> loadUser 함수 호출 -> 네이버 회원 프로필 받아오기
  • 여기서 중요한 것이 소셜 로그인을 통해서 받아오는 프로필 정보는 소셜 로그인 제공자 Proivder 마다 다르다! 즉 네이버 따로, 카카오 따로, 구글 따로 페북 따로인 것이다. 때문에 2개 이상의 소셜로그인을 지원한다면 프로필을 받아온 후 이를 어떤 제공자인지에 따라서 정보를 나누어 저장하는 과정이 필요하다.
package webChat.service.social;

import lombok.extern.slf4j.Slf4j;
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;
import webChat.dto.ChatUser;

// OAuth2 로그인 시 -> 즉 소셜 로그인 시 DefaultOAuth2UserService 아래의 loadUser 메서드가 실행됨
// 즉 OAuth2 로그인 후 후처리 - 회원 가입, 회원 정보에 따른 등록 등 - 관현 클래스와 메서드에 해당함
@Service
@Slf4j
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // 네이버 로그인 버튼 클릭 -> 네이버 로그인창 -> 로그인 완료 -> code 리턴(OAuth-client 라이브러리) -> AccessToken 요청
        // userRequest 정보 -> loadUser 함수 호출 -> 네이버 회원 프로필 받아오기

        // 매개변수로 넘어온 userRequest 를 loaduser 를 실행해서 유저 정보가 담긴 oauth2user 로 변환
        OAuth2User oauth2user = super.loadUser(userRequest);

        log.info("clientRegistration 정보 [{}] ", userRequest.getClientRegistration());
        log.info("accessToken 정보 [{}] ",userRequest.getAccessToken().getTokenValue());

        return oAuth2UserLogin(userRequest, oauth2user);
    }

    private OAuth2User oAuth2UserLogin(OAuth2UserRequest userRequest, OAuth2User oAuth2User){
        // Attribute 를 파싱해서 공통 객체로 묶기!! => 소셜 로그인 마다 다른 정보가 들어옴으로 쉽게 관리하기 위해서
        SocialLogin login = null;

        // provider 정보 확인 => 어떤 SNS 로 로그인했는지 확인
        String provider = userRequest.getClientRegistration().getRegistrationId();
        
        
        if ("kakao".equals(provider)) {
            // 카카오 로그인인 경우 KaKaoLogin 클래스에 소셜 로그인 정보가 담긴
            // oAuth2User.getAttributes() 를 보내주고 정보를 담는다
            login = new KaKaoLogin(oAuth2User.getAttributes());
        } else if ("naver".equals(provider)) {
            // 네이버 로그인인 경우 NaverLogin 클래스를 소셜 로그인 정보가 담긴
            // oAuth2User.getAttributes() 를 보내주고 정보를 담는다
            login = new NaverLogin(oAuth2User.getAttributes());
        }

        // ChatUser 에 소셜 로그인 후 받아서 나눠진 정보를 담는다
        ChatUser user = ChatUser.builder()
                .userName(login.getNickName())
                .email(login.getEmail())
                .provider(login.getProvider())
                .build();

        // 다시 한번!! 왜 return new PrincipalDetails 가 가능한가?
        // PrincipalDetails 는 OAuth2User 인터페이스를 를 구현한 구현 클래스이기 때문!!
        // 또한 oAuth2User 를 동시에! 상속받았기 때문에 sns 유저에 역시 같이 들고 다닐 수 있다!!
        // 이 정보들은 SecuritySession 의 Authentication 안에 담김
        return new PrincipalDetails(user, oAuth2User.getAttributes(), "user");
    }
}

 

SocialLogin - 소셜 로그인 인터페이스

- 어떤 소셜 로그인인지에 맞춰서 회원 정보를 파싱해야한다.

- 그러나 결국 파싱해서 가져와야할 정보를 일정하기 때문에 이런 일정하게 가져오는 부분들을 미리 메서드로 만들어서 해당 정보들만 가져올 수 있도록 한다.

package webChat.service.social;

public interface SocialLogin {
    String getProvider(); // 소셜 로그인 제공자 정보
    String getEmail(); // 소셜 로그인 이메일 정보
    String getNickName(); // 소셜 로그인 닉네임 정보
}

 

KaKaoLogin

- 카카오 소셜 로그인 시 회원 정보를 파싱하기 위한 클래스

package webChat.service.social;

import java.util.HashMap;
import java.util.Map;

public class KaKaoLogin implements SocialLogin{

    private Map<String, Object> kakaoAttributes;

    // 넘어오는 정보 를 kakaoAttributes 변수에 저장
    public KaKaoLogin(Map<String, Object> kakaoAttributes) {
        this.kakaoAttributes = kakaoAttributes;
    }

    // 제공자 가져오기
    @Override
    public String getProvider() {
        // TODO Auto-generated method stub

        return "kakao";
    }
    
    // 이메일 파싱해서 가져오기
    @Override
    public String getEmail() {
        // TODO Auto-generated method stub
        HashMap<String, Object> account = (HashMap<String, Object>) kakaoAttributes.get("kakao_account");

        String email = (String) account.get("email");
//    System.out.println("memail : "+memail);

        return email;
    }

    // 닉네임 파싱해서 가져오기
    @Override
    public String getNickName() {
        // TODO Auto-generated method stub
        HashMap<String, Object> properties = (HashMap<String, Object>) kakaoAttributes.get("properties");
        String nickName = (String) properties.get("nickname");

        return nickName;
    }
}

 

NaverLogin

- 네이버 소셜 로그인 시 정보를 파싱하기 위한 클래스

package webChat.service.social;

import java.util.Map;

public class NaverLogin implements SocialLogin{
    private Map<String, Object> naverAttributes;


    public NaverLogin(Map<String, Object> naverAttributes) {

        this.naverAttributes = naverAttributes;
    }

    @Override
    public String getProvider() {
        // TODO Auto-generated method stub

        return "naver";
    }

    @Override
    public String getEmail() {
        // TODO Auto-generated method stub
        Map<String, Object> map = (Map<String, Object>) naverAttributes.get("response");

        return (String) map.get("email");
    }

    @Override
    public String getNickName() {
        // TODO Auto-generated method stub
        Map<String, Object> map = (Map<String, Object>) naverAttributes.get("response");
        return (String) map.get("nickname");
    }
}

 

ChatRoomController

- ChatRoomController 도 살짝 바꿔줘야 한다. 특히 "/" 로 들어올 때 로그인 되어 있는 사용자의 경우 -> 세션이 있는 경우 -> PrincipalDetails 가 null 이 아닌 경우 model 에 해당 정보를 담아서 roomlist 로 보내준다.

- 이후 view 에서는 소셜 로그인 사용자의 닉네임을 보여줄 수 있게 한다.

    // 스프링 시큐리티의 로그인 유저 정보는 Security 세션의 PrincipalDetails 안에 담긴다
    // 정확히는 PrincipalDetails 안에 ChatUser 객체가 담기고, 이것을 가져오면 된다.
    @GetMapping("/")
    public String goChatRoom(Model model, @AuthenticationPrincipal PrincipalDetails principalDetails){

        model.addAttribute("list", chatRepository.findAllRoom());

        // principalDetails 가 null 이 아니라면 로그인 된 상태!!
        if (principalDetails != null) {
            // 세션에서 로그인 유저 정보를 가져옴
            model.addAttribute("user", principalDetails.getUser());
            log.info("user [{}] ",principalDetails);
        }

//        model.addAttribute("user", "hey");
        log.info("SHOW ALL ChatList {}", chatRepository.findAllRoom());
        return "roomlist";
    }

 

html && JS

- 간단하게 짚고 가겠습니다! 자세한 것은 주석 참고!!

- 로그인 페이지 : 소셜 로그인 시 링크는 아래와 같다.

<div>
   <span class="d-block text-left my-4 text-muted"></span>
   <a href="http://localhost:8080/oauth2/authorization/kakao">
      <img src="/images/login/kakaotalk.png" width="200" height="auto" alt="" />
   </a>
   <a href="http://localhost:8080/oauth2/authorization/naver">
      <img src="/images/login/naver.png" width="200" height="auto" alt="" />
   </a>
</div>

 

- roomlist : 로그인 전에는 로그인 버튼을, 로그인 이후에는 유저 닉네임을 표시한다

<div th:if="${user == null}" class="row">
    <div class="col">
        <a href="/chatlogin"><button type="button" class="btn btn-primary">로그인하기</button></a>
    </div>
</div>
<h5 th:if="${user != null}">
    [[${user.userName}]]
</h5>

 

- ChatRoom : 로그인 시에는 유저명 작성칸을 로그인한 유저의 닉네임을 미리 보여줄 수 있도록 한다.

<form id="usernameForm" name="usernameForm">
    <div  th:if="${user == null}" class="form-group">
        <input type="text" id="name" placeholder="Username" autocomplete="off" class="form-control"/>
    </div>
    <div  th:if="${user != null}" class="form-group">
        <input type="text" id="name" placeholder="Username" autocomplete="off" class="form-control" th:value="${user.userName}"/>
    </div>
    <div class="form-group">
        <button type="submit" class="accent username-submit">Start Chatting</button>
    </div>
</form>

- 구현 확인

이쁜 로그인 화면!
카카오 로그인!!
2단계 인증을 마치면

- 아마 카카오는 nickName 이 이름으로 되어있는게 아닌가 싶다

내가 나온다?! -> 카카오
네이버의 경우에는
닉네임이 나온다!
채팅도 잘 된다!


- Reference

https://programmer93.tistory.com/68

 

Spring Security UserDetails, UserDetailsService 란? - 삽질중인 개발자

Spring Security - UserDetails , UserDetailsService UserDetails 란? Spring Security에서 사용자의 정보를 담는 인터페이스이다. Spring Security에서 사용자의 정보를 불러오기 위해서 구현해야 하는 인터페이..

programmer93.tistory.com

 

https://sas-study.tistory.com/410

 

[Spring Security] @AuthenticationPrincipal 어노테이션은 어떻게 동작할까??

안녕하세요. 오늘은 스프링 시큐리티를 활용하면서 궁금했던 부분을 공부해보았습니다. 스프링 시큐리티는 SecurityContext에 인증된 Authentication 객체를 넣어두고 현재 스레드 내에서 공유

sas-study.tistory.com

https://lotuus.tistory.com/104

 

[Spring Security] OAuth 카카오 로그인하기

목차 이전글 https://lotuus.tistory.com/80 [Spring Security] OAuth 네이버 로그인하기 목차 이전글 https://lotuus.tistory.com/79 [Spring Security] OAuth 구글 로그인하기 목차 [이전 게시글] 꼭! 봐주세여..

lotuus.tistory.com

 

댓글