반응형
Oauth2.0

사용자가 애플리케이션 또는 웹 사이트에 로그인할 때 사용되는 프로토콜 중 하나. 이 프로토콜은 보안된 방식으로 사용자의 정보를 제공하고, 다른 웹 사이트나 애플리케이션에서 해당 정보를 사용할 수 있도록 한다.

- 자주 사용하지 않는 웹사이트에 개인정보를 입력해서 회원가입을 해야 하나? 할 때 간단하게 소셜 로그인을 사용해서 이용할 수 있다.

 

네이버소셜로그인을 위한 준비가 안되었다면 아래 링크를 통해 설정한 뒤 본 게시글을 따라 해야 한다.

네이버 소셜로그인을 위한 설정

 

네이버 소셜로그인을 위한 설정(Spring + Oauth2.0)

1. 네이버 앱 등록 https://developers.naver.com/apps/#/register 애플리케이션 - NAVER Developers developers.naver.com 위 링크에 들어가 준다. 위와 같은 화면이 뜰 것이다. 2. 세부 설정 애플리케이션 이름을 작성하

ngwdeveloper.tistory.com

 

 

1. Dto 설정

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class OauthUserDto {
    private String id;
    private String nickname;
    private String email;

}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class OauthTokenDto {
    private String accessToken;
}

OauthTokenDto - 액세스 토큰을 저장할 Dto

OauthUserDto - 액세스 토큰을 이용해서 사용자의 정보를 저장할 Dto

 

 

 

 

2. 엔티티 설정

    public User(OauthUserDto userDto, String password, UserRoleEnum role){
        this.username = userDto.getEmail();
        this.password = password;
        this.nickname = userDto.getNickname();
        this.introduction = "-";
        this.role = role;
    }

   	public User naverIdUpdate(String naverId){
        this.naverId = naverId;
        return this;
    }

이 부분은 본인의 User객체의 저장하는 목록에 따라 다를 수 있다.

나는 유저이름, 닉네임을 토큰을 이용해서 가져왔다. 또한 password, introduction, role만 설정해 주었다.

 

naverIdUpdate 메서드는 내가 설정해 둔 칼럼에 값을 추가할 수 있도록 만들어뒀다.

 

 

 

 

 

3. 레포지토리 설정

Optional<User> findByNaverId(String naverId);

중복가입을 체크하기 위해 UserRepository에 naverId로 유저를 검색할 수 있도록 Optional형식으로 코드를 추가해 줬다.

 

 

 

 

4. 통합 서비스 설정(카카오, 네이버, 구글)

public interface OauthService {

    OauthTokenDto socialLogin(String code) throws JsonProcessingException, UnsupportedEncodingException;


    String getToken(String code) throws JsonProcessingException, UnsupportedEncodingException;


    OauthUserDto getUserInfo(String accessToken) throws JsonProcessingException;

    User registerUserIfNeeded(OauthUserDto oauthTokenDto);

}

interface형식으로 만들어뒀다. 소셜 로그인(카카오, 네이버, 구글)을 할 때 각각 오버라이딩 해서 사용할 것이기 때문에 추가해 줬다.

 

 

 

5. 구글 서비스 설정

나는 application.properties를 사용하기 때문에 내부에 클라이언트 id, 클라이언트 비밀번호, 리다이렉트 url을 미리 넣어줬다.

@Service
@RequiredArgsConstructor
public class NaverService implements OauthService{
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;

    @Value("${naver.client.id}")
    private String CLIENT_ID;

    @Value("${naver.client.secret}")
    private String CLIENT_SECRET;

    @Value("${naver.redirect.url}")
    private String REDIRECT_URL;

    @Override
    public OauthTokenDto socialLogin(String code) throws JsonProcessingException, UnsupportedEncodingException {
        // 1. "인가 코드"로 "액세스 토큰" 요청
        String accessToken = getToken(code);

        // 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "네이버 사용자 정보" 가져오기
        OauthUserDto oauthUserDto = getUserInfo(accessToken);

        // 3. 필요시에 회원가입
        User naverUser = registerUserIfNeeded(oauthUserDto);

        // 4. JWT 토큰 생성
        String createToken = jwtUtil.createToken(naverUser.getUsername(), naverUser.getRole());

        return new OauthTokenDto(createToken);
    }

