본문 바로가기

Server Programming/Spring Boot Backend Programming

8장 -5. 소셜 로그인 처리 (+ OAuth2, 토큰, REST API 키, 인가코드)

반응형

요구사항

  • OAuth2를 이용한 소셜 로그인 처리

 

OAuth2 : 외부 서비스로 소셜 로그인을 이용해 사용자 연동 처리를 하는 프로토콜


프로젝트를 위한 설정

  1. 카카오 로그인을 위한 설정
  2. OAuth2가 카카오 소셜 로그인을 처리하는 과정
  3. 소셜 로그인 후처리

 

1. 카카오 로그인을 위한 설정

  1. 애플리케이션 등록 : https://developers.kakao.com/
  2. 등록한 애플리케이션에 생성된 REST API 키 복사
  3. 플랫폼 : Web, 사이트 도메인 : http://localhost:8080 설정
  4. Redirect URI 지정 : http://localhost:8080/login/oauth2/code/kakao
  5. 로그인 활성화 후, 카카오 로그인 보안 항목에서 'Client Secret'으로 생성한 키 복사

 

 


2. OAuth2가 카카오 소셜 로그인을 처리하는 과정

  • REST 방식
  • 문자열로 된 토큰을 주고 받는 방식으로 이루어진다.
  • 발행 후, 발행한 토큰이 맞는지 검사하는 방식으로 서비스 간 데이터를 교환

 

  1. 카카오에서 발급받은 REST API 키를 애플리케이션에 전달해, 인가코드를 받는다.
  2. 받은 인가코드를 '리다이렉트 URI'로 지정된 곳으로 전달 (정해진 패턴의 리다이렉트 URI)
  3. 인가코드 + 비밀키 = Access Token (접근 토큰)생성
  4. 접근 토큰으로 사용자 인증 (사용자의 정보를 요청)

스프링 부트에서 로그인 연동 설정

  1. build.gradle에 spring-boot-starter-oauth2-client 라이브러리 추가
  2. application.properties에 연동을 위한 설정 추가
    1. 카카오 공급자에 대한 정보
    2. 카카오 인증에 필요한 애플리케이션의 정보
    3. 카카오 인증시에 사용하는 메서드와 범위 그리고 사용자 정보
  3. CustomSecurityConfig OAuth2 사용 설정
  4. 로그인 페이지에 카카오 로그인 링크 추가
  5. OAuth2UserService 인터페이스 구현

 

1-1. build.gradle에 spring-boot-starter-oauth2-client 라이브러리 추가

//스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
//OAuth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

 

2-1. 소셜 로그인 공급자인 카카오에 대한 URI정보

  • 인증에 필요한 카카오 URI
  • 사용자 구분 정보
  • 카카오 토큰 URI
  • 카카오 유저 관련 URI
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
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

2-2. 카카오 인증에 필요한 애플리케이션의 정보

  • 인증에 사용하는 소셜 로그인 대상
  • 인가시 필요한 타입 : 인가코드
  • 인증시 리다이렉트 할 URI
  • 카카오 설정 과정에서 만들어진 REST API 키 값
spring.security.oauth2.client.registration.kakao.client-name=kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.redirect_uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.client-id=REST API 키

2-3. 카카오 인증시에 사용하는 메서드와 전달할 사용자 정보 범위

  • 보안 설정시 생성된 값 (비밀키)
  • 인증시 사용하는 HTTP 메서드
  • 인증 완료시 전달할 사용자 정보
spring.security.oauth2.client.registration.kakao.client-secret=설정된 비밀키
spring.security.oauth2.client.registration.kakao.client-authentication-method=POST
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email



3. CustomSecurityConfig OAuth2 사용 설정

-카카오 로그인 방식 추가

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
    log.info("---configure---");

    //별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
    http.formLogin().loginPage("/member/login");

    //remember-me 기능을 위한 쿠키 생성
    http.rememberMe()
            .key("12345678")
            .tokenRepository(persistentTokenRepository())
            .userDetailsService(userDetailsService)
            .tokenValiditySeconds(60*60*24*30);

    //csrf 토큰 비활성화
    http.csrf().disable();

    //403 에러를 커스텀 핸들러로 예외처리
    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());

    //카카오로 로그인 추가
    http.oauth2Login().loginPage("/member/login");
    
    return http.build();
}

 

4. login.html 변경

-추가한 카카오로 로그인하는 링크 추가

