728x90
반응형
SMALL
외부 로그인 (OAuth2)를 프로젝트에 활용해보기 위해서 NAVER OAuth2 로그인을 구현해 보고자 하였다.
Naver OAuth2 로그인 설정하기
- 위 주소로 이동 => 로그인이 되어있다면 밑의 화면이 뜰 것임
- 원하는 정보를 선택하면 된다. (본인은 회원이름 + 연락처 이메일 주소 만 선택)
- 환경 추가를 통해 PC웹을 선택
- 서비스URL에 http://localhost:8080 를 추가
- Callback URL 에는 http://localhost:8080/login/oauth2/code/naver 를 추가
- 해당 콜백 URL 에는 어떤 url이 들어가도 괜찮다. 위 URL은 본인이 프로젝트를 하면서 구상한 방식
위 세팅이 끝났다면 Spring Boot에 설정해주어야한다.
Naver OAuth2 To Spring Boot Setting
- Java: 17
- Spring Boot: 3.2.0
- Gradle => oauth2 client + security 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
- 해당 정보들의 경우 네이버 로그인 개발가이드를 보고 필요한 부분들을 분석해보았고 실질적으로 공개된 많은 예제 setting이 있어서 참고하였다.
- https://developers.naver.com/docs/login/devguide/devguide.md#%EB%84%A4%EC%9D%B4%EB%B2%84%20%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EA%B0%80%EC%9D%B4%EB%93%9C
OAuth2 To Spring Security
Filter
// 해당 코드는 SecurityFilterChain을 Bean에 등록하면서 http에 리턴될때, OAuth2 로그인을 어떻게
// 처리 할 것인지 보는 것. 추후에 Security EndPoint에 대한 내용 정리를 올려보도록 하겠다
.oauth2Login(httpSecurityOAuth2LoginConfigurer -> httpSecurityOAuth2LoginConfigurer
.loginPage("/user/login") // 로그인 페이지는 기존과 동일한 url 로 지정
// code 를 받아오는 것이 아니라, AccessToken 과 사용자 profile 정보를 받아오게 된다.
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
// userService(OAuth2UserService<OAuth2UserRequest, OAuth2User>)
// 이 설정을 통해 인증서버의 UserInfo Endpoint 후처리 진행
.userService(principalOauth2UserService)
)
)
principalOauth2UserService?
- DefaultOAuth2UserService 클래스를 상속받아 서버에서 네이버 인증 후 의 후처리를 해줄 서비스를 구현
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
// 여기서 인증후 '후처리' 를 해주어야 한다
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Value("${app.oauth2.password}")
private String oauth2Password;
// 인증직후 loadUser() 는 provider 로부터 받은 userRequest 데이터에 대한 후처리 진행
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest); // 사용자 프로필 정보 가져오기
// 후처리: 회원 가입 진행
String provider = userRequest.getClientRegistration().getRegistrationId(); // "google", "facebook"...
OAuth2UserInfo oAuth2UserInfo = switch (provider.toLowerCase()){
case "google" -> new GoogleUserInfo(oAuth2User.getAttributes());
case "naver" -> new NaverUserInfo(oAuth2User.getAttributes());
default -> null;
};
String providerId = oAuth2UserInfo.getProviderId();
// username은 중복되지 않도록 만들어야 한다
String username = provider + "_" + providerId; // "ex) google_xxxxxxxx"
String password = passwordEncoder.encode(oauth2Password);
String email = oAuth2UserInfo.getEmail();
String name = oAuth2UserInfo.getName();
// 회원 가입 진행하기 전에
// 이미 가입한 회원인지, 혹은 비가입자인지 체크하여야 한다
User newUser = User.builder()
.username(username)
.name(name)
.email(email)
.password(password)
.provider(provider)
.providerId(providerId)
.build();
User user = userService.findByUsername(username);
if (user == null) { // 비가입자인 경우에만 회원 가입 진행
user = newUser;
int cnt = userService.register(user); // 회원 가입!
if (cnt > 0) {
System.out.println("[OAuth2 인증 회원가입 성공]");
user = userService.findByUsername(username);
} else {
System.out.println("[OAuth2 인증 회원가입 실패]");
}
} else {
System.out.println("[OAuth2 인증. 이미 가입된 회원입니다]");
}
PrincipalDetails principalDetails = new PrincipalDetails(user, oAuth2User.getAttributes());
principalDetails.setUserService(userService); // 잊지말자!
return principalDetails; // 이 리턴값이 Authenticatoin 안에 들어간다!
}
}
NaverUserInfo
public class NaverUserInfo implements OAuth2UserInfo {
private Map<String, Object> attributes;
public NaverUserInfo(Map<String, Object> attributes){
this.attributes = (Map)attributes.get("response");
}
@Override
public String getProvider() {
return "naver";
}
@Override
public String getProviderId() {
return (String)attributes.get("id");
}
@Override
public String getEmail() {
return (String)attributes.get("email");
}
@Override
public String getName() {
return (String)attributes.get("name");
}
}
User DTO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private Long id;
private String username; // user 아이디
@JsonIgnore
private String password;
@ToString.Exclude // toString() 결과에서 뺌.
@JsonIgnore
private String re_password; // 비밀번호 확인 입력
private String name; // user nickname
private String email;
@JsonIgnore
private LocalDateTime regDate;
// OAuth2
private String provider; // 어떤 OAuth2 제공자? Kakao, Naver, Google....
private String providerId; // provider 내에서의 고유 id 값
}
User에 관련된 UserController, UserService, UserRepository 는 생략!
- 하지만 logout은
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response,
SecurityContextHolder.getContext().getAuthentication());
return "redirect:/";
}
이렇게 구현하였음
Naver 검색 API 사용하기
위와 같이 OAuth2 로그인 설정이 되어있으면 검색만 추가하면 된다.
- 검색 API에는 다양한 url로 다양한 정보를 요청할 수 있다. (웹문서/블로그/뉴스/책/영화/카페글/지식iN/쇼핑/이미지/백과사전/전문자료 분야)
- 처리 한도는 하루에 25,000건
- https://developers.naver.com/docs/serviceapi/search/blog/blog.md#%EB%B8%94%EB%A1%9C%EA%B7%B8
- 해당 프로젝트에서는 뉴스, 책 정보를 불러왔음 따라서 예제는 뉴스와 책에 관한 내용을 정리하도록 하겠음
뉴스 API 요청 파라미터
책 API 요청 파라미터
도메인
// 뉴스 도메인
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class News {
private Long id;
private String keyword;
private String title;
private String originallink;
private String link;
private String description;
private LocalDateTime pubDate;
}
// 책 도메인
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Book {
private Long id;
private Long userId;
private String title;
private String author;
private String link;
private String image;
private String description;
private String publisher;
private String isbn;
private int discount;
private String pubdate;
}
Mapper && Repository
// 해당 Repository에는 데이터 베이스에서 유튜브, 뉴스, 책 을 저장하고 가져오는 동작을 지원
public interface NewsRepository {
int save(News news);
int saveYoutue(YoutubeDTO youtube);
int deleteYoutube(String keyword);
int saveBooks(Book book);
int delete(String keyword);
List<News> list(String keyword);
List<YoutubeDTO> listYoutube(String keyword);
int deleteBooks(Book book);
List<Book> likeBooks(Long userId);
}
<?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.lec.spring.repository.naverapi.NewsRepository">
<insert id="save" flushCache="true" parameterType="com.lec.spring.domain.naverapi.News"
keyProperty="id" useGeneratedKeys="true" keyColumn="id">
INSERT INTO news (keyword, title, originallink, link, description, pubDate)
VALUES(#{keyword}, #{title}, #{originallink}, #{link},#{description}, #{pubDate})
</insert>
<insert id="saveYoutue" flushCache="true" parameterType="com.lec.spring.domain.naverapi.YoutubeDTO"
keyProperty="id" useGeneratedKeys="true" keyColumn="id">
INSERT INTO youtube (keyword, title, videoId)
VALUES (#{keyword},#{title},#{videoId})
</insert>
<insert id="saveBooks" flushCache="true" parameterType="com.lec.spring.domain.naverapi.Book"
keyProperty="id" useGeneratedKeys="true" keyColumn="id">
INSERT INTO books (user_id, title, link, image, author, discount, publisher, pubdate, isbn, description)
VALUES (#{userId}, #{title}, #{link}, #{image}, #{author}, #{discount}, #{publisher}, #{pubdate}, #{isbn}, #{description})
</insert>
<delete id="delete" flushCache="true">
DELETE FROM news WHERE keyword = #{keyword}
</delete>
<delete id="deleteBooks" flushCache="true">
DELETE FROM books WHERE user_id = #{userId} AND isbn = #{isbn} AND title = #{title}
</delete>
<delete id="deleteYoutube">
DELETE FROM youtube WHERE keyword = #{keyword}
</delete>
<select id="list" resultType="com.lec.spring.domain.naverapi.News">
SELECT
id,
keyword,
title,
originallink,
link,
description,
pubDate
FROM
news
WHERE keyword = #{keyword}
</select>
<select id="listYoutube" resultType="com.lec.spring.domain.naverapi.YoutubeDTO">
SELECT
id,
keyword,
title,
videoId
FROM
youtube
WHERE keyword = #{keyword}
LIMIT 4
</select>
<select id="likeBooks" resultType="com.lec.spring.domain.naverapi.Book">
SELECT *
FROM books
WHERE user_id = #{userId}
</select>
</mapper>
Service
@Service
public class NaverApiService {
@Value("${spring.security.oauth2.client.registration.naver.client-id}")
private String naver_client_id;
@Value("${spring.security.oauth2.client.registration.naver.client-secret}")
private String naver_secret;
@Value("${youtube.apikey}")
private String youtuekey;
private NewsRepository newsRepository;
public NaverApiService(SqlSession sqlSession){
newsRepository = sqlSession.getMapper(NewsRepository.class);
}
public void navernews(){
newsRepository.delete("개발자채용");
DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
URI uri = UriComponentsBuilder.fromUriString("https://openapi.naver.com")
.path("/v1/search/news.json")
.queryParam("query", "개발자채용")
.queryParam("display","100")
.queryParam("start","1")
.queryParam("sort","sim")
.encode()
.build()
.toUri();
RestTemplate restTemplate =new RestTemplate();
RequestEntity<Void> requestEntity=
RequestEntity.get(uri)
.header("X-Naver-Client-ID",naver_client_id)
.header("X-Naver-Client-Secret",naver_secret)
.build();
//헤더값을 채워서 넣기 위해서 exchange이용
ResponseEntity<String> res = restTemplate.exchange(requestEntity,String.class);
String response = res.getBody();
JSONObject jsonObject = new JSONObject(response);
JSONArray items = jsonObject.getJSONArray("items");
List<News> newsList = new ArrayList<>();
for (int i=0; i<items.length(); i++) {
JSONObject itemJson = (JSONObject) items.get(i);
News news = new News();
news.setKeyword("개발자채용");
news.setTitle(itemJson.getString("title"));
news.setOriginallink(itemJson.getString("originallink"));
news.setLink(itemJson.getString("link"));
news.setDescription(itemJson.getString("description"));
ZonedDateTime zonedDateTime = ZonedDateTime.parse(itemJson.getString("pubDate"), inputFormatter);
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
news.setPubDate(localDateTime);
newsList.add(news);
}
for (News news : newsList) {
newsRepository.save(news);
}
}
public List<Book> getbooks(String keyword) {
URI uri = UriComponentsBuilder.fromUriString("https://openapi.naver.com")
.path("/v1/search/book_adv.json")
.queryParam("d_titl", keyword.trim())
.queryParam("display", "10")
.queryParam("start", "1")
.queryParam("sort", "sim")
.encode()
.build()
.toUri();
RestTemplate restTemplate = new RestTemplate();
RequestEntity<Void> requestEntity =
RequestEntity.get(uri)
.header("X-Naver-Client-ID", naver_client_id)
.header("X-Naver-Client-Secret", naver_secret)
.build();
//헤더값을 채워서 넣기 위해서 exchange이용
ResponseEntity<String> res = restTemplate.exchange(requestEntity, String.class);
String response = res.getBody();
JSONObject jsonObject = new JSONObject(response);
JSONArray items = jsonObject.getJSONArray("items");
List<Book> BookList = new ArrayList<>();
for (int i = 0; i < items.length(); i++) {
JSONObject itemJson = (JSONObject) items.get(i);
Book book = new Book();
book.setTitle(itemJson.getString("title"));
book.setAuthor(itemJson.getString("author"));
book.setLink(itemJson.getString("link"));
book.setImage(itemJson.getString("image"));
book.setDescription(cutDesc(itemJson.getString("description")));
book.setPublisher(itemJson.getString("publisher"));
book.setIsbn(itemJson.getString("isbn"));
book.setDiscount(itemJson.getInt("discount"));
book.setPubdate(itemJson.getString("pubdate"));
// Add constructed Book object to the list
BookList.add(book);
}
return BookList;
}
}
- 해당 코드는 정해진 키워드에 대해서 뉴스를 불러오는 와서 데이터베이스에 저장 시키는 동작.
- 함수(navernews)가 시작 될때, 가장 먼저 키워드를 통한 delete를 시작
// parameter 설정해서 uri 생성
URI uri = UriComponentsBuilder.fromUriString("https://openapi.naver.com")
.path("/v1/search/news.json")
.queryParam("query", "개발자채용")
.queryParam("display","100")
.queryParam("start","1")
.queryParam("sort","sim")
.encode()
.build()
.toUri();
// Header에 필요한 값들과, 요청 보낼 uri를 사용
RequestEntity<Void> requestEntity=
RequestEntity.get(uri)
.header("X-Naver-Client-ID",naver_client_id)
.header("X-Naver-Client-Secret",naver_secret)
.build();
//헤더값을 채워서 넣기 위해서 exchange이용
ResponseEntity<String> res = restTemplate.exchange(requestEntity,String.class);
- 이후에 불러온 데이터를 (json)을 파싱은 json 라이브러리를 통해 해결 할 수 있었다.
implementation group: 'org.json', name: 'json', version: '20230227'
- build.gradle에 추가
해당 방법으로 프로젝트에서 뉴스 및 책 API 를 사용하여 데이터베이스에 CRUD 방식을 완성 할 수 있었다.
728x90
반응형
LIST
'Spring' 카테고리의 다른 글
[Spring] 화상 채팅 구현하기 (ZEGOCLOUD API 활용 O, WebRTC X, Feat.Thymeleaf) (0) | 2024.01.25 |
---|---|
[Spring] 스프링 스케쥴 작업 적용 방법 (Feat. @EnableScheduling, @Scheduled) (0) | 2024.01.24 |
[Spring] OpenAI API를 활용하여 ChatBot만들기 (0) | 2024.01.23 |
6장 스프링 DB 접근 기술 (1) | 2023.10.08 |
5장 회원관리예제 - 웹 MVC 개발 (0) | 2023.08.29 |