전체 방문자
오늘
어제
21종
종이의 코딩 공부방
21종
  • 분류 전체보기 (171)
    • JAVA (64)
    • Springboot (46)
      • 블로그만들기 (45)
    • Database (60)
      • Oracle (60)
    • 프로젝트 3 (CELOVER) (0)
    • 개발서버 구축 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

최근 글

최근 댓글

hELLO · Designed By 정상우.
21종

종이의 코딩 공부방

Springboot/블로그만들기

[Springboot] 블로그 만들기 (34)_카카오 로그인(3) 로그인 및 회원가입 구현완료

2023. 12. 1. 21:24

카카오 로그인 REST API 문서

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com


먼저 카카오톡 로그인 버튼을 다운받자

https://developers.kakao.com/tool/resource/login

 

다운받은 파일을 내 프로젝트의 src/main/resources/static/image에 추가하자

 

 

loginForm.jsp에 카카오 로그인 버튼을 추가하자.

url은 KakaoDeveloper 공식문서에 있는 URL을 넣어주면된다.

 


로그인 요청을 위한 인증 코드를 받자

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code

이 세가지 항목이 필수 값이다. 조합을 해보면 아래와 같이 URL이 완성된다.

 

로그인 요청 주소 (GET 방식)
https://kauth.kakao.com/oauth/authorize?client_id=클라이언트키&redirect_uri=콜백주소&response_type=code

 

loginForm.jsp

자신의 클라이언트키와 콜백주소를 넣어준다. 

<button id="btn-login" class="btn btn-primary">로그인</button>
<a href="https://kauth.kakao.com/oauth/authorize?client_id=클라이언트키&redirect_uri=콜백주소&response_type=code"><img height="38px" src="/img/kakao_btn.png" /></a>

 

 

동의하고 계속하기를 누르면 에러페이지와 함께 쿼리스트링으로 인증코드를 받게 된다. 

http://localhost:8001/auth/kakao/callback?code=~~~~~~~~

 

이제 이 요청주소에 대한 컨트롤러를 만들자

 

UserController.java

@GetMapping("auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수, 쿼리스트링에 있는 code값을 받을 수 있음  
    return "카카오 인증 완료" + code;
}

 

카카오 인증 성공


이제 인증토큰을 받아보자

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token

https://kauth.kakao.com/oauth/token 에 POST 방식으로 application/x-www/form-urlencoded:charset=utf-8 (key=value 형태)의 데이터를 던지라는 뜻 

 

토큰 발급 주소 (POST) - http body에 데이터를 전달 (4가지 데이터)
https://kauth.kakao.com/oauth/token

UserController.java

@GetMapping("auth/kakao/callback")
public @ResponseBody String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수, 쿼리스트링에 있는 code값을 받을 수 있음  
    // POST 방식으로 key=value 데이터를 요청 (카카오톡으로)
    /* Retrofit2
     * OkHttp
     * HttpsURLConnection
     * RestTemplate (우린 이걸로 할거다)
     */
    RestTemplate rt = new RestTemplate();

    // HttpHeader 오브젝트 생성
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 내가 지금 전송할 body data 가 key=velue 형임을 명시

    // HttpBody 오브젝트 생성
    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("grant_type", "authorization_code");
    params.add("client_id", "내 RestAPI앱키");
    params.add("redirect_uri", "http://localhost:8001/auth/kakao/callback");
    params.add("code", code);

    // HttpHeader 와 HttpBody를 하나의 오브젝트에 담기
    HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = 
            new HttpEntity<>(params, headers); // header 와 body 값을 가지고 있는 entity 값이 된다.

    // Http 요청하기 - Post 방식으로 - 그리고 Response 변수의 응답 받음.
    ResponseEntity<String> response = rt.exchange(
                "https://kauth.kakao.com/oauth/token",
                HttpMethod.POST,
                kakaoTokenRequest,
                String.class // String 타입으로 응답 데이터를 받겠다.
            );

    return "카카오 토큰 요청 완료" + response;
}

 

카카오 토큰 요청 성공

200은 성공이라는 뜻

응답데이터인 Response에 있는 데이터를 담을 수 있는 자바 객체로 만들자

OAuthToken.java

package com.lwj.blog.model;

import lombok.Data;

@Data // parsing 할때 getter setter 필요
public class OAuthToken {

	// 스펠링이 틀리지 않도록 조심하자
	private String access_token;
	private String token_type;
	private String refresh_token;
	private int expires_in;
	private String scope;
	private int refresh_token_expires_in;
}

 