<div class="card align-self-center">
    <div class="card-header">
        LOGIN Page
    </div>
    <div class="card-body">
        <!-- 문자열 로그아웃이 null이 아니면 로그아웃 메시지 출력-->
        <th:block th:if="${param.logout != null}">
            <h1>Logout........</h1>
        </th:block>

        <!-- 문자열 로그아웃이 null이면 로그인 화면 출력-->
        <form id="registerForm" action="/member/login" method="post" th:if="${param.logout==null}">
            <div class="input-group mb-3">
                <span class="input-group-text">아이디</span>
                <input type="text" name="username" class="form-control" placeholder="USER ID">
            </div>
            <div class="input-group mb-3">
                <span class="input-group-text">패스워드</span>
                <input type="text" name="password" class="form-control" placeholder="PASSWORD">
            </div>
            <div class="input-group mb-3 ">
                <input class="form-check-input" type="checkbox" name="remember-me">
                <label class="form-check-label">
                    자동 로그인
                </label>
            </div>
            <div class="my-4">
                <div class="float-end">
                    <button type="submit" class="btn btn-primary submitBtn">LOGIN</button>
                </div>
            </div>
        </form>
        <!-- 카카오로 로그인하는 링크-->
        <a href="/oauth2/authorization/kakao">KAKAO</a>

 

5. 로그인 연동해 이메일 구하는 기능

-OAuth2UserService 인터페이스 구현

-OAuth2UserService를 쉽게 구현하기 위해, 하위 클래스인 DefaultOAuth2UserService를 상속해서 구현

 

(1) DefaultOAuth2UserService를 상속하는 CustomOAuth2UserService 클래스 선언

-loadUser() 메서드 오버라이딩

  • 메서드 명 : loadUser
  • 리턴 타입 : OAuth2User
  • 파라미터 : OAuth2UserRequest
  • 예외처리 : OAuth2AuthenticationException
package org.zerock.b01.security;


import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
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;

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

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

        log.info("userRequest...");
        log.info(userRequest);

        //상속한 클래스의 생성자 호출
        return super.loadUser(userRequest);
    }
}

 

(2) loadUser()에서 연동된 결과를 파라미터인 OAuth2UserRequest로 전달

  • loadUser() 메서드 역할 :
    유저이름 출력, userRequest를 OAuth2User타입으로 변환,
    OAuth2User의 사용자 정보를 Map으로 담는다.

 

기능 순서

  1. 유저이름 출력
    1. 파라미터로 getClientRegistration() 호출해서 인증 정보 저장
    2. 인증정보로 getClientName() 호출해서 유저이름 출력
  2. userRequest를 OAuth2User타입으로 변환
    1. loadUser() 상속한 클래스 생성자 호출해 리턴타입인 OAuth2로 변환
  3. 파라미터에 getAttribute() 호출해 사용자 정보를 Map으로 생성
package org.zerock.b01.security;


import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
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 java.util.Map;

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

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

        log.info("userRequest...");
        log.info(userRequest);

        log.info("oauth2 user...");

        //유저 인증 정보 변수
        ClientRegistration clientRegistration=userRequest.getClientRegistration();
        //유저 이름 변수
        String clientName = clientRegistration.getClientName();

        log.info("NAME : "+ clientName);

        //상속한 클래스의 생성자 호출
        //return super.loadUser(userRequest);
        OAuth2User oAuth2User=super.loadUser(userRequest);

        //사용자 정보를 Map으로 저장
        Map<String, Object> paramMap=oAuth2User.getAttributes();

        paramMap.forEach((k,v)->{
            log.info("---");
            log.info(k+":"+v);
        });

        return oAuth2User;
    }
}

 

(3) 다른 소셜 로그인에서도 사용할 수 있도록 처리

-clientRegistration.getClientName()를 통해 각각의 소셜 서비스에 연결한다.

 

변경 전,

package org.zerock.b01.security;


import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
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 java.util.Map;

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

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

        log.info("userRequest...");
        log.info(userRequest);

        log.info("oauth2 user...");

        //유저 인증 정보 변수
        ClientRegistration clientRegistration=userRequest.getClientRegistration();
        //유저 이름 변수
        String clientName = clientRegistration.getClientName();

        log.info("NAME : "+ clientName);

        //상속한 클래스의 생성자 호출
        //return super.loadUser(userRequest);
        OAuth2User oAuth2User=super.loadUser(userRequest);

        //사용자 정보를 Map으로 저장
        Map<String, Object> paramMap=oAuth2User.getAttributes();

        paramMap.forEach((k,v)->{
            log.info("---");
            log.info(k+":"+v);
        });

        return oAuth2User;
    }
}

 

