사전 정보 1 - OAuth (Open Authorization)이란?
OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹 사이트 상에 존재하는 개인정보에 웹 사이트나 애플리케이션이 접근할 수 있도록 권한을 부여할 수 있는 공통적인 수단으로서 사용되는 접근 위임을 위한 개방형 표준입니다.
애플리케이션을 이용할 때 아이디, 비밀번호로 가입하지 않고 신뢰할 수 있는 외부 애플리케이션(Google, Naver, Kakao, Github 등...)에 Open API에 아이디, 비밀번호를 입력하여 해당 애플리케이션이 인증 과정을 처리해주는 방식입니다.
REST API (Representational State Transfer API) 이기 때문에 양식은 비슷합니다.
사전 정보 2 - JWT란?
Json Web Token을 줄인 말입니다. 인증에 필요한 정보들을 Json 객체에 담은 후 비밀키로 서명한 토큰입니다. 이는 인터넷 표준 인증 방식이며, 공식적으로 인증(Authentication)과 권한허가(Authorization) 방식으로 사용됩니다.
0. naver developers
https://developers.naver.com/main/
NAVER Developers
네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음
developers.naver.com
1. 애플리케이션 > 애플리케이션 등록
약관에 동의하고, 휴대전화 본인인증을 거친 후 애플리케이션 등록을 진행합니다.
2. 애플리케이션 등록 확인 및 클라이언트 아이디와 시크릿 확인하기
Client ID, Client Secret로 인가 요청, 액세스 토큰 발급, 사용자 정보 조회 등을 수행할 수 있습니다.
! 해당 정보는 내 애플리케이션을 구분해주는 중요한 정보이므로 반드시 안전하게 보관해야 합니다. 네이버 로그인 연동 과정에서 활용되는 정보이기 때문에 잘못된 Client ID, Client Secret를 사용하면 당연히 연동에 실패합니다. 한 번 발급된 Client ID는 변경할 수 없습니다. 그에 반해 Client Secret 정보는 개발자 센터를 통해 재발급 받을 수 있습니다. Client Secret는 유출이 의심될 경우 재발급을 받아서 도용을 방지할 수 있습니다.
3. 로그인 버튼 가이드
https://developers.naver.com/docs/login/bi/bi.md
로그인 버튼 사용 가이드 - LOGIN
네이버 로그인은 애플리케이션에 사용할 수 있는 네이버 로그인 버튼 기본 이미지를 제공합니다. 애플리케이션의 상황에 맞게 버튼 이미지의 디자인을 변경할 수 있지만 네이버 고유의 아이덴
developers.naver.com
4. 개발 가이드를 참고하여 네이버 로그인 연동 URL을 생성합니다.
5. 4번에서 URL 요청시 Callback 정보입니다.
6. 접근 토큰 발급 요청시 필요한 정보입니다.
https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=jyvqXeaVOVmV&client_secret=527300A0_COq1_XV33cf&code=EIc5bFrl4RibFls1&state=9kgsGTfH4j7IyAkg
그에 대한 응답 정보입니다.
7. 응답받은 액세스 토큰으로 프로필 API를 호출합니다.
curl -XGET "https://openapi.naver.com/v1/nid/me" \
-H "Authorization: Bearer AAAAPIuf0L+qfDkMABQ3IJ8heq2mlw71DojBj3oc2Z6OxMQESVSrtR0dbvsiQbPbP1/cxva23n7mQShtfK4pchdk/rc="
성공한다면 사전에 선택했던 정보가 출력됩니다.
코드
패키지 구조
동의 화면을 출력하기 위한 링크입니다.
ClientId등의 값은 yml에 선언했습니다.
<a href="https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${naverClientId}&state=${state}&redirect_uri=${naverRedirectUri}">
<img alt="네이버 로그인 버튼" class="social--btn" src="/images/btnG_완성형.png">
</a>
yml 파일
naver:
client-id: 61124mn0Y_vuN0I9gWUm # 발급 받은 Client ID
client-secret: r8vgxA9kTU # 발급 받은 Client Secret
redirect-uri: http://localhost:8080/user/naver #
client-authentication-method: client_secret_post
# scope:
# - name
# - email
# - profile_image
client-name: Naver
유저 컨트롤러
- 로그아웃과 연동해제는 세션을 삭제하고, 연동해제 같은 경우는 DB 조작 메소드만 추가했습니다.
- 회원가입, 로그인 페이지 접속시 소셜 로그인 url에 필요한 정보를 Model로 내려줬습니다.
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
@Autowired
private HttpSession session;
@Value("${naver.client-id}")
private String naverClientId;
@Value("${naver.client-secret}")
private String naverClientSecret;
@Value("${naver.redirect-uri}")
private String naverRedirectUri;
private String state;
/**
* 네이버 소셜 로그인시 필요한 state CSRF 공격을 방지하기 위한 랜덤 문자열을 생성합니다.
*
* @return
*/
public String generateState() {
SecureRandom random = new SecureRandom();
return new BigInteger(130, random).toString(32);
}
/**
* 로그인 페이지 http://localhost:8080/user/sign-in
*
* @return signIn.jsp
*/
@GetMapping("/sign-in")
public String signInPage(Model model) {
// 상태 토큰으로 사용할 랜덤 문자열 생성
String state = generateState();
model.addAttribute("naverClientId", naverClientId);
model.addAttribute("naverRedirectUri", naverRedirectUri);
model.addAttribute("state", state);
model.addAttribute("kakaoRestApiKey", kakaoClientId);
model.addAttribute("kakaoRedirectUri", kakaoRedirectUri);
model.addAttribute("googleClientId", googleClientId);
model.addAttribute("googleRedirectUri", googleRedirectUri);
return "user/signIn";
}
/**
* 네이버 소셜 로그인 로직 처리 요청
*
* @param code
* @param state
* @return
*/
@GetMapping("/naver")
public String getMethodName(@RequestParam(name = "code", required = false) String code,
@RequestParam(name = "state", required = false) String state, Model model) {
// Access Token 발급 요청
RestTemplate naverRt1 = new RestTemplate();
HttpHeaders header1 = new HttpHeaders();
MultiValueMap<String, String> params1 = new LinkedMultiValueMap<>();
params1.add("grant_type", naverGrantTypeIssue);
params1.add("client_id", naverClientId);
params1.add("client_secret", naverClientSecret);
params1.add("code", code);
params1.add("state", state);
HttpEntity<MultiValueMap<String, String>> reqNaverToken = new HttpEntity<>(params1, header1);
ResponseEntity<NaverToken> response = naverRt1.exchange(Define.NAVER_REQUEST_ACCESSTOKEN_URL, HttpMethod.POST,
reqNaverToken, NaverToken.class);
accessToken = response.getBody().getAccess_token();
// Access Token으로 유저 정보 받아오기
RestTemplate rt2 = new RestTemplate();
HttpHeaders header2 = new HttpHeaders();
header2.add("Authorization", "Bearer " + response.getBody().getAccess_token());
HttpEntity<MultiValueMap<String, String>> naverProfileRequest = new HttpEntity<>(header2);
ResponseEntity<NaverUserInfoDTO> response2 = rt2.exchange(Define.NAVER_REQUEST_USERINFO_URL, HttpMethod.POST,
naverProfileRequest, NaverUserInfoDTO.class);
NaverUserInfoDTO naverUserInfoDTO = response2.getBody();
naverUserInfoDTO.genderType(naverUserInfoDTO.getResponse().getGender());
SignUpDTO signUpDTO = SignUpDTO.builder().username(naverUserInfoDTO.getResponse().getName())
.userId(naverUserInfoDTO.getResponse().getId()).userPassword(Define.SOCIAL_PASSWORD_NAVER)
.userNickname(naverUserInfoDTO.getResponse().getNickname())
.userEmail(naverUserInfoDTO.getResponse().getEmail())
.userBirth(naverUserInfoDTO.getResponse().getBirthyear() + "-"
+ naverUserInfoDTO.getResponse().getBirthday())
.userGender(naverUserInfoDTO.getResponse().getGender())
.userTel(naverUserInfoDTO.getResponse().getMobile()).userSocialType(Define.SOCIAL_TYPE_IS_NAVER)
// .userOriginProfileImage(naverUserInfoDTO.getResponse().getProfile_image())
.build();
// 최초 소셜 사용자인지 판별
PrincipalDTO principalDTO = userService.searchUserEmail(signUpDTO.getUserEmail());
if (principalDTO == null) {
userService.createUser(signUpDTO);
}
session.setAttribute("principal", principalDTO);
return "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_PASSWORD_GOOGLE= "OAuth_Google";
public static final String SOCIAL_PASSWORD_NAVER = "OAuth_Naver";
public static final String SOCIAL_PASSWORD_KAKAO = "OAuth_Kakao";
}
DTO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class NaverToken {
private String access_token;
private String refresh_token;
private String token_type;
private Integer expires_in;
private String error;
private String error_description;
private String result;
}
네이버 성별 반환값은 M, F, U 라서 가공하는 메소드를 넣었습니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class NaverUserInfoDTO {
public String resultcode;
public String message;
public response response;
@Data
public class response {
String id;
String nickname;
String name;
String email;
String gender;
String age;
String birthday;
String profile_image;
String birthyear;
String mobile;
}
public String genderType(String gender) {
if (gender.equals("M")) {
response.setGender("남성");
// naverUserInfoDTO.getResponse().setGender("남성");
} else if (gender.equals("F")) {
response.setGender("여성");
// naverUserInfoDTO.getResponse().setGender("여성");
}else {
response.setGender("");
}
return gender;
}
}
유저 서비스 클래스
- 클라이언트는 서비스 클래스에 진입할 수 없습니다. 컨트롤러에서 작업 처리가 끝나야 합니다.
- User 모델을 컨트롤러에 반환하면 데이터를 그대로 노출하는 것이기 때문에 세션에 내려줄 최소한의 값만 DTO에 담았습니다.
- MySQL DB에 이메일 컬럼은 Unique key 값입니다. 네이버에서 이용자 이메일을 제공해주기 때문에 이메일로 기존 유저인지 존재 여부를 조회합니다.
/**
*
* userEmail로 사용자 존재 여부 조회
*
* @param String userEmail
* @return User
*/
public PrincipalDTO searchUserEmail(String userEmail) {
User user = userRepository.findByUserEmail(userEmail);
if (user != null) {
PrincipalDTO principalDTO = PrincipalDTO.builder().id(user.getId()).username(user.getUsername())
.userSocialType(user.getSocialType()).build();
return principalDTO;
}
return null;
}
@Mapper
public interface UserRepository {
public int insert(User user); // 유저 정보를 user_tb에 입력
public User findByUserEmail(String userEmail); // userEmail 기존 유저인지 판단
public User findByUserId(String userId); // userId로 기존 유저인지 판단
public User findByid(int id); // PK 값으로 유저 정보 찾기
public int insertWithdraw(User user); // 탈퇴시 탈퇴 테이블에 입력
public void delete(int id); // 탈퇴시 기존 테이블에서 삭제
}
<?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>
<!-- user_tb insert 이미지url 컬럼 포함 -->
<!-- <insert id="insert">
INSERT INTO user_tb(user_name, user_id, user_password, user_nickname,
user_email,user_birth,user_gender,user_tel,user_origin_profile_image,user_upload_profile_image,social_type)
VALUES ( #{username}, #{userId}, #{userPassword}, #{userNickname},
#{userEmail},#{userBirth},#{userGender},#{userTel},#{userOriginProfileImage},#{userUploadProfileImage},#{userSocialType})
</insert>-->
<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 * FROM user_tb WHERE user_id = #{userId}
</select>
<delete id="delete">
DELETE FROM user_tb WHERE ID = #{id}
</delete>
</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일차] 카카오 로그인 연동 구현하기 (5) | 2024.08.22 |