Java 在 Android 应用程序中存储用户设置的最合适方法是什么
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/785973/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
What is the most appropriate way to store user settings in Android application
提问by Niko Gamulin
I am creating an application which connects to the server using username/password and I would like to enable the option "Save password" so the user wouldn't have to type the password each time the application starts.
我正在创建一个使用用户名/密码连接到服务器的应用程序,我想启用“保存密码”选项,这样用户就不必在每次应用程序启动时输入密码。
I was trying to do it with Shared Preferences but am not sure if this is the best solution.
我试图用共享首选项来做到这一点,但我不确定这是否是最好的解决方案。
I would appreciate any suggestion on how to store user values/settings in Android application.
我很感激有关如何在 Android 应用程序中存储用户值/设置的任何建议。
采纳答案by Reto Meier
In general SharedPreferences are your best bet for storing preferences, so in general I'd recommend that approach for saving application and user settings.
一般而言,SharedPreferences 是存储首选项的最佳选择,因此一般而言,我建议使用这种方法来保存应用程序和用户设置。
The only area of concern here is what you're saving. Passwords are always a tricky thing to store, and I'd be particularly wary of storing them as clear text. The Android architecture is such that your application's SharedPreferences are sandboxed to prevent other applications from being able to access the values so there's some security there, but physical access to a phone could potentially allow access to the values.
这里唯一需要关注的方面是您正在保存的内容。存储密码总是一件棘手的事情,我会特别谨慎地将它们存储为明文。Android 架构是这样的,您的应用程序的 SharedPreferences 被沙箱化以防止其他应用程序能够访问这些值,因此那里有一些安全性,但对手机的物理访问可能允许访问这些值。
If possible I'd consider modifying the server to use a negotiated token for providing access, something like OAuth. Alternatively you may need to construct some sort of cryptographic store, though that's non-trivial. At the very least, make sure you're encrypting the password before writing it to disk.
如果可能,我会考虑修改服务器以使用协商令牌来提供访问权限,例如OAuth。或者,您可能需要构建某种加密存储,尽管这很重要。至少,请确保在将密码写入磁盘之前对其进行加密。
回答by Reto Meier
you need to use the sqlite, security apit to store the passwords. here is best example, which stores passwords, -- passwordsafe. here is link for the source and explanation -- http://code.google.com/p/android-passwordsafe/
您需要使用 sqlite、security api 来存储密码。这是最好的例子,它存储密码,--passwordsafe。这是来源和解释的链接 - http://code.google.com/p/android-passwordsafe/
回答by Jeremy Logan
About the simplest way to store a single preference in an Android Activity is to do something like this:
在 Android Activity 中存储单个首选项的最简单方法是执行以下操作:
Editor e = this.getPreferences(Context.MODE_PRIVATE).edit();
e.putString("password", mPassword);
e.commit();
If you're worried about the security of these then you could always encrypt the password before storing it.
如果您担心这些的安全性,那么您始终可以在存储密码之前对其进行加密。
回答by Mark
Using the snippet provided by Richard, you can encrypt the password before saving it. The preferences API however doesn't provide an easy way to intercept the value and encrypt it - you can block it being saved via an OnPreferenceChange listener, and you theoretically could modify it through a preferenceChangeListener, but that results in an endless loop.
使用 Richard 提供的代码片段,您可以在保存密码之前对其进行加密。然而,首选项 API 并没有提供一种简单的方法来拦截值并对其进行加密——您可以阻止它通过 OnPreferenceChange 侦听器保存,理论上您可以通过首选项更改侦听器修改它,但这会导致无限循环。
I had earlier suggested adding a "hidden" preference in order to accomplish this. It's definitely not the best way. I'm going to present two other options that I consider to be more viable.
我早些时候曾建议添加一个“隐藏的”偏好来实现这一点。这绝对不是最好的方法。我将介绍另外两个我认为更可行的选择。
First, the simplest, is in a preferenceChangeListener, you can grab the entered value, encrypt it, and then save it to an alternative preferences file:
首先,最简单的是在一个preferenceChangeListener中,您可以获取输入的值,对其进行加密,然后将其保存到另一个首选项文件中:
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;
}
The second way, and the way I now prefer, is to create your own custom preference, extending EditTextPreference, @Override'ing the setText()
and getText()
methods, so that setText()
encrypts the password, and getText()
returns null.
第二种方式,也是我现在更喜欢的方式,是创建您自己的自定义首选项,扩展 EditTextPreference,@Override'ingsetText()
和getText()
方法,以便setText()
对密码进行加密,并getText()
返回 null。
回答by emmby
I agree with Reto and fiXedd. Objectively speaking it doesn't make a lot of sense investing significant time and effort into encrypting passwords in SharedPreferences since any attacker that has access to your preferences file is fairly likely to also have access to your application's binary, and therefore the keys to unencrypt the password.
我同意 Reto 和 fixd。客观地说,投入大量时间和精力来加密 SharedPreferences 中的密码并没有多大意义,因为任何可以访问您的首选项文件的攻击者很可能也可以访问您的应用程序的二进制文件,因此解密密钥密码。
However, that being said, there does seem to be a publicity initiative going on identifying mobile applications that store their passwords in cleartext in SharedPreferences and shining unfavorable light on those applications. See http://blogs.wsj.com/digits/2011/06/08/some-top-apps-put-data-at-risk/and http://viaforensics.com/appwatchdogfor some examples.
然而,话虽如此,似乎确实有一项宣传计划正在识别将密码以明文形式存储在 SharedPreferences 中的移动应用程序,并对这些应用程序造成不利影响。有关一些示例,请参阅http://blogs.wsj.com/digits/2011/06/08/some-top-apps-put-data-at-risk/和http://viaforensics.com/appwatchdog。
While we need more attention paid to security in general, I would argue that this sort of attention on this one particular issue doesn't actually significantly increase our overall security. However, perceptions being as they are, here's a solution to encrypt the data you place in SharedPreferences.
虽然我们总体上需要更多地关注安全性,但我认为对这一特定问题的这种关注实际上并没有显着提高我们的整体安全性。然而,就其本身而言,这是一种加密您放置在 SharedPreferences 中的数据的解决方案。
Simply wrap your own SharedPreferences object in this one, and any data you read/write will be automatically encrypted and decrypted. eg.
只需将您自己的 SharedPreferences 对象包装在这个对象中,您读/写的任何数据都将自动加密和解密。例如。
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);
Here's the code for the class:
这是该类的代码:
/**
* 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);
}
}
}
回答by Jon O
I know this is a little bit of necromancy, but you should use the Android AccountManager. It's purpose-built for this scenario. It's a little bit cumbersome but one of the things it does is invalidate the local credentials if the SIM card changes, so if somebody swipes your phone and throws a new SIM in it, your credentials won't be compromised.
我知道这有点像死灵法术,但您应该使用 Android AccountManager。它是专门为此场景构建的。这有点麻烦,但是如果 SIM 卡发生变化,它所做的一件事就是使本地凭据无效,因此如果有人刷您的手机并将新的 SIM 卡放入其中,您的凭据不会受到损害。
This also gives the user a quick and easy way to access (and potentially delete) the stored credentials for any account they have on the device, all from one place.
这也为用户提供了一种快速简便的方法来访问(并可能删除)他们在设备上拥有的任何帐户的存储凭据,所有这些都可以从一个地方进行。
SampleSyncAdapteris an example that makes use of stored account credentials.
SampleSyncAdapter是一个使用存储的帐户凭据的示例。
回答by Joe Plante
Okay; it's been a while since the answer is kind-of mixed, but here's a few common answers. I researched this like crazy and it was hard to build a good answer
好的; 答案有点混杂已经有一段时间了,但这里有一些常见的答案。我疯狂地研究这个,很难建立一个好的答案
The MODE_PRIVATE method is considered generally safe, if you assume that the user didn't root the device. Your data is stored in plain text in a part of the file system that can only be accessed by the original program. This makings grabbing the password with another app on a rooted device easy. Then again, do you want to support rooted devices?
AES is still the best encryption you can do. Remember to look this up if you are starting a new implementation if it's been a while since I posted this. The largest issue with this is "What to do with the encryption key?"
MODE_PRIVATE 方法通常被认为是安全的,如果您假设用户没有 root 设备。您的数据以纯文本形式存储在文件系统的一部分中,只能由原始程序访问。这使得在有根设备上使用另一个应用程序轻松获取密码。再说一次,你想支持root设备吗?
AES 仍然是您能做的最好的加密。如果您正在开始一个新的实现,如果我发布这个已经有一段时间了,请记住查看这个。最大的问题是“如何处理加密密钥?”
So, now we are at the "What to do with the key?" portion. This is the hard part. Getting the key turns out to be not that bad. You can use a key derivation function to take some password and make it a pretty secure key. You do get into issues like "how many passes do you do with PKFDF2?", but that's another topic
所以,现在我们处于“如何处理密钥?” 部分。这是困难的部分。事实证明,获得钥匙并没有那么糟糕。您可以使用密钥派生函数来获取一些密码并使其成为非常安全的密钥。您确实会遇到诸如“您使用 PKFDF2 进行了多少遍?”之类的问题,但这是另一个话题
Ideally, you store the AES key off the device. You have to figure out a good way to retrieve the key from the server safely, reliably, and securely though
You have a login sequence of some sort (even the original login sequence you do for remote access). You can do two runs of your key generator on the same password. How this works is that you derive the key twice with a new salt and a new secure initialization vector. You store one of those generated passwords on the device, and you use the second password as the AES key.
理想情况下,您将 AES 密钥存储在设备之外。您必须想出一种安全、可靠和安全地从服务器检索密钥的好方法
您有某种登录序列(甚至是您为远程访问所做的原始登录序列)。您可以使用同一个密码运行两次密钥生成器。它的工作原理是您使用新的盐和新的安全初始化向量两次派生密钥。您将这些生成的密码之一存储在设备上,并将第二个密码用作 AES 密钥。
When you log in, you re-derive the key on the local login and compare it to the stored key. Once that is done, you use derive key #2 for AES.
当您登录时,您在本地登录时重新导出密钥并将其与存储的密钥进行比较。完成后,您可以为 AES 使用派生密钥 #2。
- Using the "generally safe" approach, you encrypt the data using AES and store the key in MODE_PRIVATE. This is recommended by a recent-ish Android blog post. Not incredibly secure, but way better for some people over plain text
- 使用“一般安全”方法,您使用 AES 加密数据并将密钥存储在 MODE_PRIVATE 中。这是最近一篇 Android 博客文章推荐的。不是非常安全,但对于某些人来说比纯文本更好
You can do a lot of variations of these. For example, instead of a full login sequence, you can do a quick PIN (derived). The quick PIN might not be as secure as a full login sequence, but it's many times more secure than plain text
你可以做很多这些变化。例如,您可以执行快速 PIN(派生),而不是完整的登录序列。快速 PIN 可能不如完整登录序列安全,但它比纯文本安全许多倍
回答by RenniePet
This answer is based on a suggested approach by Mark. A custom version of the EditTextPreference class is created which converts back and forth between the plain text seen in the view and an encrypted version of the password stored in the preferences storage.
此答案基于 Mark 建议的方法。创建了 EditTextPreference 类的自定义版本,它在视图中看到的纯文本和存储在首选项存储中的密码的加密版本之间来回转换。
As has been pointed out by most who have answered on this thread, this is not a very secure technique, although the degree of security depends partly on the encryption/decryption code used. But it's fairly simple and convenient, and will thwart most casual snooping.
正如大多数在此线程上回答的人所指出的那样,这不是一种非常安全的技术,尽管安全程度部分取决于所使用的加密/解密代码。但它相当简单和方便,并且会阻止大多数随意的窥探。
Here is the code for the custom EditTextPreference class:
这是自定义 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);
}
}
This shows how it can be used - this is the "items" file that drives the preferences display. Note it contains three ordinary EditTextPreference views and one of the custom EditPasswordPreference views.
这显示了如何使用它 - 这是驱动首选项显示的“项目”文件。请注意,它包含三个普通的 EditTextPreference 视图和一个自定义 EditPasswordPreference 视图。
<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>
As for the actual encryption/decryption, that is left as an exercise for the reader. I'm currently using some code based on this article http://zenu.wordpress.com/2011/09/21/aes-128bit-cross-platform-java-and-c-encryption-compatibility/, although with different values for the key and the initialization vector.
至于实际的加密/解密,留给读者作为练习。我目前正在使用基于这篇文章的一些代码http://zenu.wordpress.com/2011/09/21/aes-128bit-cross-platform-java-and-c-encryption-compatibility/,尽管具有不同的值用于密钥和初始化向量。
回答by Marcell
You can also check out this little lib, containing the functionality you mention.
您还可以查看这个小库,其中包含您提到的功能。
https://github.com/kovmarci86/android-secure-preferences
https://github.com/kovmarci86/android-secure-preferences
It is similar to some of the other aproaches here. Hope helps :)
它类似于这里的其他一些方法。希望有所帮助:)
回答by Rohit Jain
shared preferences is easiest way to store our application data. but it is possible that anyone can clear our shared preferences data through application manager.so i don't think it is completely safe for our application.
共享首选项是存储应用程序数据的最简单方法。但是任何人都可以通过应用程序管理器清除我们共享的首选项数据。所以我认为这对我们的应用程序来说不是完全安全的。