변경 후,

-email 정보를 연동된 서비스에서 가져온다.

package org.zerock.b01.security;


import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
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 java.util.LinkedHashMap;
import java.util.Map;

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

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

        log.info("userRequest...");
        log.info(userRequest);

        log.info("oauth2 user...");

        //유저 인증 정보 변수
        ClientRegistration clientRegistration=userRequest.getClientRegistration();
        //유저 이름 변수
        String clientName = clientRegistration.getClientName();

        log.info("NAME : "+ clientName);

        OAuth2User oAuth2User=super.loadUser(userRequest);

        //사용자 정보를 Map으로 저장
        Map<String, Object> paramMap=oAuth2User.getAttributes();

        // 다른 소셜 서비스도 사용 가능하도록 변경
        //email 변수를 선언 및 null로 초기화
        String email = null;

        switch(clientName){
            case "kakao":
                email=getKakaoEmail(paramMap);
                break;
        }
        log.info("===");
        log.info(email);
        log.info("===");

        return oAuth2User;
    }

    //카카오 소셜 로그인시 이메일 정보가져오기
    private String getKakaoEmail(Map<String, Object> paramMap){
        log.info("KAKAO---");

        //kakao_account라는 키로 접근하는 정보에 이메일 관련 정보를 가지고 있다.
        Object value = paramMap.get("kakao_account");

        log.info(value);

        //순서를 유지하는 해시맵으로 정보 저장
        LinkedHashMap accountMap = (LinkedHashMap) value;

        String email = (String) accountMap.get("email");

        log.info("email..."+email);

        return email;
    }
}

 


3. 소셜 로그인 후처리

  • 소셜 로그인한 사용자에 대한 처리
    • 소셜 로그인에 사용한 이메일이 존재하는 경우
      • 소셜 로그인으로 로그인 완료 : MemberSecurityDTO가 UserDetails와 OAuth2User를 함께 구현
    • 소셜 로그인에 사용한 이메일이 존재하지 않는 경우
      • 새로운 회원으로 간주하고 회원가입 처리 : 생성한 Member 도메인 객체로 MemberSecurityDTO를 생성해 반환
        (social 속성을 true로)
      • 소셜 변수가 필요한 이유
        • 사용자의 이메일을 알아도 로그인시에 false 인 경우에만 조회되므로 로그인 X
        • 하지만, 소셜 로그인한 사용자는 일반 로그인을 하기 위해서는 일반 회원으로 전환하는 화면이 필요

소셜 로그인한 사용자 처리를 위한 구현 순서

  1. 리포지토리
  2. DTO
  3. 서비스

 

1. 리포지토리에서 email 정보로 회원 정보를 찾을 수 있는 메소드 추가

-@EntityGraph : 지연 로딩 사용시 여러번의 성능 저하를 막기 위해 DB 접근을  최소화하기 위해 사용하는 어노테이션

더보기

Lazy 로딩과 @EntityGraph

지연로딩의 경우, 일대다 연관관계에서 게시물 조회시 에러 발생

성능저하 관계없이 문제 해결하는 방법

 

성능 저하가 발생하지만 간단하게 해결하는 방법

  • fetchType.EAGER로 변경
  • @Transational : 필요할 때마다 메소드 내에서 쿼리를 여러번 실행 

 

@EntityGraph

지연 로딩 사용시 여러번의 성능 저하를 막기 위해 DB 접근을  최소화하기 위해
attributePaths 속성으로 필요한 데이터들을 한번의 조인으로 select가 이루어지도록 한다.

package org.zerock.b01.repository;

import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.zerock.b01.domain.Member;

import javax.persistence.Entity;
import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, String> {

    //@EntityGraph로 회원 구분을 가져와서 일대다 연관관계의 지연로딩이어도 최소한의 DB 접근 구현
    @EntityGraph(attributePaths = "roleSet")
    @Query("select m from Member m where m.mid= :mid and m.social = false")
    Optional<Member> getWithRoles(String mid);

    //@EntityGraph로 회원 구분을 가져와서 일대다 연관관계의 지연로딩이어도 최소한의 DB 접근 구현
    //JpaRepository를 상속해서 쿼리메서드 사용가능
    @EntityGraph(attributePaths = "roleSet")
    Optional<Member> findByEmail(String email);
}

 

