1. 소개
일반적으로 앱은 인터넷을 통해 데이터를 교환합니다. 앱이 신뢰할 수 없는 서버와 통신할 수도 있으므로 공개할 수 없는 민감한 정보를 주고받을 때 주의해야 합니다.
빌드할 항목
이 Codelab에서는 메시지를 표시하는 앱을 빌드해보겠습니다. 각 메시지에는 발신자 이름, 문자 메시지, '프로필 사진'의 URL이 포함됩니다. 앱은 다음과 같은 방법으로 메시지를 표시합니다.
|
학습할 내용
- 보안 네트워크 통신이 중요한 이유
- Volley 라이브러리를 사용하여 네트워크를 요청하는 방법
- 네트워크 보안 구성을 사용하여 네트워크 통신의 보안을 강화하는 방법
- 개발 및 테스트 과정에 도움이 되는 고급 네트워크 보안 구성 옵션을 수정하는 방법
- 가장 일반적인 네트워크 보안 문제 중 하나를 살펴보고 네트워크 보안 구성이 이를 방지하는 데 어떻게 도움이 되는지 확인하는 방법
필요한 항목
- 최신 버전의 Android 스튜디오
- Android 7.0(API 수준 24) 이상을 실행하는 Android 기기 또는 에뮬레이터
- Node.js(또는 구성 가능한 웹 서버에 액세스할 권한)
이 Codelab을 진행하는 동안 코드 버그, 문법 오류, 불명확한 문구 등의 문제가 발생하면 Codelab 왼쪽 하단에 있는 오류 신고 링크를 통해 문제를 신고해 주세요.
2. 설정
코드 다운로드
다음 링크를 클릭하면 이 Codelab의 모든 코드를 다운로드할 수 있습니다.
다운로드한 ZIP 파일의 압축을 해제합니다. 이렇게 하면 루트 폴더(android-network-secure-config
)가 압축 해제되며, 이 폴더에는 Android 스튜디오 프로젝트(SecureConfig/
)와 이후 단계에서 사용할 데이터 파일(server/
)이 들어 있습니다.
GitHub에서 바로 코드를 확인할 수도 있습니다(master
브랜치로 시작).
각 단계 후 최종 코드가 포함될 브랜치도 준비되어 있습니다. 문제가 발생하면 GitHub의 브랜치를 살펴보거나 전체 저장소(https://github.com/android/codelab-android-network-security-config/branches/all)를 클론합니다.
3. 앱 실행
'로드' 아이콘을 클릭하면 앱이 원격 서버에 액세스하여 JSON 파일로부터 메시지, 이름, 프로필 사진의 URL 목록을 로드합니다. 그런 다음, 메시지가 목록에 표시되고 앱이 참조 URL에서 이미지를 로드합니다.
참고: 이 Codelab에서 사용하는 앱은 데모 전용입니다. 따라서 프로덕션 환경에서처럼 많은 오류를 처리하지 않습니다.
앱 아키텍처
이 앱은 데이터 저장소와 네트워크 액세스(모델)가 로직(프레젠터) 및 디스플레이(뷰)와 분리되는 MVP 패턴을 따릅니다.
MainContract
클래스에는 뷰와 프레젠터 간의 인터페이스를 설명하는 계약이 포함됩니다.
MainContract.java
/*
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.networksecurity;
import com.example.networksecurity.model.Post;
/**
* Contract defining the interface between the View and Presenter.
*/
public interface MainContract {
interface View {
/**
* Sets the presenter for interaction from the View.
*
* @param presenter
*/
void setPresenter(Presenter presenter);
/**
* Displays or hides a loading indicator.
*
* @param isLoading If true, display a loading indicator, hide it otherwise.
*/
void setLoadingPosts(boolean isLoading);
/**
* Displays a list of posts on screen.
*
* @param posts The posts to display. If null or empty, the list should not be shown.
*/
void setPosts(Post[] posts);
/**
* Displays an error message on screen and optionally prints out the error to logcat.
*/
void showError(String title, String error);
/**
* Hides the error message.
*
* @see #showError(String, String)
*/
void hideError();
/**
* Displays an empty message and icon.
*
* @param showMessage If true, the message is show. If false, the message is hidden
*/
void showNoPostsMessage(boolean showMessage);
}
interface Presenter {
/**
* Call to start the application. Sets up initial state.
*/
void start();
/**
* Loads post for display.
*/
void loadPosts();
/**
* An error was encountered during the loading of profile images.
*/
void onLoadPostImageError(String error, Exception e);
}
}
앱 구성
시연을 위해 이 앱의 모든 네트워크 캐싱을 사용 중지하였습니다. 프로덕션 환경에서는 앱이 로컬 캐시를 활용하여 원격 네트워크 요청 수를 제한하는 것이 이상적입니다.
gradle.properties
파일에는 다음과 같이 메시지 목록이 로드된 URL이 포함됩니다.
gradle.properties
postsUrl="http://storage.googleapis.com/network-security-conf-codelab.appspot.com/v1/posts.json"
앱 빌드 및 실행
- Android 스튜디오를 시작하고 SecureConfig 디렉터리를 Android 프로젝트로 엽니다.
- '실행'을 클릭하여 앱을 시작합니다.
아래의 앱 스크린샷은 기기에서 앱이 어떻게 표시되는지 보여줍니다.
4. 기본 네트워크 보안 구성
이 단계에서는 기본 네트워크 보안 구성을 설정하고 구성 규칙 중 하나를 위반했을 때 발생하는 오류를 관찰합니다.
개요
네트워크 보안 구성을 사용하면 앱이 선언적 구성 파일을 통해 네트워크 보안 설정을 맞춤설정할 수 있습니다. 전체 구성은 이 XML 파일 내에 포함되어 있으며 코드를 변경할 필요가 없습니다.
이 구성에서는 다음과 같은 작업이 가능합니다.
- 일반 텍스트 트래픽 선택 해제: 일반 텍스트 트래픽을 사용 중지합니다.
- 맞춤 신뢰 앵커: 앱이 신뢰하는 인증 기관 및 소스를 지정합니다.
- 디버그 전용 재정의: 출시 빌드에 영향을 주지 않고 보안 연결을 안전하게 디버그합니다.
- 인증서 고정: 보안 연결을 특정 인증서로 제한합니다.
이 파일은 도메인별로 정리할 수 있으며 이를 사용하여 네트워크 보안 설정을 모든 URL에 적용하거나 특정 도메인에만 적용할 수 있습니다.
네트워크 보안 구성은 Android 7.0(API 수준 24) 이상에서 사용할 수 있습니다.
네트워크 보안 구성 XML 파일 만들기
network_security_config.xml
이라는 XML 리소스 파일을 새로 만듭니다.
왼쪽의 Android Project 패널에서 res
를 마우스 오른쪽 버튼으로 클릭한 다음 New > Android Resource File을 선택합니다.
다음 옵션을 설정하고 OK를 클릭합니다.
File name |
|
Resource type |
|
Root element |
|
Directory name |
|
xml/network_security_config.xml
파일을 엽니다(자동으로 열리지 않는 경우).
파일 내용을 다음 스니펫으로 바꿉니다.
res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" >
</base-config>
</network-security-config>
이 구성은 앱의 기본 구성, 즉 기본 보안 구성에 적용되며 모든 일반 텍스트 트래픽을 사용 중지합니다.
네트워크 보안 구성 사용 설정
다음으로 AndroidManifest.xml
파일에 앱 구성에 관한 참조를 추가합니다.
AndroidManifest.xml
파일을 열고 application
요소를 찾습니다.
먼저, android:usesCleartextTraffic="true"
속성을 설정하는 행을 삭제합니다.
그런 다음, network_security_config
XML 파일 리소스를 참조하도록 android:networkSecurityConfig
속성을 AndroidManifest의 application
요소에 추가합니다. 속성값은 @xml/network_security_config
입니다.
위의 두 속성을 삭제하고 추가하면 애플리케이션 태그의 여는 부분이 아래와 같게 됩니다.
AndroidManifest.xml
<application
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:fullBackupContent="false"
tools:ignore="GoogleAppIndexingWarning">
...
앱 컴파일 및 실행
앱을 컴파일하고 실행합니다.
앱에서 일반 텍스트 연결을 통해 데이터를 로드하려고 시도하는 중에 오류가 발생합니다.
Logcat에서 이 오류를 확인할 수 있습니다.
java.io.IOException: Cleartext HTTP traffic to storage.googleapis.com not permitted
앱이 암호화되지 않은 HTTP 연결에서 메시지 목록을 로드하도록 구성되어 있으므로 앱이 데이터를 로드하지 않습니다. gradle.properties
파일에 구성된 URL이 TLS를 사용하지 않는 HTTP 서버를 가리킵니다.
이 URL을 변경하여 다른 서버를 사용하고 보안 HTTPS 연결을 통해 데이터를 로드해보겠습니다.
gradle.properties
파일을 다음과 같이 변경합니다.
gradle.properties
postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v1/posts.json"
(URL의 https 프로토콜에 주목해 주세요.)
이 변경사항을 적용하기 위해 프로젝트를 다시 빌드해야 할 수도 있습니다. 메뉴에서 Build > Rebuild
를 선택합니다.
앱을 다시 실행합니다. 네트워크 요청이 HTTPS 연결을 사용하므로 이제 데이터 로드가 표시됩니다.
5. 일반적인 문제: 서버 측 업데이트
네트워크 보안 구성은 앱이 안전하지 않은 연결을 통해 요청할 때 발생하는 취약점으로부터 앱을 보호할 수 있습니다.
네트워크 보안 구성에서 다루고 있는 또 하나의 일반적인 문제는 Android 앱에 로드된 URL에 영향을 주는 서버 측 변경사항입니다. 예를 들어, 앱에서 서버가 프로필 이미지에 관해 안전한 HTTPS URL 대신 안전하지 않은 HTTP URL을 반환하기 시작했다고 가정해보겠습니다. 그러면 HTTPS 연결을 적용하는 네트워크 보안 구성이 런타임 시 이 요구사항을 만족하지 못하므로 예외가 발생합니다.
앱 백엔드 업데이트
앞서 언급한 대로 앱은 메시지 목록을 먼저 로드하며 각 메시지는 프로필 사진 URL을 참조합니다.
앱이 사용하는 데이터가 변경되어 다른 이미지 URL을 요청했다고 가정해보겠습니다. 백엔드 데이터 URL을 수정하여 이 변경사항을 시뮬레이션해보겠습니다.
gradle.properties
파일을 다음과 같이 변경합니다.
gradle.properties
postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v2/posts.json"
(경로의 'v2'에 주목해 주세요.)
이 변경사항을 적용하기 위해 프로젝트를 다시 빌드해야 할 수도 있습니다. 메뉴에서 Build > Rebuild
를 선택합니다.
브라우저에서 '새' 백엔드에 액세스하여 수정된 JSON 파일을 확인할 수 있습니다. 참조된 모든 URL이 HTTPS 대신 HTTP를 사용하는 방식을 확인해 보세요.
앱 실행 및 오류 검사
앱을 컴파일하고 실행합니다.
앱이 메시지를 로드하지만, 이미지가 로드되지 않습니다. 앱과 logcat에서 오류 메시지를 검사하여 이유를 알아보세요.
java.io.IOException: Cleartext HTTP traffic to storage.googleapis.com not permitted
앱은 여전히 HTTPS를 사용하여 JSON 파일에 액세스합니다. 하지만 JSON 파일 내부의 프로필 이미지 링크는 HTTP 주소를 사용하므로 앱은 (안전하지 않은) HTTP로 이미지를 로드하려고 시도합니다.
데이터 보호
네트워크 보안 구성을 사용하여 실수로 인한 데이터 노출을 차단했습니다. 앱이 안전하지 않은 데이터에 액세스하려고 시도하는 대신 연결 시도를 차단합니다.
출시 전에 백엔드 변경사항을 충분히 테스트하지 않은 것과 같은 시나리오를 생각해보세요. Android 앱에 네트워크 보안 구성을 적용하면 앱이 출시된 후에도 비슷한 문제가 발생하는 것을 파악할 수 있습니다.
백엔드를 변경하여 앱 수정
백엔드 URL을 '수정'된 새 버전으로 변경합니다. 이 예에서는 올바른 HTTPS URL을 사용하여 프로필 이미지를 참조하는 방식으로 수정사항을 시뮬레이션합니다.
gradle.properties
파일의 백엔드 URL을 변경하고 프로젝트를 새로고침합니다.
gradle.properties
postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v3/posts.json"
(경로의 v3에 주목해 주세요.)
앱을 다시 실행합니다. 이제 다음과 같이 의도한 대로 작동합니다.
6. 도메인별 구성
지금까지 base-config
에 네트워크 보안 구성을 명시했으며, 이는 앱이 연결을 시도하는 모든 연결에 구성을 적용합니다.
domain-config
요소를 명시하여 특정 대상에 사용할 구성을 재정의할 수 있습니다. domain-config
는 특정 도메인 집합의 구성 옵션을 선언합니다.
앱의 네트워크 보안 구성을 다음과 같이 업데이트해보겠습니다.
res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
이 구성은 다른 구성이 적용되는 'localhost' 및 localhost의 하위 도메인을 제외한 모든 도메인에 base-config
를 적용합니다.
이제 기본 구성은 모든 도메인에서 일반 텍스트 트래픽을 차단합니다. 하지만, 도메인 구성은 규칙을 재정의하여 앱이 재정의한 규칙을 통해 일반 텍스트로 localhost에 액세스할 수 있도록 합니다.
로컬 HTTP 서버를 사용하여 테스트
이제 앱이 일반 텍스트를 사용하여 localhost에 액세스할 수 있으므로 로컬 웹 서버를 시작하고 이 액세스 프로토콜을 테스트해보겠습니다.
Node.js, Python, PERL을 비롯하여 매우 기본적인 웹 서버를 호스팅하는 데 사용할 수 있는 다양한 도구가 있습니다. 이 Codelab에서는 http-server
Node.js 모듈을 사용하여 앱에 데이터를 제공합니다.
- 터미널을 열고
http-server
를 설치합니다.
npm install http-server -g
- 코드를 체크아웃한 디렉터리로 이동한 후
server/
디렉터리로 이동합니다.
cd server/
- 웹 서버를 시작하고 data/ 디렉터리에 있는 파일을 제공합니다.
http-server ./data -p 8080
- 웹브라우저를 열고 http://localhost:8080으로 이동하여 파일에 액세스할 수 있고 '
posts.json
' 파일을 볼 수 있는지 확인합니다.
- 다음으로, 기기에서 로컬 머신으로 포트 8080을 전달합니다. 다른 터미널 창에서 다음 명령어를 실행합니다.
adb reverse tcp:8080 tcp:8080
이제 앱이 Android 기기에서 'localhost:8080'에 액세스할 수 있습니다.
- 앱에서 데이터를 로드하는 데 사용하는 URL을 변경하여
localhost
의 새 서버를 가리키도록 합니다.gradle.properties
파일을 다음과 같이 변경합니다(이 파일을 변경한 후에는 Gradle 프로젝트를 동기화해야 할 수 있음).
gradle.properties
postsUrl="http://localhost:8080/posts.json"
- 앱을 실행하고 데이터가 로컬 머신에서 로드되었는지 확인합니다.
data/posts.json
파일을 수정하고 앱을 새로고침하여 새 구성이 의도한 대로 작동하는지 확인할 수 있습니다.
부가 정보: 도메인 구성
특정 도메인에 적용되는 구성 옵션은 domain-config
요소에 정의합니다. 이 요소에는 domain-config
규칙이 적용되어야 하는 도메인을 지정하는 domain
항목이 여러 개 포함될 수 있습니다. 여러 domain-config
요소에 비슷한 domain
항목이 포함되어 있다면 네트워크 보안 구성은 일치하는 문자 수에 따라 지정된 URL에 적용할 구성을 선택합니다. URL과 대부분의 문자가 연속적으로 일치하는 domain
항목을 포함하는 구성이 사용됩니다.
도메인 구성은 여러 도메인에 적용될 수 있고 하위 도메인을 포함할 수도 있습니다.
다음 예는 여러 도메인을 포함하는 네트워크 보안 구성을 보여줍니다. (다음은 하나의 예일 뿐 앱을 바꾸는 것은 아닙니다.)
<network-security-config>
<domain-config>
<domain includeSubdomains="true">secure.example.com</domain>
<domain includeSubdomains="true">cdn.example.com</domain>
<trust-anchors>
<certificates src="@raw/trusted_roots"/>
</trust-anchors>
</domain-config>
</network-security-config>
자세한 내용은 구성 파일 형식 정의를 참고하세요.
7. 디버그 재정의
HTTPS를 통해 요청하도록 설계된 앱을 개발하고 테스트할 때는 이전 단계에서 했던 것처럼 로컬 웹 서버 또는 테스트 환경에 연결해야 할 수도 있습니다.
네트워크 보안 구성의 debug-override
옵션을 사용하면 이러한 사용 사례에 관해 일반 텍스트 트래픽을 허용하거나 코드를 수정하는 대신 애플리케이션이 디버그 모드로 실행(즉, android:debuggable
이 참)될 때만 적용하는 보안 옵션을 설정할 수 있습니다. 이는 디버그 모드에만 적용되는 명시적 정의이므로 조건부 코드를 사용하는 것보다 훨씬 더 안전합니다. 또한 Play 스토어는 디버그 가능 앱이 업로드되지 않도록 차단하므로 이 옵션이 더 안전합니다.
로컬 웹 서버에서 SSL 사용 설정
앞서 포트 8080에서 HTTP를 통해 데이터를 제공하는 로컬 웹 서버를 시작했습니다. 이제 자체 서명 SSL 인증서를 생성하고 이를 사용하여 HTTPS를 통해 데이터를 제공합니다.
- 터미널 창에서
server/
디렉터리로 변경한 후 다음 명령어를 실행하여 인증서를 생성합니다. 아직 http-server를 실행 중이라면[CTRL] + [C]
를 눌러 중지하세요.
# Run these commands from inside the server/ directory! # Create a certificate authority openssl genrsa -out root-ca.privkey.pem 2048 # Sign the certificate authority openssl req -x509 -new -nodes -days 100 -key root-ca.privkey.pem -out root-ca.cert.pem -subj "/C=US/O=Debug certificate/CN=localhost" -extensions v3_ca -config openssl_config.txt # create DER format crt for Android openssl x509 -outform der -in root-ca.cert.pem -out debug_certificate.crt
그러면 인증 기관을 생성하고 서명하여 Android가 요구하는 DER 형식으로 인증서가 생성됩니다.
- 새로 생성된 인증서를 사용하여 HTTPS를 사용하는 웹 서버를 시작합니다.
http-server ./data --ssl --cert root-ca.cert.pem --key root-ca.privkey.pem
백엔드 URL 업데이트
HTTPS를 통해 localhost 서버에 액세스하도록 앱을 변경합니다.
gradle.properties
파일을 변경합니다.
gradle.properties
postsUrl="https://localhost:8080/posts.json"
앱을 컴파일하고 실행합니다.
서버의 인증서가 유효하지 않아 다음과 같은 오류가 발생합니다.
java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
서버가 시스템의 일부로 신뢰할 수 없는 자체 서명 인증서를 사용하므로 앱이 웹 서버에 액세스할 수 없습니다. 다음 단계에서는 HTTPS를 사용 중지하는 대신 localhost 도메인에 자체 서명 인증서를 추가해 보겠습니다.
맞춤 인증 기관 참조
이제 웹 서버는 기본적으로 다른 기기에서 허용하지 않는 자체 서명 인증 기관(CA)을 사용하여 데이터를 제공하고 있습니다. 브라우저에서 서버에 액세스하면 다음과 같은 보안 경고가 표시됩니다. https://localhost:8080
다음으로 네트워크 보안 구성에서 debug-overrides
옵션을 사용하여 localhost
도메인만 이 맞춤 인증 기관을 허용하도록 합니다.
- 다음이 포함되도록
xml/network_security_config.xml
파일을 변경합니다.
res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<debug-overrides>
<trust-anchors>
<!-- Trust a debug certificate in addition to the system certificates -->
<certificates src="system" />
<certificates src="@raw/debug_certificate" />
</trust-anchors>
</debug-overrides>
</network-security-config>
이 구성은 일반 텍스트 네트워크 트래픽을 사용 중지하고 디버그 빌드*,*에 관련하여 res/raw
디렉터리에 저장된 인증 파일과 함께 시스템에서 제공하는 인증 기관을 사용 설정합니다.
참고: 디버그 구성은 <certificates src="system" />
을 암시적으로 추가하므로 이 부분이 없어도 앱이 작동합니다. 고급 구성에서 이 기능을 어떻게 추가하는지 보이기 위해 이 부분을 추가했습니다.
- 다음으로
server/
디렉터리의 'debug_certificate.crt
' 파일을 Android 스튜디오의 앱 리소스 디렉터리인res/raw
로 복사합니다. 파일을 Android 스튜디오의 적절한 위치로 드래그 앤 드롭할 수도 있습니다.
이 디렉터리가 존재하지 않으면 먼저 디렉터리를 만들어야 합니다.
server/ 디렉터리에서 다음 명령어를 실행하여 파일을 복사할 수 있습니다. 또는, 파일 관리자나 Android 스튜디오를 사용하여 폴더를 만들고 파일을 올바른 위치에 복사합니다.
mkdir ../SecureConfig/app/src/main/res/raw/ cp debug_certificate.crt ../SecureConfig/app/src/main/res/raw/
이제 Android 스튜디오의 app/res/raw
아래에 debug_certificate.crt
파일이 표시됩니다.
앱 실행
앱을 컴파일하고 실행합니다. 이제 앱이 자체 서명 디버그 인증서를 사용하여 HTTPS를 통해 로컬 웹 서버에 액세스합니다.
오류가 발생하면 logcat 출력을 주의 깊게 확인하고 새로운 명령줄 옵션을 사용하여 http-server
를 다시 시작했는지 확인하세요. 또한, debug_certificate.crt
파일이 올바른 위치(res/raw/debug_certificate.crt
)에 있는지 확인합니다.
8. 자세히 알아보기
네트워크 보안 구성은 다음을 비롯하여 다양한 고급 기능을 지원합니다.
이러한 기능을 사용하려면 권장사항 및 제한사항 세부정보가 담긴 문서를 참고하세요.
더 안전한 앱 만들기
이 Codelab의 일환으로 네트워크 보안 구성을 사용하여 Android 앱의 보안을 강화하는 방법을 살펴보았습니다. 앱이 이러한 기능을 활용할 수 있는 방법과 테스트 및 개발 과정에서 더 견고한 디버그 구성의 이점을 얻을 수 있는 방법을 생각해보세요.