UserController.java

// Gson, Json, Simple, ObjectMapper라이브러리를 사용하여 자바객체로 만들 수 있다.
ObjectMapper objectMapper = new ObjectMapper();
OAuthToken oauthToken = null;
try {
    oauthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
} catch (JsonMappingException e) {
    e.printStackTrace();
} catch (JsonProcessingException e) {
    e.printStackTrace();
}
System.out.println("카카오 액세스 토큰 : " + oauthToken.getAccess_token());


이제 토큰을 가지고 유저 정보를 불러오자

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info

UserController.java

RestTemplate rt2 = new RestTemplate();

// HttpHeader 오브젝트 생성
HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + oauthToken.getAccess_token());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 내가 지금 전송할 body data 가
                                                                                    // key=velue 형임을 명시

// HttpHeader 와 HttpBody를 하나의 오브젝트에 담기
HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers2);

// Http 요청하기 - Post 방식으로 - 그리고 Response 변수의 응답 받음.
ResponseEntity<String> response2 = rt2.exchange("https://kapi.kakao.com/v2/user/me", HttpMethod.POST,
        kakaoProfileRequest, String.class // String 타입으로 응답 데이터를 받겠다.
);

System.out.println("유저정보 : " + response2.getBody());

 

다시 카카오 로그인을 실행하면 데이터를 확인 할 수 있다.

 

이 데이터를 담을 자바 오브젝트로 만들자.

KakaoProfile.java

package com.lwj.blog.model;

import lombok.Data;

@Data
public class KakaoProfile {

public Long id;
public String connected_at;
public Properties properties;
public Kakao_account kakao_account;

	@Data
	public class Kakao_account {
		
		public Boolean profile_nickname_needs_agreement;
		public Boolean profile_image_needs_agreement;
		public Profile profile;
		public Boolean has_email;
		public Boolean email_needs_agreement;
		public Boolean is_email_valid;
		public Boolean is_email_verified;
		public String email;
		
		@Data
		public class Profile {
			
			public String nickname;
			public String thumbnail_image_url;
			public String profile_image_url;
			public Boolean is_default_image;
			
		}
	}
	
	@Data
	public class Properties {
		
		public String nickname;
		public String profile_image;
		public String thumbnail_image;
		
	}
}

UserController.java

// Gson, Json, Simple, ObjectMapper
ObjectMapper objectMapper2 = new ObjectMapper();
KakaoProfile kakaoProfile = null;
try {
    kakaoProfile = objectMapper2.readValue(response2.getBody(), KakaoProfile.class);
} catch (JsonMappingException e) {
    e.printStackTrace();
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

// User 오브젝트 : userName, password, email
System.out.println("카카오 아이디(번호) : " + kakaoProfile.getId());
System.out.println("카카오 이메일 : " + kakaoProfile.getKakao_account().getEmail());


이제 마지막으로 카카오계정으로 로그인 및 회원가입을 구현해보자

User.java

length를 30 -> 100 변경

@Column(nullable=false, unique = true, length = 100)
private String userName; // 아이디

 

UserController.java

// 상단에 추가
@Value("${lwj.key}")
private String lwjKey;

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserService userService;

--------------------------------------------------------------------------

System.out.println("블로그서버 유저네임 : " + kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId());
System.out.println("블로그서버 이메일 : " + kakaoProfile.getKakao_account().getEmail());
// UUID garbagePassword = UUID.randomUUID(); 
// UUID란 -> 중복되지 않는 어떤 특정 값을 만들어내는 알고리즘
//	 System.out.println("블로그서버 패스워드 : " + garbagePassword); // DB에 넣기위한 랜덤 임시 비밀번호
System.out.println("블로그서버 패스워드 : " + lwjKey); // DB에 넣기위한 랜덤 임시 비밀번호

User kakakoUser = User.builder()
        .userName( kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId())
        .password(lwjKey)
        .email(kakaoProfile.getKakao_account().getEmail())
        .build(); 

// 이미 등록된 회원이면 로그인 , 등록되있지 않은 회원일 경우 로그인을 하는 분기 처리를 해야한다.
// 가입자 혹은 비가입자 체크 해서 처리
User originUser = userService.회원찾기(kakakoUser.getUserName());

if(originUser.getUserName() == null) {
    System.out.println("기존 회원이 아닙니다. ~~~");
    userService.회원가입(kakakoUser);			
}

System.out.println("자동 로그인을 진행합니다.");
// 로그인 처리
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(kakakoUser.getUserName(), lwjKey));
SecurityContextHolder.getContext().setAuthentication(authentication);

