Androidの共享登录之方案研究
由于最近公司提到了一个需求是,一个应用登录成功了,另一个自动登录。
绞尽脑汁想了好几天,看起来很容易但是想深点就漏洞百出,有的时候代码都写完了测试都成功了突然发现给一个假设就完全失效。
先前几个同事之间讨论了一下,也在QQ技术群里讨论了一下,也在网上搜过一些但是资料甚少,又都是没经过实践去认证的。
那次开会的时候我那个同事就说,用shareUserId来共享数据,多个应用在同一个进程里可以互相访问数据。我自己先想到的是ContentProvider,在群里讨论了一下都说直接用SharePrefrence比较便捷。于是我就准备叫另一个同事用shareUserId+SharePrefrence来架设,我自己用ContentProvider来架设,双管齐下,哪个下得快下得好就用哪个。其实用SharePrefrence是简单点,这我自己也知道,但是为什么我要用ContentProvider来做呢?
第一:SharePrefrence有诸多限制,有几个权限在4.2之后谷歌建议不再使用了,如MODE_WORLD_WRITEABLE ,谷歌还是建议用其他方式进行数据共享This constant was deprecated in API level 17.我自己试了几次发现行不通有的时候可以读到,有的时候读不到,后来同事告诉我说要加这个flag:MODE_MULTI_PROCESS ,但是这个flag在2.3之后又...看文档吧
This was the legacy (but undocumented) behavior in and before Gingerbread (Android 2.3) and this flag is implied when targetting such releases. For applications targetting SDK versions greater than Android 2.3, this flag must be explicitly set if desired.
第二:shareUserId要用一个签名工具导出。
第三:之前正好用到了ContentProvider,一个很吸引我的就是可以使用观察者模式,当数据库有变动可以及时得进行通知。
现在我相处四种方案跟大家分享一下:
第一种:还是用shareUserId+SharePrefrence,理由是数据配置比较简单,比较轻量型就算要改动也比较快捷得改动,船小好调头但是有一定的风险。
第二种:用ContentProvider,当我很完美得写下各种操作接口,甚至都可以直接拿到另一个项目上用了,各种适配衔接都做好了。想想大部分Android应用数据共享也是用的这个,但是这有一个致命的缺点虽然第一种方式也有这个缺点,但是我说了船小好调头解决起来也快。那么这个硬伤是什么呢?就是共享主应用,必须先安装主应用,如果先装的另一个应用,而这个应用通过一个公开的URI去查数据是查不到的应为共享主程序该没装就还没把这个URI注册到系统,就像是你要操作一个编辑系统联系人的URI在平板上没有联系人的应用这样是无法操作的。而我在以第一种方式的时候也考虑到了这点,并且也实现了其实就是互读,如果用ContentProvider也可以实现,就要创建两份ContentProvider,哪个先安装就用哪分Provider,这就要去检测另外一个应用是否安装并且URI不能同名。
第三种:直接把数据放在一份公共的地方,那就只能放在SD卡上,这样也非常方便。这就是真正的公共数据,谁先安装就是谁先创建数据,然后两个应用都读取一份数据。但是这也有个显而易见的缺点就是,一旦SD卡脱离手机数据就读不到了,并且在SD卡上也有被用户删掉的风险。似乎这种方案也不太可取。。。
第四种:就像PC端常见的QQ登录一样,假如QQ在客户端登录了,在进去QQ空间网站就可以检测在QQ客户端登录了就可以直接进入QQ空间,实现自动登录。这里简单设想一下实现方法,应用A登录会有一个调用服务器请求的接口,通过这个接口服务器就可以知道哪个设备登录了哪个帐号,当手机上应用B启动时先去服务器查一下当前有没有在这个设备上登录的帐号,如果该帐号在线就显示“已检测到你在A应用登录帐号,将以该帐号自动登录”,或者弹出个对话框提示“已检测到你在A应用登录帐号,点击快速登录”。
分析一下四种方案的可行性,第四种是最简单的,但是得注意加密传输,第一种是其次,第二种比较符合Android平台的建议,但是复杂度最大,第三种最不可取。
第一到第三种流程都是差不多,一个是读数据,一个存数据。
读的流程就是先检测自己有没有设置自动登录,如果设置了就登录自己的账户,如果没有设置自动登录或者检测到登录状态不是登录,就去检测另一个应用数据,如果检测到在另一个用户中已经登录了帐号,就直接拿过它的帐号来用。
private void readUserData(){//读完之后要存储一下 SharedPreferences sharedPreferences = getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); isUserOnline = sharedPreferences.getBoolean("isUserOnline", false); Log.e("readSUBData",String.valueOf(isUserOnline) ); if (isUserOnline) { //读取用户数据,不需要去修改数据 } else { try {//存在就读别人的,不存在就读自己的 Context otherContext = createPackageContext("com.bvin.test.sua", Context.CONTEXT_IGNORE_SECURITY); sharedPreferences = otherContext.getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); isUserOnline = sharedPreferences.getBoolean("isUserOnline", false); Log.e("readSUAData",String.valueOf(isUserOnline) ); if (isUserOnline) { //读取用户数据,成共后保存用户数据 saveUserData(); }else{ //弹出需要登录 } } catch (NameNotFoundException e) { //说明这个应用还未安装 // TODO Auto-generated catch block e.printStackTrace(); Log.e("NameNotFoundException", "com.bvin.test.sua找不到"); } } }
存的流程就是,第一次登录插入所有需要的用户数据,以后的自动登录和注销账户就直接更新登录状态。
private void saveUserData(){ SharedPreferences sharedPreferences = getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); Editor editor = sharedPreferences.edit(); //editor.putString("isUserOnline", isUserOnline==true?"已登录":"未登录"); Log.e("saveUserData",String.valueOf(isUserOnline) ); editor.putBoolean("isUserOnline", isUserOnline); editor.commit(); }
下面直接给出第一种方案的Demo:
.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" /> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/textView1" android:layout_alignParentBottom="true" android:layout_marginBottom="26dp" android:layout_marginLeft="15dp" android:onClick="loginIn" android:text="登录" /> <Button android:id="@+id/button2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/button1" android:layout_alignBottom="@+id/button1" android:layout_alignParentRight="true" android:onClick="loginOut" android:layout_marginRight="40dp" android:text="注销" /> </RelativeLayout>
.java
public class MainActivity extends Activity { private String SHARE_USER_TAG = "userinfo"; private boolean isUserOnline; private TextView tvUserInfo; private Button btLogin,btLoginOut; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); readUserData(); tvUserInfo = (TextView) findViewById(R.id.textView1); btLogin = (Button) findViewById(R.id.button1); btLoginOut = (Button) findViewById(R.id.button2); if (isUserOnline) { tvUserInfo.setText("已登录"); btLogin.setEnabled(false); btLoginOut.setEnabled(true); } else { tvUserInfo.setText("未登录"); btLogin.setEnabled(true); btLoginOut.setEnabled(false); } } private void saveUserData(){ SharedPreferences sharedPreferences = getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); Editor editor = sharedPreferences.edit(); //editor.putString("isUserOnline", isUserOnline==true?"已登录":"未登录"); Log.e("saveUserData",String.valueOf(isUserOnline) ); editor.putBoolean("isUserOnline", isUserOnline); editor.commit(); } private void readUserData(){//读完之后要存储一下 SharedPreferences sharedPreferences = getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); isUserOnline = sharedPreferences.getBoolean("isUserOnline", false); Log.e("readSUBData",String.valueOf(isUserOnline) ); if (isUserOnline) { //读取用户数据,不需要去修改数据 } else { try {//存在就读别人的,不存在就读自己的 Context otherContext = createPackageContext("com.bvin.test.sua", Context.CONTEXT_IGNORE_SECURITY); sharedPreferences = otherContext.getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); isUserOnline = sharedPreferences.getBoolean("isUserOnline", false); Log.e("readSUAData",String.valueOf(isUserOnline) ); if (isUserOnline) { //读取用户数据,成共后保存用户数据 saveUserData(); }else{ //弹出需要登录 } } catch (NameNotFoundException e) { //说明这个应用还未安装 // TODO Auto-generated catch block e.printStackTrace(); Log.e("NameNotFoundException", "com.bvin.test.sua找不到"); } } } public void loginIn(View v){ isUserOnline = true; tvUserInfo.setText("已登录"); btLogin.setEnabled(false); btLoginOut.setEnabled(true); saveUserData(); } public void loginOut(View v){ isUserOnline = false; tvUserInfo.setText("未登录"); btLogin.setEnabled(true); btLoginOut.setEnabled(false); saveUserData(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
还有一份第二种方案的Demo
这是一个操作ContentProvider的帮助类,定义操作句柄CONTENT_URI,任何操作都关联到哪个URL这里就是指定到了哪个用户表
public class UserContentHelper { public static final String AUTHORITY = "com.bvin.app.share.UserContentProvider";//房上房域名 public static final Uri USER_LOGIN_URI = Uri.parse("content://" + AUTHORITY+ "/" + Paths.USER_TABLE); private static final class Paths { private static final String USER_TABLE = "user_login";//自动登录表 } public static final class Columns { public static final String USER_ID = "uid";//用户id,注册帐号[text] public static final String USER_PW = "password";//密码 [text] public static final String EMPLOYEE_ID = "eid";//员工Id[long]>0?公司员工:外部注册&&离职-1 public static final String USER_NAME = "name";//用户姓名[text]: public static final String IS_ONLINE = "is_online";//是否在线[boolean],登录成功为true,登录失败|尚未登录为false public static final String DEVICE_UNICODE_ANDROID = "device_imei";//设备唯一码[Android]:手机Imei[text] public static final String CUSTOM_FIELDS = "custom_fields";//自定义字段[text] } public static final class ShareKey { public static final String USER_ID = "AccNumber";//工号 public static final String USER_PW = "AccPassword";//密码 public static final String EMPLOYEE_ID = "EmId";//员工Id //public static final String USER_NAME = "name";//用户姓名 public static final String IS_ONLINE = "IsOnline";//是否在线[boolean] public static final String DEVICE_UNICODE_ANDROID = "DeviceIMEI";//设备唯一码[Android] //public static final String CUSTOM_FIELDS = "custom_fields";//自定义字段 } public static ContentValues getContentValues(ShareUser item) { if (item!=null&&!TextUtils.isEmpty(item.getAccNumber())) { ContentValues values = new ContentValues(); values.put(Columns.USER_ID, item.getAccNumber()); values.put(Columns.USER_PW, item.getAccPassword()); values.put(Columns.EMPLOYEE_ID, item.getEmId()); values.put(Columns.IS_ONLINE, item.isOnline()); values.put(Columns.DEVICE_UNICODE_ANDROID, item.getDeviceIMEI()); return values; }else{ return null; } } /** * Save user data. * 第一次登录的时候,插入用户数据 * @param uif the uif */ public static void saveUserData(Context ctx,ShareUser uif){ if(uif!=null){ ContentValues values = getContentValues(uif); if(values!=null&&values.containsKey("uid")){ //更新当前用户的数据 //ctx.getContentResolver().update(UserContentProvider.USER_LOGIN_URI, values, "uid = "+ values.getAsString("uid"), null); ctx.getContentResolver().insert(USER_LOGIN_URI, values); } } } /** * Update user login. * 登录的时候更新 * @param ctx the ctx * @param uid the uid */ public static void updateUserLogin(Context ctx,String uid){ updateUserOnlineState(ctx,uid,true); } /** * Update user logout. * 注销该用户 * @param ctx the ctx * @param uid the uid */ public static void updateUserLogout(Context ctx,String uid){ updateUserOnlineState(ctx,uid,false); } private static void updateUserOnlineState(Context ctx,String uid,boolean isOnline){ if(!TextUtils.isEmpty(uid)){ ContentValues values = new ContentValues(); values.put(Columns.IS_ONLINE, isOnline); ctx.getContentResolver().update(USER_LOGIN_URI,values, "uid = "+uid, null); } } /** * Gets the target user. * 获取目标用户数据,按道理只会有一个 * @param ctx the ctx * @param uid the uid * @param eid the eid * @return the target user */ public static ShareUser getTargetUser(Context ctx,String uid,String eid){ String selection = ""; if (TextUtils.isEmpty(uid)&&!TextUtils.isEmpty(eid)) {//用户名为空 selection = "eid = "+eid; } else if (TextUtils.isEmpty(eid)&&!TextUtils.isEmpty(uid)){//不知道员工id selection = "uid = "+uid; }else if(!TextUtils.isEmpty(eid)&&!TextUtils.isEmpty(uid)){//两个都写了 selection = "eid = "+eid+" and uid = "+uid; }else{//都没写 try { throw new Exception("the query must input at least one parameter"); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } Cursor cursor = ctx.getContentResolver().query(//查出公司内部员工已经登录 USER_LOGIN_URI, null, selection, null, null); if (cursor!=null&&cursor.getCount()==1) { ContentValues data = new ContentValues(); data.put(ShareKey.USER_ID, cursor.getString(cursor.getColumnIndex(Columns.USER_ID))); data.put(ShareKey.USER_PW, cursor.getString(cursor.getColumnIndex(Columns.USER_PW))); data.put(ShareKey.EMPLOYEE_ID, cursor.getString(cursor.getColumnIndex(Columns.EMPLOYEE_ID))); data.put(ShareKey.IS_ONLINE, cursor.getInt(cursor.getColumnIndex(Columns.IS_ONLINE))==1?true:false); data.put(ShareKey.DEVICE_UNICODE_ANDROID, cursor.getString(cursor.getColumnIndex(Columns.DEVICE_UNICODE_ANDROID))); return createShareUser(data); } else { cursor.close(); return null; } } /** * Gets the online user. * 获取所有在线的用户 * @param ctx the ctx * @return the online user */ public static List<ShareUser> getOnlineUser(Context ctx){ String selection = "is_online = 1"; Cursor cursor = ctx.getContentResolver().query(//查出公司内部员工已经登录 USER_LOGIN_URI, null, selection, null, null); return getUserFromCursor(cursor); } /** * Gets the company user. * 获取在手机上登录过的内部用户数据 * @param ctx the ctx * @return the company user */ public static List<ShareUser> getCompanyUser(Context ctx){ String selection = "eid > 0"; Cursor cursor = ctx.getContentResolver().query(//查出公司内部员工已经登录 USER_LOGIN_URI, null, selection, null, null); return getUserFromCursor(cursor); } /** * Gets the company online user. * 获取公司在线的用户,按道理讲用户只能是一个 * @param ctx the ctx * @return the company online user */ public static List<ShareUser> getCompanyOnlineUser(Context ctx){ String selection = "eid > 0 and is_online = 1"; Cursor cursor = ctx.getContentResolver().query(//查出公司内部员工已经登录 USER_LOGIN_URI, null, selection, null, null); return getUserFromCursor(cursor); } /** * Gets the user from cursor. * 从游标获取用户数据 * @param cursor the cursor * @return the user from cursor */ private static List<ShareUser> getUserFromCursor(Cursor cursor){ if (cursor!=null) { List<ShareUser> users = new ArrayList<ShareUser>(); while (cursor.moveToNext()) { //simpleUser.setAccNumber( cursor.getString(cursor.getColumnIndex("uid"))); //simpleUser.setAccPassword(cursor.getString(cursor.getColumnIndex("password"))); ContentValues data = new ContentValues(); data.put(ShareKey.USER_ID, cursor.getString(cursor.getColumnIndex(Columns.USER_ID))); data.put(ShareKey.USER_PW, cursor.getString(cursor.getColumnIndex(Columns.USER_PW))); data.put(ShareKey.EMPLOYEE_ID, cursor.getString(cursor.getColumnIndex(Columns.EMPLOYEE_ID))); data.put(ShareKey.IS_ONLINE, cursor.getInt(cursor.getColumnIndex(Columns.IS_ONLINE))==1?true:false); data.put(ShareKey.DEVICE_UNICODE_ANDROID, cursor.getString(cursor.getColumnIndex(Columns.DEVICE_UNICODE_ANDROID))); users.add(createShareUser(data)); } cursor.close(); return users; } else { return null; } } private static ShareUser createShareUser(final ContentValues data){ return new ShareUser(){ @Override public String getAccNumber() { // TODO Auto-generated method stub return data.getAsString(ShareKey.USER_ID); } @Override public String getAccPassword() { // TODO Auto-generated method stub return data.getAsString(ShareKey.USER_PW); } @Override public String getEmId() { // TODO Auto-generated method stub return data.getAsString(ShareKey.EMPLOYEE_ID); } @Override public String getDeviceIMEI() { // TODO Auto-generated method stub return data.getAsString(ShareKey.DEVICE_UNICODE_ANDROID); } @Override public boolean isOnline() { // TODO Auto-generated method stub return data.getAsBoolean(ShareKey.IS_ONLINE); } }; } public interface ShareUser { public String getAccNumber();//工号 public String getAccPassword();//密码 public String getEmId();//员工id public String getDeviceIMEI();//IMEI public boolean isOnline();//是否在线,也就是判断是否登录还是登录失败还是未登录 } }
然后给出Activity
public class MainActivity extends Activity implements UserStateOpera{ private String SHARE_USER_TAG = "userinfo"; private TextView tvUserInfo; private Button btLogin,btLoginOut; private ShareUser curUser = null; private ShareUser newlyUser = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); readUserData(); tvUserInfo = (TextView) findViewById(R.id.textView1); btLogin = (Button) findViewById(R.id.button1); btLoginOut = (Button) findViewById(R.id.button2); if (curUser!=null&&curUser.isOnline()) { tvUserInfo.setText("已登录"); btLogin.setEnabled(false); btLoginOut.setEnabled(true); } else { tvUserInfo.setText("未登录"); btLogin.setEnabled(true); btLoginOut.setEnabled(false); } } /*private void saveUserData(){ SharedPreferences sharedPreferences = getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); Editor editor = sharedPreferences.edit(); //editor.putString("isUserOnline", isUserOnline==true?"已登录":"未登录"); Log.e("saveUserData",String.valueOf(isUserOnline) ); editor.putBoolean("isUserOnline", isUserOnline); editor.commit(); }*/ /*private void readUserData(){//读完之后要存储一下 SharedPreferences sharedPreferences = getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); isUserOnline = sharedPreferences.getBoolean("isUserOnline", false); Log.e("readSUBData",String.valueOf(isUserOnline) ); if (isUserOnline) { //读取用户数据,不需要去修改数据 } else { try {//存在就读别人的,不存在就读自己的 Context otherContext = createPackageContext("com.bvin.test.sua", Context.CONTEXT_IGNORE_SECURITY); sharedPreferences = otherContext.getSharedPreferences(SHARE_USER_TAG, MODE_WORLD_READABLE); isUserOnline = sharedPreferences.getBoolean("isUserOnline", false); Log.e("readSUAData",String.valueOf(isUserOnline) ); if (isUserOnline) { //读取用户数据,成共后保存用户数据 saveUserData(); }else{ //弹出需要登录 } } catch (NameNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); Log.e("NameNotFoundException", "com.bvin.test.sua找不到"); } } }*/ public void loginIn(View v){ UserContentHelper.updateUserLogin(this, curUser.getAccNumber()); tvUserInfo.setText("已登录"); btLogin.setEnabled(false); btLoginOut.setEnabled(true); saveUserData(); } public void loginOut(View v){ UserContentHelper.updateUserLogout(this, curUser.getAccNumber()); tvUserInfo.setText("未登录"); btLogin.setEnabled(true); btLoginOut.setEnabled(false); saveUserData(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public void saveUserData() { // TODO Auto-generated method stub if(newlyUser!=null){//保存新用户,主应用没有的用户数据 UserContentHelper.saveUserData(this, newlyUser); } } @Override public void readUserData() { // TODO Auto-generated method stub List<ShareUser> userList = UserContentHelper.getOnlineUser(this); if (userList!=null&&userList.get(0)!=null) {//只能抽取第一个吧 curUser = userList.get(0); }else{//没有就跳到登录界面,登录成功后插入数据 } } }
就到这里吧,ContentProvider和DB操作就不贴了,欢迎各位大神的指导。。。。!