    @Override
    public String getToken(String code) throws JsonProcessingException, UnsupportedEncodingException {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("https://nid.naver.com")
                .path("/oauth2.0/token")
                .queryParam("grant_type", "authorization_code")
                .queryParam("client_id", CLIENT_ID)
                .queryParam("client_secret", CLIENT_SECRET)
                .queryParam("code", code)
                .queryParam("state", URLEncoder.encode("1016", "UTF-8")) // state: 임의 값 1016으로 설정
                .encode()
                .build()
                .toUri();

        StringBuilder responseBuilder = new StringBuilder();

        try {
            URL url = uri.toURL();
            HttpURLConnection con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod("GET");
            int responseCode = con.getResponseCode();
            BufferedReader br;

            if (responseCode == 200) { // 정상 호출
                br = new BufferedReader(new InputStreamReader(con.getInputStream()));
            } else {  // 에러 발생
                br = new BufferedReader(new InputStreamReader(con.getErrorStream()));
            }

            String inputLine;
            while ((inputLine = br.readLine()) != null) {
                responseBuilder.append(inputLine);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        String response = responseBuilder.toString();

        // HTTP 응답 (JSON) -> 액세스 토큰 파싱
        JsonNode jsonNode = new ObjectMapper().readTree(response);
        return jsonNode.get("access_token").asText();
    }


    @Override
    public OauthUserDto getUserInfo(String accessToken) throws JsonProcessingException {
        String header = "Bearer " + accessToken; // Bearer 다음에 공백 추가

        String apiURL = "https://openapi.naver.com/v1/nid/me";

        Map<String, String> requestHeaders = new HashMap<>();
        requestHeaders.put("Authorization", header);
        String responseBody = getApiRequest(apiURL, requestHeaders);

        JsonNode jsonNode = new ObjectMapper().readTree(responseBody);
        String id = jsonNode.get("response").get("id").asText();
        String nickname = jsonNode.get("response").get("nickname").asText();
        String email = jsonNode.get("response").get("email").asText();

        return new OauthUserDto(id, nickname, email);
    }

    private static String getApiRequest(String apiUrl, Map<String, String> requestHeaders) {
        // api 연결 확인
        HttpURLConnection connect = connecting(apiUrl);
        try {
            // get 메소드로 연결
            connect.setRequestMethod("GET");
            for (Map.Entry<String, String> header : requestHeaders.entrySet()) {
                connect.setRequestProperty(header.getKey(), header.getValue());
            }

            int responseCode = connect.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
                return readBody(connect.getInputStream());
            } else { // 에러 발생
                return readBody(connect.getErrorStream());
            }
        } catch (IOException e) {
            throw new RuntimeException("API 요청과 응답 실패", e);
        } finally {
            // 연결 해제
            connect.disconnect();
        }
    }

    private static HttpURLConnection connecting(String apiUrl) {
        try {
            URL url = new URL(apiUrl);
            return (HttpURLConnection) url.openConnection();
        } catch (MalformedURLException e) {
            throw new RuntimeException("API URL이 잘못되었습니다. : " + apiUrl, e);
        } catch (IOException e) {
            throw new RuntimeException("연결이 실패했습니다. : " + apiUrl, e);
        }
    }

    private static String readBody(InputStream body) {
        InputStreamReader streamReader = new InputStreamReader(body);

        try (BufferedReader lineReader = new BufferedReader(streamReader)) {
            StringBuilder responseBody = new StringBuilder();

            String line;
            while ((line = lineReader.readLine()) != null) {
                responseBody.append(line);
            }

            return responseBody.toString();
        } catch (IOException e) {
            throw new RuntimeException("API 응답을 읽는데 실패했습니다.", e);
        }
    }

    @Override
    public User registerUserIfNeeded(OauthUserDto apiUserInfoDto) {
        // naverId 중복 확인
        String naverId = apiUserInfoDto.getId();
        User naverUser = userRepository.findByNaverId(naverId).orElse(null);

        if (naverUser == null) {
            // 네이버 사용자 email 동일한 email 가진 회원이 있는지 확인
            String naverEmail = apiUserInfoDto.getEmail();
            User sameEmailUser = userRepository.findByUsername(naverEmail).orElse(null);
            if (sameEmailUser != null) {
                naverUser = sameEmailUser;
                // 기존 회원정보에 네이버 Id 추가
                naverUser = naverUser.naverIdUpdate(naverId);
            } else {
                // 신규 회원가입
                // password: random UUID
                String password = UUID.randomUUID().toString();
                String encodedPassword = passwordEncoder.encode(password);

                String email = apiUserInfoDto.getEmail();
                apiUserInfoDto.setNickname(email.substring(0,email.indexOf('@')));

                naverUser = new User(apiUserInfoDto, encodedPassword, UserRoleEnum.USER);
                naverUser.naverIdUpdate(naverId);
            }

            userRepository.save(naverUser);
        }

        return naverUser;
    }

socialLogin - 소셜 로그인 메서드. 인가 코드를 넘겨주고 JWT 토큰을 반환받는다.

getToken - 인가 코드를 사용한 액세스 토큰 요청 메서드. 전달받은 인가 코드를 넘겨주고 액세스 토큰을 반환받는다.

getUserInfo - 인가 토큰을 통해 사용자 정보를 가져오는 메서드. 액세스 토큰을 넘겨주고 회원정보를 반환받는다.

getApiRequest - GET요청을 보내 응답을 받아온다. 요청에 필요한 헤더 정보를 담고 있는 맵.

connecting - apiUrl로 HTTP연결을 생성한다. HttpURLConnection을 열고 반환한다.

readBody - InputStream에서 응답 바디를 읽어와 문자로 반환한다.

registerUserIfNeeded - 구글 ID 정보로 회원가입을 해주는 메서드. 회원정보를 넘겨주고 User객체를 추가한다.

 

 

 

 

6. 컨트롤러 설정

    @GetMapping("/user/naver/callback")
    public String naverLogin(@RequestParam String code, HttpServletResponse response) throws  JsonProcessingException, UnsupportedEncodingException {
        OauthTokenDto tokenDto = naverService.socialLogin(code);
        jwtUtil.addJwtToCookie(tokenDto.getAccessToken(), response);
        return "redirect:/";
    }

GET매핑으로 받아올 것이고 주소는 설정해 둔 리다이렉션 주소와 일치하게 설정하면 된다.

매개변수는 RequestParam 형식으로 code를 받아오고, HttpServletResponse로 응답을 받아온다.

OauthTokenDto는 액세스토큰 정보를 가지고 있는 Dto이다.

여기에 naverService클래스의 socialLogin 메서드에 code값을 넣어준 뒤 액세스 토큰을 받아온다.

그 뒤 jwtUtil의 addJwtToCookie 메서드를 호출하여 JWT 토큰을 생성하고 응답의 쿠키에 추가한다.

넘겨주는 값은 액세스토큰값, 응답값이다.

 

 

 

 

7. 테스트 방법

https://nid.naver.com/oauth2.0/authorize?
client_id=본인의 client Id
&redirect_uri=본인의 redirect URI
&response_type=code

위 코드를 넣어서 테스트해 보면 된다.

 

나는 버튼을 하나 만들어서 테스트했다.

 

 

<추가>

카카오 소셜로그인 구현하기

 

Spring Boot 소셜로그인(카카오) 구현법(Oauth2.0)

oauth2.0 사용자가 애플리케이션 또는 웹 사이트에 로그인할 때 사용되는 프로토콜 중 하나. 이 프로토콜은 보안된 방식으로 사용자의 정보를 제공하고, 다른 웹 사이트나 애플리케이션에서 해당

ngwdeveloper.tistory.com

구글 소셜로그인 구현하기

 

Spring Boot 소셜로그인(구글) 구현법(Oauth2.0)

Oauth2.0 사용자가 애플리케이션 또는 웹 사이트에 로그인할 때 사용되는 프로토콜 중 하나. 이 프로토콜은 보안된 방식으로 사용자의 정보를 제공하고, 다른 웹 사이트나 애플리케이션에서 해당

ngwdeveloper.tistory.com

 

반응형

+ Recent posts