return "redirect:/";

application.yml

lwj:
  key: lwj1234 # 카카오 회원가입한 회원들의 비밀번호, 절대로 노출되면 안된다.

 

하지만 OAuth로 로그인 한 회원은 회원정보를 수정하지 못하게 막아야한다.

User.java

먼저 User객체에 이 회원이 OAuth로 회원가입을 했는지 일반회원가입을 했는지 담아주는 속성을 추가해준다.

private String oauth; // kakao, google

updateForm.jsp

<!-- 이렇게만 막으면 보안에 취약하다 서버쪽에서도 막아줘야한다. -->
<c:if test="${empty principal.user.oauth }"> 
    <div class="form-group">
        <label for="pwd">Password</label> 
        <input type="password" class="form-control" placeholder="password"  id="password">
    </div>		
    <div class="form-group">
        <label for="email">Email</label> 
        <input type="email" value="${ principal.user.email }" class="form-control" placeholder="email" id="email" >
    </div>
</c:if>
<div class="form-group">
    <label for="email">Email</label> 
    <input type="email" value="${ principal.user.email }" class="form-control" placeholder="email" id="email"  readonly>
</div>

UserService.java

@Transactional
public User 회원찾기(String userName) {
    User user = userRepository.findByUserName(userName).orElseGet(()->{
        return new User(); // 비어있는 객체 반환
    });
    return user;
}

@Transactional
public void 회원수정(User user) {   
    // 수정시에는 영속성 컨텍스트 User 오브젝트를 영속화시키고, 영속화된 User 오브젝트를 수정
    // select 를 해서 User 오브젝트를 DB로 부터 가져오는 이유는 영속화를 하기 위해서 !!
    // 영속화된 오브젝트를 변경하면 자동으로 DB에 update 문을 날려준다
    User persistance = userRepository.findById(user.getId()).orElseThrow(()->{
        return new IllegalArgumentException("회원 찾기 실패");
    });

    // Validate 체크 -> oauth 필드에 값이 없으면 수정가능
    if(persistance.getOauth() == null || persistance.getOauth().equals("")) {
        String rawPassword = user.getPassword();
        String encPassword = encoder.encode(rawPassword);
        persistance.setPassword(encPassword);
        persistance.setEmail(user.getEmail());			
    }


    // 회원수정 함수 종료시 = 서비스 종료시 = 트랜잭션 종료 = commit이 자동으로 된다.
    // 영속화된 persistance 객체의 변화가 감지되면 더티체킹이 되어 update문을 날려줌.
}

 


전체코드

UserController.java

@Controller
public class UserController {
	
	@Value("${lwj.key}")
	private String lwjKey;
	
	@Autowired
	private AuthenticationManager authenticationManager;
	
	@Autowired
	private UserService userService;

	@GetMapping("auth/kakao/callback")
	public  String kakaoCallback(String code) { // Data를 리턴해주는 컨트롤러 함수, 쿼리스트링에 있는 code값을 받을 수 있음
		// POST 방식으로 key=value 데이터를 요청 (카카오톡으로)
		/*
		 * Retrofit2 OkHttp HttpsURLConnection RestTemplate (우린 이걸로 할거다)
		 */
		RestTemplate rt = new RestTemplate();

		// HttpHeader 오브젝트 생성
		HttpHeaders headers = new HttpHeaders();
		headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 내가 지금 전송할 body data 가
																						// key=velue 형임을 명시

		// HttpBody 오브젝트 생성
		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
		params.add("grant_type", "authorization_code");
		params.add("client_id", "b9d1d15c7c76c5b18e989b19383acaf0");
		params.add("redirect_uri", "http://localhost:8001/auth/kakao/callback");
		params.add("code", code);

		// HttpHeader 와 HttpBody를 하나의 오브젝트에 담기
		HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = 
				new HttpEntity<>(params, headers); // header 와 body 값을 가지고 있는 entity 값이 된다.

		// Http 요청하기 - Post 방식으로 - 그리고 Response 변수의 응답 받음.
		ResponseEntity<String> response = rt.exchange(
				"https://kauth.kakao.com/oauth/token", 
				HttpMethod.POST,
				kakaoTokenRequest, 
				String.class // String 타입으로 응답 데이터를 받겠다.
		);

		// Gson, Json, Simple, ObjectMapper라이브러리를 사용하여 자바객체로 만들 수 있다.
		ObjectMapper objectMapper = new ObjectMapper();
		OAuthToken oauthToken = null;
		try {
			oauthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
		} catch (JsonMappingException e) {
			e.printStackTrace();
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}
		System.out.println("카카오 액세스 토큰 : " + oauthToken.getAccess_token());

		RestTemplate rt2 = new RestTemplate();

		// HttpHeader 오브젝트 생성
		HttpHeaders headers2 = new HttpHeaders();
		headers2.add("Authorization", "Bearer " + oauthToken.getAccess_token());
		headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); // 내가 지금 전송할 body data 가
																							// key=velue 형임을 명시

