모바일 앱 OAuth2 (1) -액세스 토큰으로 유저 정보를 받아오자

Alex·2025년 1월 21일
0

Plaything

목록 보기
81/118

Plaything은 모바일앱이라서 웹과는 다른 방식으로 OAuth2를 구현해야 한다.

웹에서는 OAuth2인증과 토큰을 통한 유저 정보요청까지 모두 백엔드단에서 진행하고 클라이언트로 JWT 토큰을 보낸다. 대부분의 작업을 백에서 한다.

반면, 모바일방식에서는 OAuth2 인증을 클라이언트에서 한다. 그러면 액세스 토큰을 백엔드로 전달하는데, 이 액세스 토큰을 갖고서 유저정보를 받아오고 가입/로그인을 한 뒤에 클라이언트로 JWT 토큰을 전달해주면 된다.

관련 자료를 찾을 수가 없어서 구현하는 게 난감했는데
안드로이드, 스프링, 로그인 관련 질문입니다 여기에 참고할 내용이 많았다.

우선, 비밀번호같은 경우는 서버에서 임의의 난수로 만든 뒤 저장한다.

또한, 이 경우 모바일에서 각 프로바이더가 제공하는 SDK를 사용해서 OAuth 인증을 하기에 스프링시큐리티에서 제공하는 OAuth2 클라이언트를 사용할 필요가 없다.

// 안드로이드에서 디바이스 ID 생성
val deviceId = Settings.Secure.getString(
    context.contentResolver,
    Settings.Secure.ANDROID_ID
)

@Entity
@Table(name = "device_info")
public class DeviceInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String deviceId;      // 디바이스 고유 식별자 (보안/인증용)

    @Column
    private String fcmToken;      // FCM 토큰 (푸시 알림용)

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    private LocalDateTime lastTokenUpdate;
}


    
    public void validateLogin(String deviceId, Member member) {
        // 로그인 시 디바이스 ID로 인증
        DeviceInfo deviceInfo = deviceRepository.findByMemberAndDeviceId(member, deviceId)
            .orElseThrow(() -> new SecurityException("미등록 디바이스"));
            
        if (deviceInfo.isBlocked()) {
            throw new SecurityException("차단된 디바이스");
        }
    }
    
    

이런식으로 디바이스 정보를 저장한 뒤에(해싱한 뒤 저장), 이를 통해 검증하는 것을 말하는 과정도 보안에 필요한 것으로 보인다.

Proof Key for Code Exchange by OAuth Public Clients여기에서는 중간에 OAuth2 액세스 토큰이 탈취당하는 경우에 대비해서 PCKE 방식을 쓸 것을 권하고 있다. 카카오디벨로퍼에서 설명해준 내용이다. 이외에도 보안권장사항이 있는데, 이 내용들을 개발하면서 적용해볼 것이다.

우선 기본 뼈대를 만들자

https://accounts.google.com/o/oauth2/v2/auth
?client_id= 클라이언트 id
&redirect_uri=http://localhost:8080/login/oauth2/code/google
&response_type=code
&scope=email%20profile

여기에 들어가서 로그인을 해야 Authoization 토큰을 받는다.
리다이렉트 되는 url 주소에 code=~ 이 부분이 있는데 이게 Authoization 토큰이다.

이 Authoization토큰을 통해서 액세스 토큰을 받아야 한다.

 public String getAccessToken(String authorizationCode) throws UnsupportedEncodingException {

        String decodedCode = URLDecoder.decode(authorizationCode, StandardCharsets.UTF_8.name());
        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", clientId);
        params.add("client_secret", clientSecret);
        params.add("code", decodedCode);
        params.add("redirect_uri", redirectUri);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

        ResponseEntity<String> response = restTemplate.postForEntity(googleAuthUrl, request, String.class);

        return response.getBody();
    }
    

프로덕션 코드에서는 클라이언트에서 액세스 토큰을 전달해주지만
개발 환경에서는 내가 직접 액세스 토큰을 받아서 이를 통해 유저 정보를 저장하는 걸 구현해서
로직이 실제로 돌아가는지 확인해야 한다.

위 코드로 애겟스 토큰을 받아올 수 있다.

그러면, 이 액세스 토큰을 통해서 유저 정보를 받을 차례다.

    @GetMapping("/google/userinfo")
    public ResponseEntity<GoogleUserInfo> getGoogleUserInfo(@RequestParam String accessToken) {
        GoogleUserInfo userInfo = googleApiClient.getUserInfo("Bearer " + accessToken);
        return ResponseEntity.ok(userInfo);
    }
    
    
    @FeignClient(name = "googleApi", url = "https://www.googleapis.com")
public interface GoogleApiClient {
    @GetMapping("/oauth2/v3/userinfo")
    GoogleUserInfo getUserInfo(@RequestHeader("Authorization") String bearerToken);
}

그럼 위처럼 액세스토큰으로 유저 정보를 받아올 수 있다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글