2. MemberSecurityDTO에 소셜 로그인에서도 사용할 수 있도록 OAuth2User 인터페이스 구현

  • 클래스명 : MemberSecurityDTO
  • 상속한 클래스 : User
  • 구현한 인터페이스 : OAuth2User
  • 구현해야할 메서드 : getAttribute(), getName()
    • 소셜 로그인 정보를 props 멤버 변수로 선언해 처리

변경 전, User 클래스만 상속한 MemberSecurityDTO

package org.zerock.b01.security.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

@Getter
@Setter
@ToString
public class MemberSecurityDTO extends User {

    private String mid;
    private String mpw;
    private String email;
    private boolean del;
    private boolean social;


    public MemberSecurityDTO(String username, String password, String email, boolean del, boolean social, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);

        this.mid=username;
        this.mpw=password;
        this.email= email;
        this.del=del;
        this.social=social;
    }
}

 

변경 후, OAuth2User인터페이스 구현 + 클래스 상속한 MemberSecurityDTO

package org.zerock.b01.security.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.core.user.OAuth2User;

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

@Getter
@Setter
@ToString
public class MemberSecurityDTO extends User implements OAuth2User {

    private String mid;
    private String mpw;
    private String email;
    private boolean del;
    private boolean social;

    //소셜 로그인 정보를 담은 Map
    private Map<String, Object> props;

    public MemberSecurityDTO(String username, String password, String email, boolean del, boolean social, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);

        this.mid=username;
        this.mpw=password;
        this.email= email;
        this.del=del;
        this.social=social;
    }

    //OAuth2 인터페이스 구현을 위한 메서드 오버라이딩
    @Override
    public Map<String, Object> getAttributes() {
        
        return this.getProps();
    }

    @Override
    public String getName() {
        return this.mid;
    }

}

 

3. CustomOAuth2UserService에 카카오 서비스로부터의 받은 이메일 중복확인

-이메일 중복확인 후, 같은 이메일 가진 사용자 없을 때 : 자동 회원가입 후, MemberSecurityDTO를 반환

-이메일 중복확인 후, 같은 이메일 가진 사용자 있을 때 : 일반 회원으로 로그인하도록 처리

 

변경 전, CustomOAuth2UserService

package org.zerock.b01.security;


import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
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 java.util.LinkedHashMap;
import java.util.Map;

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

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

        log.info("userRequest...");
        log.info(userRequest);

        log.info("oauth2 user...");

        //유저 인증 정보 변수
        ClientRegistration clientRegistration=userRequest.getClientRegistration();
        //유저 이름 변수
        String clientName = clientRegistration.getClientName();

        log.info("NAME : "+ clientName);

        OAuth2User oAuth2User=super.loadUser(userRequest);

        //사용자 정보를 Map으로 저장
        Map<String, Object> paramMap=oAuth2User.getAttributes();

        // 다른 소셜 서비스도 사용 가능하도록 변경
        //email 변수를 선언 및 null로 초기화
        String email = null;

        switch(clientName){
            case "kakao":
                email=getKakaoEmail(paramMap);
                break;
        }
        log.info("===");
        log.info(email);
        log.info("===");

        return oAuth2User;
    }

    //카카오 소셜 로그인시 이메일 정보가져오기
    private String getKakaoEmail(Map<String, Object> paramMap){
        log.info("KAKAO---");

        //kakao_account라는 키로 접근하는 정보에 이메일 관련 정보를 가지고 있다.
        Object value = paramMap.get("kakao_account");

        log.info(value);

        //순서를 유지하는 해시맵으로 정보 저장
        LinkedHashMap accountMap = (LinkedHashMap) value;

        String email = (String) accountMap.get("email");

        log.info("email..."+email);

        return email;
    }
}

 

 

변경 후, 카카오 서비스에서 얻은 이메일로 같은 이메일을 가진 사용자 찾기는 메서드 작성

  • 메서드 명 : generateDTO
  • 리턴타입 : MemberSecurityDTO
  • 파라미터 : email, Map<String, Object> paramMap
  • 역할 : 소셜 로그인에서 얻은 아이디가 존재하는지 여부 확인 후,
    아이디가 존재하지 않으면 : 회원가입 처리 후 MemberSecurity 구성
    아이디가 존재하면 :  member를 이용해 MemberSecurity 구성