		// HttpHeader 와 HttpBody를 하나의 오브젝트에 담기
		HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers2);

		// Http 요청하기 - Post 방식으로 - 그리고 Response 변수의 응답 받음.
		ResponseEntity<String> response2 = rt2.exchange("https://kapi.kakao.com/v2/user/me", HttpMethod.POST,
				kakaoProfileRequest, String.class // String 타입으로 응답 데이터를 받겠다.
		);
		
		System.out.println("유저정보 : " + response2.getBody());
		
		// Gson, Json, Simple, ObjectMapper
		ObjectMapper objectMapper2 = new ObjectMapper();
		KakaoProfile kakaoProfile = null;
		try {
			kakaoProfile = objectMapper2.readValue(response2.getBody(), KakaoProfile.class);
		} catch (JsonMappingException e) {
			e.printStackTrace();
		} catch (JsonProcessingException e) {
			e.printStackTrace();
		}
		
		// User 오브젝트 : userName, password, email
		System.out.println("카카오 아이디(번호) : " + kakaoProfile.getId());
		System.out.println("카카오 이메일 : " + kakaoProfile.getKakao_account().getEmail());

		System.out.println("블로그서버 유저네임 : " + kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId());
		System.out.println("블로그서버 이메일 : " + kakaoProfile.getKakao_account().getEmail());
		// UUID garbagePassword = UUID.randomUUID(); 
		// UUID란 -> 중복되지 않는 어떤 특정 값을 만들어내는 알고리즘
		//	 System.out.println("블로그서버 패스워드 : " + garbagePassword); // DB에 넣기위한 랜덤 임시 비밀번호
		System.out.println("블로그서버 패스워드 : " + lwjKey); // DB에 넣기위한 랜덤 임시 비밀번호
		
		User kakakoUser = User.builder()
				.userName( kakaoProfile.getKakao_account().getEmail() + "_" + kakaoProfile.getId())
				.password(lwjKey)
				.email(kakaoProfile.getKakao_account().getEmail())
				.oauth("kakao")
				.build(); 

		// 이미 등록된 회원이면 로그인 , 등록되있지 않은 회원일 경우 로그인을 하는 분기 처리를 해야한다.
		// 가입자 혹은 비가입자 체크 해서 처리
		User originUser = userService.회원찾기(kakakoUser.getUserName());
		
		if(originUser.getUserName() == null) {
			System.out.println("기존 회원이 아닙니다. ~~~");
			userService.회원가입(kakakoUser);			
		}
		
		System.out.println("자동 로그인을 진행합니다.");
		// 로그인 처리
		Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(kakakoUser.getUserName(), lwjKey));
		SecurityContextHolder.getContext().setAuthentication(authentication);
		
		// OAuth로 로그인한 회원은 회원정보를 수정하지 못하게 해야된다.
		
		return "redirect:/";
	}

}

UserService.java

@Service
// service : 트랜젝션 관리를 위해 사용
public class UserService {

	@Autowired
	private UserRepository userRepository;
	
	@Autowired
	private BCryptPasswordEncoder encoder;
	
	@Transactional
	public User 회원찾기(String userName) {
		User user = userRepository.findByUserName(userName).orElseGet(()->{
			return new User(); // 비어있는 객체 반환
		});
		return user;
	}
	
