사전 정보 - OAuth & JWT 개념을 재점검해야 합니다.
https://devnote0203.tistory.com/129 (네이버 로그인 연동때 설명 기술)
토스페이먼츠 개발자센터 JWT(JSON 웹 토큰) 설명
https://docs.tosspayments.com/resources/glossary/jwt
JWT(JSON 웹 토큰) | 토스페이먼츠 개발자센터
JSON 웹 토큰(JWT)은 온라인 네트워크에서 정보를 안전하게 통신할 때 사용하는 인터넷 표준 토큰입니다.
docs.tosspayments.com
0. Kakao Developers
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
1. 내 애플리케이션 > 애플리케이션 추가하기
2. 내 애플리케이션 > 앱 설정 > 플랫폼 등록하기
3. Redirect URI 등록하기
4. 내 애플리케이션 > 제품 설정 > 카카오 로그인 > 동의항목 설정하기
카카오 로그인은 닉네임과 프로필 사진을 기본으로 제공합니다. 이외의 유저 정보는 사업자 등록증을 제출해야 하기 때문에 두 항목만 필수 동의로 설정하였습니다.
문서 > 카카오 로그인 > REST API 문서를 참고하여 카카오 로그인 구현하기
5. 인가 코드받기
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
요청문
https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=
${REST_API_KEY}
&redirect_uri=
${REDIRECT_URI}
6. 토큰 받기
7. 액세스 토큰 방식으로 유저 정보 받아오기
코드
동의 화면을 출력하기 위한 링크입니다. ClientId등의 값은 yml에 선언했습니다.
<a href="https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${kakaoRestApiKey}&redirect_uri=${kakaoRedirectUri}">
<img alt="카카오 로그인 버튼" class="social--btn" src="/images/kakao_login_medium_narrow.png">
</a>
yml 파일
kakao:
client-id: 49a90f391b06f25728ec0d6f9f30ce86 # 발급 받은 REST API 키
redirect-uri: http://localhost:8080/user/kakao #
client-authentication-method: client_secret_post
# scope:
# - name
# - email
# - profile_image
client-name: Kakao
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
@Value("${kakao.client-id}")
private String kakaoClientId;
@Value("${kakao.redirect-uri}")
private String kakaoRedirectUri;
private KakaoUserInfoDTO kakaoUserInfoDTO;
/**
* 카카오 로그인 API
*
* @param code
* @return
*/
@GetMapping("/kakao")
public String getMethodName(@RequestParam(name = "code", required = false) String code, Model model) {
// Access Token 발급 요청
RestTemplate rt1 = new RestTemplate();
HttpHeaders header1 = new HttpHeaders();
header1.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", kakaoClientId);
params.add("redirect_uri", kakaoRedirectUri);
params.add("code", code);
HttpEntity<MultiValueMap<String, String>> reqkakaoToken = new HttpEntity<>(params, header1);
ResponseEntity<KakaoToken> response1 = rt1.exchange(Define.KAKAO_REQUEST_ACCESSTOKEN_URL, HttpMethod.POST,
reqkakaoToken, KakaoToken.class);
accessToken = response1.getBody().getAccess_token();
// Access Token으로 유저 정보 받아오기
RestTemplate rt2 = new RestTemplate();
HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + response1.getBody().getAccess_token());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
HttpEntity<MultiValueMap<String, String>> reqKakaoMessage = new HttpEntity<>(headers2);
ResponseEntity<KakaoUserInfoDTO> response2 = rt2.exchange(Define.KAKAO_REQUEST_USERINFO_URL, HttpMethod.POST,
reqKakaoMessage, KakaoUserInfoDTO.class);
kakaoUserInfoDTO = response2.getBody();
// 최초 소셜 사용자인지 판별
PrincipalDTO principalDTO = userService.searchUserId(kakaoUserInfoDTO.getId());
if (principalDTO == null) {
return "user/addKakaoUserInfo";
}
session.setAttribute("principal", principalDTO);
return "redirect:/user/main";
}
/**
* 카카오 로그인시 추가 정보 로직 처리
*
* @param
* @param kakaoAddUserInfoDTO - 추가 정보: 이름, 이메일, 성별, 생일, 전화번호
* @param session
* @return
*/
@PostMapping("/addKakaoUserInfo")
public String addKakaoUserInfo(KakaoAddUserInfoDTO kakaoAddUserInfoDTO, HttpSession session) {
SignUpDTO signUpDTO = SignUpDTO.builder().username(kakaoAddUserInfoDTO.getUsername())
.userId(kakaoUserInfoDTO.getId()).userPassword(Define.SOCIAL_PASSWORD_KAKAO)
.userNickname(kakaoUserInfoDTO.getProperties().getNickname())
.userEmail(kakaoAddUserInfoDTO.getUserEmail()).userBirth(kakaoAddUserInfoDTO.getUserBirth())
.userGender(kakaoAddUserInfoDTO.getUserGender()).userTel(kakaoAddUserInfoDTO.getUserTel())
.userSocialType(Define.SOCIAL_TYPE_IS_KAKAO)
// .userOriginProfileImage(naverUserInfoDTO.getResponse().getProfile_image())
.build();
// 유효성 검사
if (!ValidationUtil.isValidateName(kakaoAddUserInfoDTO.getUsername())) {
throw new DataDeliveryException(Define.SIGNUP_NAME, HttpStatus.BAD_REQUEST);
}
if (!ValidationUtil.isValidateEmail(kakaoAddUserInfoDTO.getUserEmail())) {
throw new DataDeliveryException(Define.SIGNUP_EMAIL, HttpStatus.BAD_REQUEST);
}
if (kakaoAddUserInfoDTO.getUserBirth() == null || kakaoAddUserInfoDTO.getUserBirth().trim().isEmpty()) {
throw new DataDeliveryException(Define.SIGNUP_BIRTH, HttpStatus.BAD_REQUEST);
}
if (!ValidationUtil.isValidateTel(kakaoAddUserInfoDTO.getUserTel())) {
throw new DataDeliveryException(Define.SIGNUP_TEL, HttpStatus.BAD_REQUEST);
}
userService.createUser(signUpDTO);
PrincipalDTO principalDTO = userService.searchUserId(kakaoUserInfoDTO.getId());
session.setAttribute("principal", principalDTO);
return "redirect:/user/main";
}
}
public class Define {
// 이미지 관련
public static final String UPLOAD_FILE_DERECTORY = "C:\\Users\\user\\git\\perfect_folio\\src\\main\\resources\\static\\images/";
public static final int MAX_FILE_SIZE = 1024 * 1024 * 20; // 20MB
// REST api 요청 URL
public static final String NAVER_REQUEST_ACCESSTOKEN_URL = "https://nid.naver.com/oauth2.0/token";
public static final String NAVER_REQUEST_USERINFO_URL = "https://openapi.naver.com/v1/nid/me";
public static final String KAKAO_REQUEST_ACCESSTOKEN_URL = "https://kauth.kakao.com/oauth/token";
public static final String KAKAO_REQUEST_USERINFO_URL = "https://kapi.kakao.com/v2/user/me";
public static final String GOOGLE_REQUEST_ACCESSTOKEN_URL = "https://oauth2.googleapis.com/token";
public static final String GOOGLE_REQUEST_USERINFO_URL = "https://www.googleapis.com/userinfo/v2/me?access_token=";
// 소셜 로그인 타입
public static final String SOCIAL_TYPE_IS_LOCAL = "local";
public static final String SOCIAL_TYPE_IS_NAVER = "naver";
public static final String SOCIAL_TYPE_IS_GOOGLE = "google";
public static final String SOCIAL_TYPE_IS_KAKAO = "kakao";
public static final String SOCIAL_TYPE_IS_ENTERPRISE = "com";
// 소셜 로그인 비밀번호 할당
public static final String SOCIAL_PASSWORD_GOOGLE = "OAuth_Google";
public static final String SOCIAL_PASSWORD_NAVER = "OAuth_Naver";
public static final String SOCIAL_PASSWORD_KAKAO = "OAuth_Kakao";
}
토큰 정보를 받는 DTO입니다.
@Data
@ToString
public class KakaoToken {
public String token_type;
public String access_token;
public Integer expires_in;
public String refresh_token;
public Integer refresh_token_expires_in;
}
카카오 유저 정보를 받는 DTO입니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class KakaoUserInfoDTO {
public String id;
public String connectedApp;
public Properties properties;
@Data
public class Properties {
private String nickname;
private String profileImage;
private String thumbnailImage;
}
}
카카오에서 제공받지 못하는 정보를 추가로 받기 위한 DTO입니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class KakaoAddUserInfoDTO {
private String username;
private String userEmail;
private String userBirth;
private String userGender;
private String userTel;
}
유저 서비스 클래스
- 클라이언트는 서비스 클래스에 진입할 수 없습니다. 컨트롤러에서 작업 처리가 끝나야 합니다.
- User 모델을 컨트롤러에 반환하면 데이터를 그대로 노출하는 것이기 때문에 세션에 내려줄 최소한의 값만 DTO에 담았습니다.
만났던 사소한 오류 : 카카오, 구글은 사업자 등록증이 없는 경우 프로젝트에서 필요한 유저의 필수 정보를 모두 제공해주지 않았습니다. 추가로 받는 정보들 중 이메일이 있었습니다. 예를 들어 카카오, 구글 모두 연동한 유저의 경우, 카카오로 연동 했을 때 기입한 이메일이 구글이라면 다음 로그인시 구글로 자동 로그인되었습니다. 그래서 이메일 중복 체크를 진행하여 이미 가입 내역이 존재하는 이메일일 경우 가입을 제한하였습니다.
@Service
@RequiredArgsConstructor
public class UserService {
@Autowired
private final UserRepository userRepository;
/**
*
* userEmail로 사용자 존재 여부 조회
*
* @param = String userEmail
* @return User
*/
public PrincipalDTO searchUserEmail(String userEmail) {
User user = userRepository.findByUserEmail(userEmail);
System.out.println("유저 서비스::: 사용자???:" + user);
if (user != null) {
PrincipalDTO principalDTO = PrincipalDTO.builder().id(user.getId()).userId(user.getUserId())
.username(user.getUsername()).userSocialType(user.getSocialType()).build();
return principalDTO;
}
return null;
}
}
@Mapper
public interface UserRepository {
public int insert(User user);
public User findByUserEmail(String userEmail);
public User findByUserId(String userId);
public User findByid(int id);
public List<User> findAll();
public int insertWithdraw(User user);
public int insertWithdrawReason(@Param("userId") String userId, @Param("reason") String reason, @Param("reasonDetail") String reasonDetail);
public void delete(int id);
public void deleteOldWithdraw();
public int checkDuplicateID(String userId);
public int checkDuplicateEmail(String email);
public void changePassword(String newPassword, String userId);
public void updateUserInfo(User user);
public List<Map<String, Object>> getUserSkillList(int id);
public List<CountResultDTO> getCountResults();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper
namespace="com.tenco.perfectfolio.repository.interfaces.UserRepository">
<insert id="insert">
INSERT INTO user_tb(user_name, user_id,
user_password, user_nickname,
user_email,user_birth,user_gender,user_tel,social_type)
VALUES (
#{username}, #{userId}, #{userPassword}, #{userNickname},
#{userEmail},#{userBirth},#{userGender},#{userTel},#{socialType})
</insert>
<insert id="insertWithdraw">
INSERT INTO user_withdraw_tb(user_name, user_id,
user_password, user_nickname,
user_email,user_birth,user_gender,user_tel,social_type,created_at)
VALUES ( #{username}, #{userId}, #{userPassword}, #{userNickname},
#{userEmail},#{userBirth},#{userGender},#{userTel},#{socialType},#{createdAt})
</insert>
<insert id="insertWithdrawReason">
INSERT INTO withdraw_reason_tb(user_id, reason,
reason_detail)
VALUES (#{userId}, #{reason}, #{reasonDetail})
</insert>
<select id="checkDuplicateID">
SELECT count(user_id) FROM user_tb WHERE user_id =
#{userId}
</select>
<select id="checkDuplicateEmail">
SELECT count(user_email) FROM user_tb WHERE
user_email = #{userEmail}
</select>
<update id="changePassword">
UPDATE user_tb SET user_password = #{newPassword}
WHERE user_id = #{userId}
</update>
<select id="findByid">
SELECT * FROM user_tb WHERE id = #{id}
</select>
<select id="findByUserEmail">
SELECT * FROM user_tb WHERE user_email = #{userEmail}
</select>
<select id="findByUserId">
SELECT
u.*,
s.subscribing,
s.order_name
FROM user_tb as u
LEFT JOIN subscribing_tb as s
ON u.id = s.user_id
WHERE u.user_id = #{userId};
</select>
<delete id="delete">
DELETE FROM user_tb WHERE ID = #{id}
</delete>
<select id="findAllNotices"
resultType="com.tenco.perfectfolio.repository.model.Notice">
SELECT * FROM notice_tb
</select>
<delete id="deleteOldWithdraw">
DELETE FROM user_withdraw_tb
WHERE withdraw_at <= NOW() - INTERVAL 3 YEAR;
</delete>
<update id="updateUserInfo">
UPDATE user_tb
SET
user_name = #{username},
user_password = #{userPassword},
user_nickname = #{userNickname},
user_email = #{userEmail},
user_tel = #{userTel}
WHERE id = #{id}
</update>
<select id="getCountResults" resultType="com.tenco.perfectfolio.dto.CountResultDTO">
SELECT
1 AS id, COUNT(*) AS count
FROM
recommended_companies
UNION SELECT
2 AS id, COUNT(*) AS count
FROM
recommended_companies
WHERE
DATE(recommended_date) = CURDATE()
UNION SELECT
3 AS id, COUNT(*) AS count
FROM
crawl_notice_json
UNION SELECT
4 AS id, COUNT(*) AS count
FROM
crawl_notice_json
WHERE
DATE(created_at) = (CURDATE() - 1)
</select>
<select id="findAll">
select * from user_tb
</select>
</mapper>
CREATE TABLE user_tb(
id int auto_increment primary key,
user_name varchar(255) not null,
user_id varchar(255) not null unique,
user_password varchar(255) NOT NULL,
user_nickname varchar(20),
user_email varchar(255) NOT NULL unique ,
user_birth varchar(100) NOT NULL,
user_gender enum('남성','여성'),
user_tel varchar(13),
created_at timestamp default now() not null,
social_type varchar(30) not null
);
'Team project > [파이널] 개발자 매칭 서비스 - Perfectfolio' 카테고리의 다른 글
웹프레임 디자인 시안 (1) | 2024.10.07 |
---|---|
[11~14일차] 문의사항 게시판 CRUD, 페이징, 검색 + JS Fetch (1) | 2024.10.04 |
[10~15일차]토스페이먼츠 자동결제, 결제 취소 api (0) | 2024.09.13 |
[4~7일차] 네이버 로그인 연동 구현하기 (0) | 2024.08.23 |