正确姿势在Android项目中集成sqlcipher

先吐槽一下国内很多IT教程的文章,老旧不说太多不负责任的照抄。所以各位看客如果想在android上面使用sqlcipher不妨先去看看github和官方博客,特别是后者,建议在集成之前,先看看官方的博客讲解和说明。


介绍一下选择sqlcipher的背景;因项目需要,用到一个三方处理模块,需要对引用的资源(sqlite)进行数据加密,加密的方案其实很简单,要么直接对数据库本身加密,要么对数据加密后再写入,取出后解密。单从实现对效率来说后者肯定就不如前者。作为有追求的新时代coder肯定不会选用后者。那剩下的就只有数据库本身加密,查了一下资料,sqlite本身也有加密方案,但是需要付费,这个肯定也不用了,那么剩下的就是开源方案了,github你值得拥有。

目前来看sqlcipher 是github上fork 和 star 最多的开源方案,分社区版和付费版。作为有追求的coder当然是选择社区版!废话到此结束,下面开始姿势讲解。


  • the first

还是建议先去看看官方的介绍文档官方博客;sqlcipher 的使用分成两个部分,一、生成sqlcipher支持的加密数据库,二、代码内引入sqlcipher依赖,修改很少的代码,正常的业务流程内的数据库操作。

  • the second

如何生成sqlcipher支持的加密数据库,此处不得不再次吐槽很多不负责任的教程和文档,各种要求你写代码去生成加密数据库的文章,你们真的有去实践吗?

其实,对于上面操作,官方的文档也说的很清楚了,下面是摘要:

基于已有的明文数据库生成一个全新加密数据库:

$ ./sqlcipher plaintext.db
sqlite> ATTACH DATABASE 'encrypted.db' AS encrypted KEY 'testkey';
sqlite> SELECT sqlcipher_export('encrypted');
sqlite> DETACH DATABASE encrypted;

看到这里是不是存在一个疑问, sqlcipher的可执行文件从哪里来?

我是这样做的:

brew install sqlcipher

linux 应该可以这个姿势:(未实践,看客们自己试试)

sudo  apt-get install  sqlcipher

windows自行搜索把。

成功安装之后应该是这个样子的:

Hehr-2:assets hehr$ sqlcipher
SQLCipher version 3.20.1 2017-08-24 16:21:36
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.

所以上面的第一行命令:

./sqlcipher plaintext.db ->  sqlcipher plaintext.db

如果有耐心看到这里,估计各位已经能顺利生成你们的encrypted.db,生成完毕之后可以使用sqlite的图形化浏览工具(推荐db browser),访问数据库试试,应该会提示你输入密码,输入你们上面设置的testkey应该就能看到明文的数据库内容了。

  • the third 剩下就是代码内集成的工作了,此处如果已经写好了一套自己的数据库操作,其实要做的事情很简单,大概4步,如下:

1、代码内引入sqlcipher 依赖,如下:

compile 'net.zetetic:android-database-sqlcipher:3.5.9'

确定版本之前,请一定自己先去github上看看现在最新release的版本是多少,在填写版本。毕竟coder解决问题都是在下一个版本,你我都懂。

2、导入sqlcipher仓库下的SQLiteOpenHelper、SQLiteDatabase等等如下:

import net.sqlcipher.DatabaseErrorHandler;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteOpenHelper;
import net.sqlcipher.Cursor;

这里强调一下,Cursor 必须更换成sqlcipher 包下的,如使用原生的sqlite cursor会有各种莫名奇妙的问题等待你。不要相信网上其它不负责任的教程的文章。

3、在数据库实际操作之前加入如下代码:

SQLiteDatabase.loadLibs(context);

我是在实例的DBHelper的构造方法内引入的,各位看客你们任意。

4、 getReadableDatabase() 、getWritableDatabase() 方法内填入之前设置的数据库访问密钥,比方说这个姿势的代码