	@Transactional
	public void 회원수정(User user) {   
		// 수정시에는 영속성 컨텍스트 User 오브젝트를 영속화시키고, 영속화된 User 오브젝트를 수정
		// select 를 해서 User 오브젝트를 DB로 부터 가져오는 이유는 영속화를 하기 위해서 !!
		// 영속화된 오브젝트를 변경하면 자동으로 DB에 update 문을 날려준다
		User persistance = userRepository.findById(user.getId()).orElseThrow(()->{
			return new IllegalArgumentException("회원 찾기 실패");
		});
		
		// Validate 체크 -> oauth 필드에 값이 없으면 수정가능
		if(persistance.getOauth() == null || persistance.getOauth().equals("")) {
			String rawPassword = user.getPassword();
			String encPassword = encoder.encode(rawPassword);
			persistance.setPassword(encPassword);
			persistance.setEmail(user.getEmail());			
		}
		
		
		// 회원수정 함수 종료시 = 서비스 종료시 = 트랜잭션 종료 = commit이 자동으로 된다.
		// 영속화된 persistance 객체의 변화가 감지되면 더티체킹이 되어 update문을 날려줌.
	}
}

User.java

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
	
	@Id // Primary key
	@GeneratedValue(strategy = GenerationType.IDENTITY) 
	
	@Column(nullable=false, unique = true, length = 100)
	private String userName; // 아이디
	
	@Column(nullable=false, length = 100) 
	// 100은 너무 길잖아 why ? 123456 => 해쉬 (비밀번호 암호화-> 엄청길다)
	private String password; // 비밀번호
	
	@Column(nullable=false, length = 50)
	private String email; // 이메일
	
	@Enumerated(EnumType.STRING)
	private RoleType role; // ADMIN, USER 만 값이 들어가게된다. 
	
	private String oauth; // kakao, google
	
	@CreationTimestamp // 현재시간으로 시간이 자동 입력
	private Timestamp createDate;
	
}

KakaoProfile.java

@Data
public class KakaoProfile {

public Long id;
public String connected_at;
public Properties properties;
public Kakao_account kakao_account;

	@Data
	public class Kakao_account {
		
		public Boolean profile_nickname_needs_agreement;
		public Boolean profile_image_needs_agreement;
		public Profile profile;
		public Boolean has_email;
		public Boolean email_needs_agreement;
		public Boolean is_email_valid;
		public Boolean is_email_verified;
		public String email;
		
		@Data
		public class Profile {
			
			public String nickname;
			public String thumbnail_image_url;
			public String profile_image_url;
			public Boolean is_default_image;
			
		}
	}
	
	@Data
	public class Properties {
		
		public String nickname;
		public String profile_image;
		public String thumbnail_image;
		
	}
}

OAuthToken.java

@Data // parsing 할때 getter setter 필요
public class OAuthToken {

	// 스펠링이 틀리지 않도록 조심하자
	private String access_token;
	private String token_type;
	private String refresh_token;
	private int expires_in;
	private String scope;
	private int refresh_token_expires_in;
}

updateForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ include file="../layout/header.jsp"%>

<div class="container">
	<form>
		<input type="hidden" id="id" value="${principal.user.id }">
		<div class="form-group">
			<label for="userName">UserName</label> 
			<input type="text" value="${ principal.user.userName }" class="form-control" placeholder="userName"  id="userName" readonly>
		</div>
		
		<!-- 이렇게만 막으면 보안에 취약하다 서버쪽에서도 막아줘야한다. -->
		<c:if test="${empty principal.user.oauth }"> 
			<div class="form-group">
				<label for="pwd">Password</label> 
				<input type="password" class="form-control" placeholder="password"  id="password">
			</div>		
			<div class="form-group">
				<label for="email">Email</label> 
				<input type="email" value="${ principal.user.email }" class="form-control" placeholder="email" id="email" >
			</div>
		</c:if>
		<div class="form-group">
			<label for="email">Email</label> 
			<input type="email" value="${ principal.user.email }" class="form-control" placeholder="email" id="email"  readonly>
		</div>
		
		

	</form>
	<button id = "btn-update" class="btn btn-primary">회원수정완료</button>
</div>


<script src="/js/user.js"></script>
<%@ include file="../layout/footer.jsp"%>

 


참고 유튜브 (메타코딩님 강의)

https://youtu.be/WACDTFroYNI?si=CCBab9RnjVhusVWM

    'Springboot/블로그만들기' 카테고리의 다른 글
    • [Springboot] 블로그 만들기 (36)_무한참조 방지하기
    • [Springboot] 블로그 만들기 (35)_댓글 디자인, 불러오기
    • [Springboot] 블로그 만들기 (33)_카카오 로그인(2) OAuth2.0 개념
    • [Springboot] 블로그 만들기 (32)_카카오 로그인(1) 환경설정
    21종
    21종
    코딩 공부한 것 정리하려고 만든 블로그

    티스토리툴바