본문 바로가기

Spring

[Spring] Naver OAuth2 로그인 + Spring Security + Naver 검색 API 활용

728x90
반응형
SMALL

외부 로그인 (OAuth2)를 프로젝트에 활용해보기 위해서 NAVER OAuth2 로그인을 구현해 보고자 하였다.

프로젝트 내 네이버 로그인 동작

Naver OAuth2 로그인 설정하기

 

애플리케이션 - NAVER Developers

 

developers.naver.com

 

  • 위 주소로 이동 => 로그인이 되어있다면 밑의 화면이 뜰 것임

사용 API에서 네이버 로그인을 선택
위 로그인을 선택하면 제공 정보 선택이 보여짐

  • 원하는 정보를 선택하면 된다. (본인은 회원이름 + 연락처 이메일 주소 만 선택)

환경 추가에서 pc웹을 선택

  • 환경 추가를 통해 PC웹을 선택
  • 서비스URL에 http://localhost:8080 를 추가
  • Callback URL 에는 http://localhost:8080/login/oauth2/code/naver 를 추가
    • 해당 콜백 URL 에는 어떤 url이 들어가도 괜찮다. 위 URL은 본인이 프로젝트를 하면서 구상한 방식

Client ID, Client Secret 확인

위 세팅이 끝났다면 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'

 

 

application.yml 파일 설정

 

네이버 로그인 개발가이드 - LOGIN

네이버 로그인 개발가이드 1. 개요 4,200만 네이버 회원을 여러분의 사용자로! 네이버 회원이라면, 여러분의 사이트를 간편하게 이용할 수 있습니다. 전 국민 모두가 가지고 있는 네이버 아이디

developers.naver.com

 


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 추가

 

검색 > 블로그 - Search API

검색 > 블로그 블로그 검색 개요 개요 검색 API와 블로그 검색 개요 검색 API는 네이버 검색 결과를 뉴스, 백과사전, 블로그, 쇼핑, 영화, 웹 문서, 전문정보, 지식iN, 책, 카페글 등 분야별로 볼 수

developers.naver.com

  • 해당 프로젝트에서는 뉴스, 책 정보를 불러왔음 따라서 예제는 뉴스와 책에 관한 내용을 정리하도록 하겠음

뉴스 API 요청 파라미터

필수 여부와 sort부분 주의 깊게 확인 하였음
API를 요청해야 할때 Client-ID 와 Client-Secret 필요

책 API 요청 파라미터

d_title을 통해 책 제목으로 책의 정보들을 받아올 수 있다는 것을 주의 깊게 확인
뉴스와 마찬가지로 Client-ID 와 Client-Secret 필요

 

도메인

// 뉴스 도메인
@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