package org.zerock.b01.security;


import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
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 org.zerock.b01.domain.Member;
import org.zerock.b01.domain.MemberRole;
import org.zerock.b01.repository.MemberRepository;
import org.zerock.b01.security.dto.MemberSecurityDTO;
import org.zerock.b01.service.MemberService;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    //의존성 주입
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

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

        log.info("userRequest...");
        log.info(userRequest);

        log.info("oauth2 user...");

        //유저 인증 정보 변수
        ClientRegistration clientRegistration=userRequest.getClientRegistration();
        //유저 이름 변수
        String clientName = clientRegistration.getClientName();

        log.info("NAME : "+ clientName);

        OAuth2User oAuth2User=super.loadUser(userRequest);

        //사용자 정보를 Map으로 저장
        Map<String, Object> paramMap=oAuth2User.getAttributes();

        // 다른 소셜 서비스도 사용 가능하도록 변경
        //email 변수를 선언 및 null로 초기화
        String email = null;

        switch(clientName){
            case "kakao":
                email=getKakaoEmail(paramMap);
                break;
        }
        log.info("===");
        log.info(email);
        log.info("===");

        //카카오에서 얻은 사용자 정보인 이메일을 이용해 회원가입할지, 로그인할지 처리
        return generateDTO(email, paramMap);
    }


    //카카오에서 얻은 이메일로 사용자 회원가입여부 확인
    private MemberSecurityDTO generateDTO(String email, Map<String, Object> paramMap){

        //카카오에서 얻은 이메일가 리포지토리에 존재여부
        Optional<Member> result = memberRepository.findByEmail(email);

        //아이디가 존재하지 않는다면 회원 가입 후 MemberSecurityDTO 구성하고 반환한다.
        if(result.isEmpty()){
            //소셜 회원 추가
            //소셜 회원은 mid가 email이고, 비밀번호가 1111 고정 된 채로 회원가입
            Member member= Member.builder()
                    .mid(email)
                    .mpw(passwordEncoder.encode("1111"))
                    .email(email)
                    .social(true)
                    .build();

            //회원 구분 설정
            member.addRole(MemberRole.USER);
            memberRepository.save(member);


            MemberSecurityDTO memberSecurityDTO=
                    new MemberSecurityDTO(email, "1111", email, false, true,
                            Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));

            //카카오에서 받은 사용자 정보를 이용해 소셜 로그인 정보 설정
            memberSecurityDTO.setProps(paramMap);

            return memberSecurityDTO;

        }
        //아이디가 존재하면
        else{
            Member member=result.get();
            MemberSecurityDTO memberSecurityDTO=
                    new MemberSecurityDTO(
                            member.getMid(),
                            member.getMpw(),
                            member.getEmail(),
                            member.isDel(),
                            member.isSocial(),
                            member.getRoleSet()
                                    .stream().map(memberRole -> new SimpleGrantedAuthority("ROLE_"+memberRole.name())).collect(Collectors.toList())
                    );

            return memberSecurityDTO;
        }

    }

    //카카오 소셜 로그인시 이메일 정보가져오기
    private String getKakaoEmail(Map<String, Object> paramMap){
        log.info("KAKAO---");

        //kakao_account라는 키로 접근하는 정보에 이메일 관련 정보를 가지고 있다.
        Object value = paramMap.get("kakao_account");

        log.info(value);

        //순서를 유지하는 해시맵으로 정보 저장
        LinkedHashMap accountMap = (LinkedHashMap) value;

        String email = (String) accountMap.get("email");

        log.info("email..."+email);

        return email;
    }
}

-> 소셜 로그인은 일반 로그인 불가능

-> 일반 로그인하기 위해서는 로그인 후, 사용자 정보를 일반 회원으로 수정해야 한다.

 

4. 소셜 로그인 성공시 사용자 패스워드에 따라 사용자 정보 수정 혹은 특정 페이지 이동 처리

-AuthenticationSuccessHandler / AuthenticationSuccessHandler :

스프링 시큐리티에서 로그인 성공과 실패를 커스터마이징하도록 제공하는 인터페이스

 

(1) 로그인 성공시 처리하기 위해 AuthenticationSuccessHandler 인터페이스를 구현하는 CustomSocialLoginSuccessHandler 클래스 작성

-소셜 로그인 성공 && 기본 비밀번호면 : 비밀번호 수정하는 페이지로 이동

