Android 응용 프로그램에서 사용자 설정을 저장하는 가장 적절한 방법은 무엇입니까?
사용자 이름/비밀번호를 사용하여 서버에 연결하는 응용 프로그램을 만들고 있는데, 사용자가 응용 프로그램을 시작할 때마다 암호를 입력할 필요가 없도록 "Save password" 옵션을 활성화하고 싶습니다.
공유 환경설정을 사용하여 수행하려고 했지만 이것이 최선의 해결책인지 잘 모르겠습니다.
Android 어플리케이션에서 사용자 가치/설정을 저장하는 방법을 제안해 주시면 감사하겠습니다.
일반적으로 Shared Preferences는 기본 설정을 저장하는 데 가장 적합합니다. 따라서 일반적으로 응용 프로그램과 사용자 설정을 저장하는 방법을 권장합니다.
여기서 유일한 관심사는 당신이 무엇을 구하느냐입니다.비밀번호는 항상 저장하기가 까다롭기 때문에 클리어 텍스트로 저장하는 것은 특히 주의해야 합니다.Android 아키텍처에서는 다른 응용 프로그램이 값에 액세스할 수 없도록 응용 프로그램의 SharedPreferences가 샌드박스화되므로 보안이 다소 강화되지만 전화기에 물리적으로 액세스하면 잠재적으로 값에 액세스할 수 있습니다.
가능하다면 OAuth와 같은 네고시에이트된 토큰을 사용하여 접근을 제공하도록 서버를 수정하는 것을 검토하고 있습니다.또는 암호화 저장소를 구축해야 할 수도 있습니다.단, 간단한 것은 아닙니다.적어도 디스크에 쓰기 전에 암호를 암호화해야 합니다.
나는 레토와 fiXedd에 동의한다.객관적으로 말하면 기본 설정 파일에 액세스할 수 있는 공격자는 응용 프로그램의 바이너리, 즉 암호를 해독하기 위한 키에 액세스할 가능성이 높기 때문에 Shared Preferences의 암호화에 상당한 시간과 노력을 투자하는 것은 의미가 없습니다.
그러나 Shared Preferences에서 비밀번호를 클리어 텍스트에 저장하는 모바일 애플리케이션을 식별하고 이러한 애플리케이션에 불리한 빛을 비추는 홍보 이니셔티브가 진행되고 있는 것 같습니다.예에 대해서는, http://blogs.wsj.com/digits/2011/06/08/some-top-apps-put-data-at-risk/ 및 http://viaforensics.com/appwatchdog 를 참조해 주세요.
일반적으로 보안에 더 많은 주의를 기울일 필요가 있지만, 저는 이 특정 문제에 대한 이러한 종류의 관심이 실제로 전체적인 보안을 크게 증가시키지는 않는다고 주장합니다.다만, 인식은 그대로입니다.Shared Preferences 에 배치하는 데이터를 암호화하는 솔루션이 있습니다.
자신의 Shared Preferences 오브젝트를 이 오브젝트에 랩하기만 하면 읽기/쓰기 데이터가 자동으로 암호화 및 복호화됩니다.예:
final SharedPreferences prefs = new ObscuredSharedPreferences(
this, this.getSharedPreferences(MY_PREFS_FILE_NAME, Context.MODE_PRIVATE) );
// eg.
prefs.edit().putString("foo","bar").commit();
prefs.getString("foo", null);
클래스 코드는 다음과 같습니다.
/**
* Warning, this gives a false sense of security. If an attacker has enough access to
* acquire your password store, then he almost certainly has enough access to acquire your
* source binary and figure out your encryption key. However, it will prevent casual
* investigators from acquiring passwords, and thereby may prevent undesired negative
* publicity.
*/
public class ObscuredSharedPreferences implements SharedPreferences {
protected static final String UTF8 = "utf-8";
private static final char[] SEKRIT = ... ; // INSERT A RANDOM PASSWORD HERE.
// Don't use anything you wouldn't want to
// get out there if someone decompiled
// your app.
protected SharedPreferences delegate;
protected Context context;
public ObscuredSharedPreferences(Context context, SharedPreferences delegate) {
this.delegate = delegate;
this.context = context;
}
public class Editor implements SharedPreferences.Editor {
protected SharedPreferences.Editor delegate;
public Editor() {
this.delegate = ObscuredSharedPreferences.this.delegate.edit();
}
@Override
public Editor putBoolean(String key, boolean value) {
delegate.putString(key, encrypt(Boolean.toString(value)));
return this;
}
@Override
public Editor putFloat(String key, float value) {
delegate.putString(key, encrypt(Float.toString(value)));
return this;
}
@Override
public Editor putInt(String key, int value) {
delegate.putString(key, encrypt(Integer.toString(value)));
return this;
}
@Override
public Editor putLong(String key, long value) {
delegate.putString(key, encrypt(Long.toString(value)));
return this;
}
@Override
public Editor putString(String key, String value) {
delegate.putString(key, encrypt(value));
return this;
}
@Override
public void apply() {
delegate.apply();
}
@Override
public Editor clear() {
delegate.clear();
return this;
}
@Override
public boolean commit() {
return delegate.commit();
}
@Override
public Editor remove(String s) {
delegate.remove(s);
return this;
}
}
public Editor edit() {
return new Editor();
}
@Override
public Map<String, ?> getAll() {
throw new UnsupportedOperationException(); // left as an exercise to the reader
}
@Override
public boolean getBoolean(String key, boolean defValue) {
final String v = delegate.getString(key, null);
return v!=null ? Boolean.parseBoolean(decrypt(v)) : defValue;
}
@Override
public float getFloat(String key, float defValue) {
final String v = delegate.getString(key, null);
return v!=null ? Float.parseFloat(decrypt(v)) : defValue;
}
@Override
public int getInt(String key, int defValue) {
final String v = delegate.getString(key, null);
return v!=null ? Integer.parseInt(decrypt(v)) : defValue;
}
@Override
public long getLong(String key, long defValue) {
final String v = delegate.getString(key, null);
return v!=null ? Long.parseLong(decrypt(v)) : defValue;
}
@Override
public String getString(String key, String defValue) {
final String v = delegate.getString(key, null);
return v != null ? decrypt(v) : defValue;
}
@Override
public boolean contains(String s) {
return delegate.contains(s);
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
delegate.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) {
delegate.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
}
protected String encrypt( String value ) {
try {
final byte[] bytes = value!=null ? value.getBytes(UTF8) : new byte[0];
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(SEKRIT));
Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
pbeCipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID).getBytes(UTF8), 20));
return new String(Base64.encode(pbeCipher.doFinal(bytes), Base64.NO_WRAP),UTF8);
} catch( Exception e ) {
throw new RuntimeException(e);
}
}
protected String decrypt(String value){
try {
final byte[] bytes = value!=null ? Base64.decode(value,Base64.DEFAULT) : new byte[0];
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey key = keyFactory.generateSecret(new PBEKeySpec(SEKRIT));
Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
pbeCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(Settings.Secure.getString(context.getContentResolver(),Settings.Secure.ANDROID_ID).getBytes(UTF8), 20));
return new String(pbeCipher.doFinal(bytes),UTF8);
} catch( Exception e) {
throw new RuntimeException(e);
}
}
}
Android Activity에서 단일 기본 설정을 저장하는 가장 간단한 방법은 다음과 같습니다.
Editor e = this.getPreferences(Context.MODE_PRIVATE).edit();
e.putString("password", mPassword);
e.commit();
이러한 암호의 보안이 걱정되는 경우 암호를 저장하기 전에 항상 암호화할 수 있습니다.
Richard가 제공한 스니펫을 사용하여 암호를 저장하기 전에 암호화할 수 있습니다.그러나 Preferences API는 값을 가로채고 암호화하는 쉬운 방법을 제공하지 않습니다.OnPreferenceChange 청취자를 통해 저장되는 값을 차단할 수 있으며 이론적으로는 preferenceChangeListener를 통해 변경할 수 있지만 결과적으로 무한 루프가 발생합니다.
이것을 실현하기 위해서, 「숨겨진」선호를 추가하는 것을 제안했습니다.확실히 최선의 방법은 아니야.저는 더 실현 가능성이 있다고 생각되는 다른 두 가지 옵션을 제시하겠습니다.
먼저 가장 간단한 것은 preference Change Listener에 있습니다.입력한 값을 가져와 암호화한 후 대체 설정 파일에 저장할 수 있습니다.
public boolean onPreferenceChange(Preference preference, Object newValue) {
// get our "secure" shared preferences file.
SharedPreferences secure = context.getSharedPreferences(
"SECURE",
Context.MODE_PRIVATE
);
String encryptedText = null;
// encrypt and set the preference.
try {
encryptedText = SimpleCrypto.encrypt(Preferences.SEED,(String)newValue);
Editor editor = secure.getEditor();
editor.putString("encryptedPassword",encryptedText);
editor.commit();
}
catch (Exception e) {
e.printStackTrace();
}
// always return false.
return false;
}
및 은 EditTextPreference를를 작성하는 것입니다.@Override the @Override 。EditTextPreference @ 。setText()
★★★★★★★★★★★★★★★★★」getText()
「」, 「」, 「」, 「」, 「」, 「」,setText()
하고, 「 「」는 「」로 암호화합니다.getText()
manager가 됩니다.
네, 꽤 오랫동안 답이 엇갈리긴 했지만, 여기 몇 가지 공통적인 답이 있습니다.나는 이것을 정신없이 조사했지만 좋은 답을 만드는 것이 어려웠다.
MODE_PRIVATE 메서드는 사용자가 디바이스를 루팅하지 않았다고 가정할 경우 일반적으로 안전한 것으로 간주됩니다.데이터는 원래 프로그램에서만 액세스할 수 있는 파일 시스템의 일부에 일반 텍스트로 저장됩니다.루트 디바이스의 다른 앱으로 패스워드를 쉽게 취득할 수 있습니다.루트 디바이스를 지원하시겠습니까?
AES는 여전히 최선의 암호화입니다.새로운 구현을 시작하는 경우 게시한 지 오래되었으면 이 항목을 찾아 보십시오.이 문제의 가장 큰 문제는 "암호화 키를 어떻게 할 것인가?"입니다.
자, 이제 '키를 어떻게 해야 할까?'에 대해 설명하겠습니다.이게 어려운 부분이에요.열쇠를 손에 넣는 것은 나쁘지 않은 것으로 판명되었다.키 파생 기능을 사용하여 암호를 추출하여 매우 안전한 키로 만들 수 있습니다."PKFDF2에서 몇 번 패스합니까?"와 같은 문제가 발생하지만 이는 다른 주제입니다.
AES 키는 디바이스에서 저장하는 것이 이상적입니다.서버로부터 안전하고, 신뢰성 있고, 안전하게 키를 취득할 수 있는 좋은 방법을 찾아내야 합니다.
어떤 종류의 로그인 시퀀스(리모트액세스의 경우 원래 로그인 시퀀스도 포함)가 있습니다.동일한 암호로 키 생성기를 두 번 실행할 수 있습니다.이 동작은 새로운 salt와 새로운 안전한 초기화 벡터를 사용하여 키를 두 번 도출하는 것입니다.생성된 비밀번호 중 하나를 디바이스에 저장하고 두 번째 비밀번호를 AES 키로 사용합니다.
로그인 시 로컬로그인 시 키를 다시 파생하여 저장된 키와 비교합니다.이 작업이 완료되면 AES에 파생 키 #2 를 사용합니다.
- "일반적으로 안전한" 접근 방식을 사용하여 AES를 사용하여 데이터를 암호화하고 키를 MODE_PRIVATE에 저장합니다.이것은 최근 Android 블로그 투고에서 추천하고 있습니다.놀라울 정도로 안전하지는 않지만 일반 텍스트보다 훨씬 나은 사용자도 있습니다.
이것들의 많은 변형을 할 수 있습니다.예를 들어 완전한 로그인 시퀀스 대신 빠른 PIN(파생)을 실행할 수 있습니다.빠른 PIN은 전체 로그인 시퀀스만큼 안전하지 않을 수 있지만 일반 텍스트보다 몇 배 더 안전합니다.
이게 좀 괴짜라는 건 알지만 Android Account Manager를 사용해야 합니다.이 시나리오에 맞게 설계되어 있습니다.조금 번거롭지만 SIM 카드가 변경되어도 로컬 자격 정보가 무효가 되기 때문에 누군가가 전화기를 훔쳐 새 SIM을 던져도 자격 정보가 손상되지 않습니다.
또한 사용자는 단말기에 저장된 계정의 자격 증명에 한 곳에서 쉽고 빠르게 액세스할 수 있습니다(또한 삭제도 가능합니다.
SampleSyncAdapter는 저장된 계정 자격 증명을 사용하는 예입니다.
Android에서의 패스워드 보안에 대해 말씀드리기 위해 출사표를 던집니다.Android에서는 디바이스 바이너리가 손상된 것으로 간주해야 합니다.이것은 사용자가 직접 제어하는 모든 엔드 어플리케이션에서도 마찬가지입니다.개념적으로 해커는 바이너리에 대한 필요한 액세스를 사용하여 바이너리를 디컴파일하고 암호화된 패스워드를 뿌리뽑을 수 있습니다.
따라서 보안이 주요 관심사라면 다음 두 가지 제안을 드리고 싶습니다.
1) 실제 비밀번호를 저장하지 마십시오.허용된 액세스 토큰을 저장하고 액세스 토큰과 전화기의 시그니처를 사용하여 세션서버측을 인증합니다.이 방법의 이점은 토큰의 기간을 제한할 수 있고 원래 비밀번호를 침해하지 않으며 나중에 트래픽과 관련짓는 데 사용할 수 있는 좋은 시그니처가 있다는 것입니다(예: 침입 시도를 확인하고 토큰을 무효로 하는 등).
2) 2 팩터 인증을 활용한다.이는 불가피하게 컴플라이언스 상황에 따라서는 더 귀찮고 거슬릴 수 있습니다.
이 답변은 질문 제목(저처럼)에 따라 여기에 도착하는 사용자를 위한 보충 답변으로 비밀번호 저장과 관련된 보안 문제를 처리할 필요가 없습니다.
공유 프리퍼런스 사용방법
에서 Android를 사용하여 로컬로 됩니다.SharedPreferences
이치노요.String
키를 눌러 관련 값을 저장 또는 조회합니다.
공유 기본 설정에 쓰기
String key = "myInt";
int valueToSave = 10;
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(key, valueToSave).commit();
apply()
commit()
바로 저장하기 보다는 백그라운드에서 저장하기 위해서입니다.
공유 기본 설정에서 읽기
String key = "myInt";
int defaultValue = 0;
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
int savedValue = sharedPref.getInt(key, defaultValue);
키를 찾을 수 없는 경우 기본값이 사용됩니다.
메모들
위에서와 같이 로컬키 String을 여러 곳에서 사용하는 것보다 한 곳에서 상수를 사용하는 것이 좋습니다.설정 작업의 맨 위에서 다음과 같은 기능을 사용할 수 있습니다.
final static String PREF_MY_INT_KEY = "myInt";
사용하였습니다.
int
예에서는, 「」, 「」를 사용할 수도 있습니다.putString()
,putBoolean()
,getString()
,getBoolean()
등등.상세한 것에 대하여는, 메뉴얼을 참조해 주세요.
Shared Preferences를 취득하는 방법은 여러 가지가 있습니다.주의해야 할 사항은 이 답변을 참조하십시오.
또한 이 작은 lib에는 당신이 언급한 기능이 포함되어 있습니다.
https://github.com/kovmarci86/android-secure-preferences
그것은 여기 있는 다른 아프로치들과 비슷하다.희망은 도움이 된다:)
이 답변은 Mark가 제안한 접근법에 기초하고 있습니다.뷰에 표시되는 일반 텍스트와 기본 설정 저장소에 저장된 암호의 암호화된 버전 사이를 왔다 갔다 변환하는 EditTextPreference 클래스의 사용자 정의 버전이 생성됩니다.
이 스레드에 대해 답변한 대부분의 사용자가 지적했듯이 보안 수준은 부분적으로 사용되는 암호화/복호화 코드에 따라 다르지만 이는 매우 안전한 기술은 아닙니다.하지만 그것은 꽤 간단하고 편리하며, 대부분의 가벼운 기웃거림을 방해할 것이다.
커스텀 EditTextPreference 클래스의 코드는 다음과 같습니다.
package com.Merlinia.OutBack_Client;
import android.content.Context;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.util.Base64;
import com.Merlinia.MEncryption_Main.MEncryptionUserPassword;
/**
* This class extends the EditTextPreference view, providing encryption and decryption services for
* OutBack user passwords. The passwords in the preferences store are first encrypted using the
* MEncryption classes and then converted to string using Base64 since the preferences store can not
* store byte arrays.
*
* This is largely copied from this article, except for the encryption/decryption parts:
* https://groups.google.com/forum/#!topic/android-developers/pMYNEVXMa6M
*/
public class EditPasswordPreference extends EditTextPreference {
// Constructor - needed despite what compiler says, otherwise app crashes
public EditPasswordPreference(Context context) {
super(context);
}
// Constructor - needed despite what compiler says, otherwise app crashes
public EditPasswordPreference(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
// Constructor - needed despite what compiler says, otherwise app crashes
public EditPasswordPreference(Context context, AttributeSet attributeSet, int defaultStyle) {
super(context, attributeSet, defaultStyle);
}
/**
* Override the method that gets a preference from the preferences storage, for display by the
* EditText view. This gets the base64 password, converts it to a byte array, and then decrypts
* it so it can be displayed in plain text.
* @return OutBack user password in plain text
*/
@Override
public String getText() {
String decryptedPassword;
try {
decryptedPassword = MEncryptionUserPassword.aesDecrypt(
Base64.decode(getSharedPreferences().getString(getKey(), ""), Base64.DEFAULT));
} catch (Exception e) {
e.printStackTrace();
decryptedPassword = "";
}
return decryptedPassword;
}
/**
* Override the method that gets a text string from the EditText view and stores the value in
* the preferences storage. This encrypts the password into a byte array and then encodes that
* in base64 format.
* @param passwordText OutBack user password in plain text
*/
@Override
public void setText(String passwordText) {
byte[] encryptedPassword;
try {
encryptedPassword = MEncryptionUserPassword.aesEncrypt(passwordText);
} catch (Exception e) {
e.printStackTrace();
encryptedPassword = new byte[0];
}
getSharedPreferences().edit().putString(getKey(),
Base64.encodeToString(encryptedPassword, Base64.DEFAULT))
.commit();
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
if (restoreValue)
getEditText().setText(getText());
else
super.onSetInitialValue(restoreValue, defaultValue);
}
}
사용 방법을 보여 줍니다. 이 파일은 기본 설정 표시를 구동하는 "항목" 파일입니다.일반 EditTextPreference 보기 3개와 사용자 정의 EditPasswordPreference 보기 1개가 있습니다.
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<EditTextPreference
android:key="@string/useraccountname_key"
android:title="@string/useraccountname_title"
android:summary="@string/useraccountname_summary"
android:defaultValue="@string/useraccountname_default"
/>
<com.Merlinia.OutBack_Client.EditPasswordPreference
android:key="@string/useraccountpassword_key"
android:title="@string/useraccountpassword_title"
android:summary="@string/useraccountpassword_summary"
android:defaultValue="@string/useraccountpassword_default"
/>
<EditTextPreference
android:key="@string/outbackserverip_key"
android:title="@string/outbackserverip_title"
android:summary="@string/outbackserverip_summary"
android:defaultValue="@string/outbackserverip_default"
/>
<EditTextPreference
android:key="@string/outbackserverport_key"
android:title="@string/outbackserverport_title"
android:summary="@string/outbackserverport_summary"
android:defaultValue="@string/outbackserverport_default"
/>
</PreferenceScreen>
실제의 암호화/복호화에 대해서는, 독자의 연습으로서 남겨져 있습니다.키와 초기화 벡터의 값은 다르지만 현재 이 기사 http://zenu.wordpress.com/2011/09/21/aes-128bit-cross-platform-java-and-c-encryption-compatibility/,에 기반한 코드를 사용하고 있습니다.
우선 사용자의 데이터는 전화기에 저장해서는 안 되며, 데이터를 전화기에 저장해야 하는 경우에는 앱의 개인 데이터로 암호화해야 한다고 생각합니다.사용자 자격 정보의 보안이 애플리케이션의 우선 사항이어야 합니다.
중요한 데이터는 안전하게 저장하거나 전혀 저장하지 않아야 합니다.디바이스 분실이나 말웨어 감염의 경우 안전하지 않게 저장된 데이터가 손상될 수 있습니다.
Android KeyStore를 사용하여 ECB 모드에서 RSA를 사용하여 비밀번호를 암호화한 후 Shared Preferences에 저장합니다.
비밀번호를 반환하려면 Shared Preferences에서 암호화된 비밀번호를 읽고 KeyStore를 사용하여 암호를 해독합니다.
이 방법으로 Android에 의해 안전하게 저장 및 관리되는 공개/개인 키 쌍을 생성할 수 있습니다.
다음은 그 방법에 대한 링크입니다.Android KeyStore 튜토리얼
이미 지적된 바와 같이 Shared Preferences는 일반적으로 사용할 수 있지만 데이터를 암호화하여 저장하려는 경우에는 다소 불편합니다.다행히 키와 값을 암호화하는 Shared Preferences가 구현되어 있기 때문에 데이터를 쉽고 빠르게 암호화할 수 있습니다.Android JetPack Security에서 EncryptedSharedPreferences를 사용할 수 있습니다.
AndroidX Security를 빌드에 추가하기만 하면 됩니다.gradle:
implementation 'androidx.security:security-crypto:1.0.0-rc01'
다음과 같이 사용할 수 있습니다.
String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
"secret_shared_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
// use the shared preferences and editor as you normally would
SharedPreferences.Editor editor = sharedPreferences.edit();
자세한 것은, https://android-developers.googleblog.com/2020/02/data-encryption-on-android-with-jetpack.html 를 참조해 주세요.
공식 문서: https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences
비밀번호를 저장하려면 sqlite, security apit을 사용해야 합니다.다음은 비밀번호를 저장하는 가장 좋은 예입니다. -- password safe.소스 및 설명 링크입니다.http://code.google.com/p/android-passwordsafe/
공유 환경설정은 애플리케이션 데이터를 저장하는 가장 쉬운 방법입니다.그러나 누구나 애플리케이션 manager.so을 통해 공유 환경설정에 대한 데이터를 지울 수 있습니다.저는 이것이 우리 애플리케이션에 완전히 안전하다고 생각하지 않습니다.
언급URL : https://stackoverflow.com/questions/785973/what-is-the-most-appropriate-way-to-store-user-settings-in-android-application
'programing' 카테고리의 다른 글
C에서 클래스를 구현하려면 어떻게 해야 합니까? (0) | 2022.08.29 |
---|---|
iOS에서 더블/플로트의 최대값은 얼마입니까? (0) | 2022.08.29 |
Java String 배열: 메서드의 크기가 있습니까? (0) | 2022.08.29 |
하위 컴포넌트에서 부모 메서드를 호출합니다(Vue.js). (0) | 2022.08.29 |
자기 참조 구조 정의? (0) | 2022.08.29 |