sqLiteDatabase = dbHelper.getWritableDatabase(Conf.DB.PWD);`

到此,代码内的集成工作已经全部完成,总结一下。导了4个包,复制了一句代码,改了一个错误提示。

为了满足喜欢拿来主义的coder,我把我这里的数据库操作的代码,贴上来。

public class DBHelper extends SQLiteOpenHelper {

    //数据库版本号
    private static final int DATABASE_VERSION=1;

    public DBHelper (Context context , String dbName)
    {
        this(context,dbName,null,DATABASE_VERSION);
        SQLiteDatabase.loadLibs(context);//加载数据库SO
    }

    public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);

    }

    public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}
 /**
 * COPY FROM  https://blog.csdn.net/qq_16624353/article/details/53728373
 * @param
 */
public class DatabaseManger {

    private static DBHelper dbHelper ;

    private SQLiteDatabase sqLiteDatabase;

    private static DatabaseManger instance = null;

    /**
     *
     * 构造方法上下文
     *
     * @param context
     * @return
     */
    private DatabaseManger(Context context , String dbName)
    {
        dbHelper = new DBHelper(context,dbName);

        sqLiteDatabase = dbHelper.getWritableDatabase(Conf.DB.PWD);

    }


    /**
     *
     * 获取本类对象的实例
     * @param context
     * @return
     */
    public static synchronized DatabaseManger getInstance(Context context,String dbName)
    {
        if (instance == null) {
            if(context == null) {
                throw new RuntimeException("NullContextException");
            }
            instance = new DatabaseManger(context , dbName );
        }

        return instance;
    }

    /**
     * 关闭数据库
     */
    public synchronized void close()
    {
        if(sqLiteDatabase.isOpen())
        {
            sqLiteDatabase.close();
            sqLiteDatabase=null;
        }
        if(dbHelper!=null)
        {
            dbHelper.close();
            dbHelper=null;
        }

        if(instance != null)
        {
            instance = null;
        }

    }

    /**
     * 执行一条sql语句
     *
     */

    public void execSql(String sql)
    {
        if(sqLiteDatabase.isOpen())
        {
            sqLiteDatabase.execSQL(sql);
        }
        else
        {
            throw  new RuntimeException("The DataBase has already closed");
        }
    }

    /**
     * sql执行查询操作的sql语句
     * selectionargs查询条件
     * 返回查询的游标,可对数据进行操作,但是需要自己关闭游标
     */
    public Cursor queryData2Cursor(String sql, String[] selectionArgs)throws Exception
    {
        Cursor cursor = null;
        if(sqLiteDatabase.isOpen())
        {
            cursor = sqLiteDatabase.rawQuery(sql,selectionArgs);
        }else
        {
            throw  new RuntimeException("The DataBase has already closed");
        }
        return cursor;
    }

    /**
     * 查询表中数据总条数
     * 返回表中数据条数
     *
     */

    public int getDataCounts(String table)throws Exception
    {
        Cursor cursor = null;
        int counts = 0;
        if(sqLiteDatabase.isOpen())
        {
            cursor = queryData2Cursor("select * from "+ table,null);
            if(cursor != null && cursor.moveToFirst())
            {
                counts = cursor.getCount();
            }
        }else
        {
            throw  new RuntimeException("The DataBase has already closed");
        }
        return counts;
    }

    /**
     *
     * 消除表中所有数据
     * @param table
     * @throws Exception
     */
    public void clearAllData(String table)throws Exception
    {
        if(sqLiteDatabase.isOpen())
        {
            execSql("delete from "+ table);
        }else
        {
            throw  new RuntimeException("The DataBase has already closed");
        }
    }

    /**
     *
     * 插入数据
     * @param sql 执行操作的sql语句
     * @param bindArgs sql中的参数,参数的位置对于占位符的顺序
     * @return 返回插入对应的额ID,返回0,则插入无效
     * @throws Exception
     */

    public long insertDataBySql(String sql,String[] bindArgs)throws Exception
    {
        long id = 0;
        if(sqLiteDatabase.isOpen())
        {
            SQLiteStatement sqLiteStatement = sqLiteDatabase.compileStatement(sql);
            if(bindArgs != null)
            {
                int size = bindArgs.length;
                for (int i=0; i < size;i++)
                {
                    sqLiteStatement.bindString(i+1,bindArgs[i]);
                }
                id=sqLiteStatement.executeInsert();
                sqLiteStatement.close();
            }
        }else
        {
            throw  new RuntimeException("The DataBase has already closed");
        }
        return id;
    }

    /**
     *
     * 插入数据
     * @param table 表名
     * @param values 数据
     * @return 返回插入的ID,返回0,则插入失败
     * @throws Exception
     */
    public  long insetData(String table, ContentValues values)throws Exception
    {
        long id=0;
        if(sqLiteDatabase.isOpen())
        {
            id=sqLiteDatabase.insertOrThrow(table,null,values);
        }else
        {
            throw  new RuntimeException("The DataBase has already closed");
        }
        return id;
    }

    /**
     *
     * 批量插入数据
     * @param table 表名
     * @param list 数据源
     * @param args 数据键名 key
     * @return
     * @throws Exception
     */
    public long insertBatchData(String table, List> list, String[] args)throws Exception
    {
        long insertNum =0;
        sqLiteDatabase.beginTransaction();
        ContentValues contentValues = new ContentValues();
        for(int i=0; i 0)
            {
                insertNum++;
            }
        }
        sqLiteDatabase.setTransactionSuccessful();
        sqLiteDatabase.endTransaction();
        return insertNum;
    }

    /**
     *
     * 更新数据
     * @param table 表名
     * @param values 需要更新的数据
     * @param whereClaause 表示sql语句中条件部分的语句
     * @param whereArgs 表示占位符的值
     * @return
     * @throws Exception
     */
    public  int updateData(String table,ContentValues values,String whereClaause,String[] whereArgs)throws  Exception
    {
        int rowsNum = 0;
        if(sqLiteDatabase.isOpen())
        {
            rowsNum = sqLiteDatabase.update(table,values,whereClaause,whereArgs);
        }else
        {
            throw  new RuntimeException("The DataBase has already closed");
        }
        return rowsNum;
    }

    /**
     *
     * 删除数据
     * @param sql 待执行的sql语句
     * @param bindArgs sql语句中的参数,参数的顺序对应占位符的顺序
     */
    public void deleteDataBySql(String sql,String[] bindArgs)throws Exception
    {
        if(sqLiteDatabase.isOpen())
        {
            SQLiteStatement statement = sqLiteDatabase.compileStatement(sql);
            if(bindArgs != null)
            {
                int size = bindArgs.length;
                for(int i= 0;i> query2List(String sql,String[] selectionArgs,Object object)throws Exception
    {
        List> list = new ArrayList<>();
        if(sqLiteDatabase.isOpen())
        {
            Cursor cursor = null;
            cursor = queryData2Cursor(sql,selectionArgs);
            Field[] fields;
            HashMap map;
            if(cursor !=null && cursor.getCount()>0)
            {
                while (cursor.moveToNext())
                {
                    map = new HashMap<>();
                    fields = object.getClass().getDeclaredFields();
                    for(int i =0; i< fields.length;i++)
                    {
                        /**
                         * 1通过key,即列名,得到所在的列索引
                         * 2通过所在行以及所在列的索引,得到唯一确定的队友值
                         * 3将值与键封装到MAP集合中,此条数据读取完毕
                         */
                        map.put(fields[i].getName(),cursor.getString(cursor.getColumnIndex(fields[i].getName())));
                    }
                    list.add(map);
                }
                cursor.close();
            }
        }else
        {
            throw new RuntimeException("The database has already closed!");
        }
        return list;
    }
}

关于打包和混淆

在这里提一下,打包尤其是jar包,建议把依赖让外部去做,不要把sqlcipher的代码全部打如你自己的jar包内,原因如下:1、本来一句话就可以搞定的事情,为什么要折腾这么多? 2、如果外部也需要引入sqlcipher呢?

混淆部分我直接贴了:

#KEEP  SQLCHIPHER
-keep class com.xxx.xxx.utils.db.**{*;}
-keep class net.sqlcipher.database.**{*;}
-keep class net.sqlcipher.**{*;}

注意,自己的这俩数据库操作的类不要混淆了,没啥技术含量,也没有那个必要。


以上,祝君成功!

hehr 2018.6.23 00:48

转载于:https://my.oschina.net/u/2278066/blog/1834372

你可能感兴趣的:(正确姿势在Android项目中集成sqlcipher)