-소셜 로그인 성공만 : 목록 페이지로 이동

  • 구현해야할 메서드 명 : onAuthenticationSuccess
  • 파라미터 : HttpServletRequest, HttpServletResponse, Authentication
  • 예외처리 : IOException, ServletException
  • 멤버 변수
    1. memberSecurityDTO : 파라미터인 Authentication로 getPrincipal() 메서드 호출해 MemberSecurityDTO 변수 선언 및 초기화
    2. endcodePw : MemberSecurityDTO에서 인코딩한 패스워드 변수 선언 및 초기화
  • 역할
    1. 소셜 로그인 성공 && 기본 비밀번호면 -> 수정페이지로 이동
    2. 소셜 로그인이 아니거나, 기본 비밀번호가 아니면 -> 목록페이지로 이동
package org.zerock.b01.security.handler;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.zerock.b01.security.dto.MemberSecurityDTO;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Log4j2
@RequiredArgsConstructor
public class CustomSocialLoginSuccessHandler implements AuthenticationSuccessHandler {
    private final PasswordEncoder passwordEncoder;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("---");
        log.info("CustomLoginSuccessHandler onAuthenticationSuccess ...");
        log.info(authentication.getPrincipal());

        MemberSecurityDTO memberSecurityDTO = (MemberSecurityDTO) authentication.getPrincipal();

        String encodePw = memberSecurityDTO.getMpw();
        
        //소셜 로그인과 기본 패스워드 확인
        if(memberSecurityDTO.isSocial() && (memberSecurityDTO.getMpw().equals("1111") || passwordEncoder.matches("1111", memberSecurityDTO.getMpw()))){
            log.info("Should Change Password");
            log.info("Redirect to Member Modify");
            
            response.sendRedirect("/member/modify");
            
            //return;
        }
        else{
            response.sendRedirect("/board/list");
        }
    }
}

 

(2) 시큐리티 설정에 해당 핸들러 추가

-CustomSecurityConfig에 OAuth2를 이용한 로그인 성공 처리시 CustomSocialLoginSuccessHandler 이용하도록 추가

 

변경 전, filterChain()

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    log.info("---configure---");

    //별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
    http.formLogin().loginPage("/member/login");

    //remember-me 기능을 위한 쿠키 생성
    http.rememberMe()
            .key("12345678")
            .tokenRepository(persistentTokenRepository())
            .userDetailsService(userDetailsService)
            .tokenValiditySeconds(60 * 60 * 24 * 30);

    //csrf 토큰 비활성화
    http.csrf().disable();

    //403 에러를 커스텀 핸들러로 예외처리
    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());

    
    //카카오로 로그인 추가
    http.oauth2Login()
            .loginPage("/member/login");

    return http.build();
}

 

변경 후, filterChain()

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    log.info("---configure---");

    //별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
    http.formLogin().loginPage("/member/login");

    //remember-me 기능을 위한 쿠키 생성
    http.rememberMe()
            .key("12345678")
            .tokenRepository(persistentTokenRepository())
            .userDetailsService(userDetailsService)
            .tokenValiditySeconds(60 * 60 * 24 * 30);

    //csrf 토큰 비활성화
    http.csrf().disable();

    //403 에러를 커스텀 핸들러로 예외처리
    http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());

    
    //카카오로 로그인 추가
    //+) 카카오로 로그인시 로그인 성공했을 때, 핸들러 호출
    http.oauth2Login()
            .loginPage("/member/login")
            .successHandler(authenticationSuccessHandler());

    return http.build();
}

 

(3) MemberRepository 패스워드 수정 메서드 작성

-소셜 로그인시 기본 패스워드가 인코딩한 값으로 저장된다.

-리포지토리에 패스워드 수정하는 메서드 작성

//패스워드 수정 메서드
@Modifying
@Transactional
@Query("update Member m set m.mpw =:mpw where m.mid =:mid")
void updatePassword(@Param("mpw") String password, @Param("mid") String mid);
  • @Query + @Modifying : DML 처리 (insert/update/delete)
    (@Query는 기본적으로 select시 사용하지만, @Modifying과 함께 사용해 DML문을 처리한다.)

(4) 패스워드 수정하는 테스트 코드 작성

//멤버 패스워드 수정하는 테스트
@Commit
@Test
public void testUpdate(){

    String mid="cookie_00naver.com";
    String mpw= passwordEncoder.encode("54321");

    memberRepository.updatePassword(mpw, mid);
}

 


 

 

반응형