Android App 安全登录认证解决方案



 

基于Android App 安全登录认证解决方案

近几年移动互联网的高速发展,智能手机的使用用户呈现爆炸性增长,手机终端上的App 种类繁多,大多数App 都需要与后台系统进行交互,交互的第一步需要进行登录认证,过于简单的认证方式可能被破解从而造成用户信息的泄露甚至威胁着用户的财产安全。为此基于Android 系统,对比现有几种常见的App 登录认证方式,并提出一种采用RSA 非对称加密和加入Token 时效机制的登录认证解决方案。在登录验证阶段采用RSA 非对称加密方式,App 端对服务器端返回的Token 信息加上时间戳,将处理后的Token 信息保存到本地,后面的每次请求都携带该Token 从而实现免登录的登录状态的保持。

1. 登录认证方式

   1.1 Web登录认证方式

   目前常见的Web认证的方式主要有三种:

   (1)HTTP Basic Auth。 这种方式就是每次请求服务器时都携带用户名和密码,优点是使用非常简单,缺点也非常明显,因为每次都需要携带用户名和密码,很有可能造成密码被截获。

   (2)OAuth。一种开放的授权标准,允许第三方应用访问用户在某一个服务商服务器上存储的私密数据,其处理流程先是第三方应用通过App Key和App secret换取OAuth_Token进行授权(此时颜色有可能需要输入用户名和密码),授权完成后服务商页面会跳转到第三方应用同时返回Access Token,此后第三方应用就可以通过这个Access Token去服务商服务器中访问相应数据。

    (3)Cookie Auth。Cookie认证机制就是浏览器在发起一次登录认证请求时,服务端验证通过后将会在产生一段Cookie信息并返回给浏览器,浏览器会将其保存到本地,以后的每次请求都会使用该    Cookie信息而不再进行登录验证。

    1.2 App登录认证方式

    由于App客户端无法处理Cookie信息,因此App登录认证无法使用Web认证方式中的Cookie认证方式,为了登录状态的保持,一般会模拟Cookie认证方式,即在App端发起登录认证请求后,得到服务端验证成功的确认之后,App端一般会保存一些状态信息在本地,后面每次请求都是携带该状态信息,根据状态信息的不同,可以分为如下两种:

   (1) 保存用户信息表中的某个唯一标识。App端发起登录请求,服务器端在验证成功之后一般会将该登录用户的信息返回给客户端,客户端此时可以将用户信息中的某个唯一标识字段给保存下来,如使用SharedPreference进行保存,后面每次发起网络请求时,先判断本地是否存在该字段,如果不存在说明用户没有进行登录认证,跳转到登录页;如果存在,则直接将这个字段携带进请求信息中,从而实现登录保持状态。这种方式优点是比较简单,缺点就是如果保存的字段容易被别人截获,缺乏安全性。

    (2)保存Token信息。App中非常常用的一种登录认证方式,他的实现过程是,由App端发起登录请求,服务器端在验证成功后生成一份Token信息保存到用户表中并设置一定的时效,同时将此Token返回给App端,App端将此Token保存到本地,以后的每次发起请求都是用该Token。与前面一种方式相比,避免了用户表中信息的泄露,相对更加安全。其流程图如下:


   


这种方式相对于第一种来说更加安全,但还是存在着明显的安全漏洞,需要进行优化。本文将以这种方案为基础,提出一种更加安全的基于Android平台的App登录解决方案。这里我们把现有的这种方案成为Token认证机制,本文提出的方案成为改进的Token认证机制。

2. 改进的Token认证机制详细设计

上述Token认证机制也是存在着一些明显的安全漏洞,本文提出的改进的Token认证机制就是基于对原来Token认证机制中安全漏洞的优化。对于登录认证机制,我们可以把它分为登录验证,状态保持和登出三个阶段,改进的Token认证机制主要是在登录验证和状态保持阶段进行优化。

   2.1 登录验证优化

      登录验证阶段是指App客户端向服务器端发起登录认证请求,并携带用户名和密码,服务器端收到请求后获取用户名和密码,并向数据库进行查询验证的阶段。由于这一阶段需要密码的传输,很多情况下可能都是明文或者简单的MD5加密后直接传输,一旦被黑客截获可能造成密码的泄露风险。因此这一阶段的优化就是加强密码加密功能,这里我们采用RSA非对称加密方式。

     RSA是一种非对称加密,它是对称加密的一种加强版,使用对称加密的服务器端和客户端都使用同一种加密规则,因此在服务器端生成加密密钥之后需要传递给客户端,客户端也需要保存这个密钥,而传递密钥的过程和保存密钥在客户端后都有可能发生密钥的截获造成安全漏洞。而非对称加密方式会在服务器端生成两套密钥,生成的公钥是公开的并传给客户端,私钥保存在服务器端,客户端用公钥加密信息后传递到服务器端,服务器端再用私钥进行解密,因此只要私钥不泄露,通信就是安全的。

    由上面的分析可以知道,要使用RSA加密方式先要让服务器生成公钥和私钥,并将公钥返回给客户端,因此,在图1 的登录验证阶段需要额外进行一次请求获取公钥(这个过程只需要一次,只要获得了公钥以后登录认证就不再需要该过程),其流程如图2所示。



       根据上面的分析, 可以看到这一阶段最核心的是服务器端公钥和密钥的生成过程以及利用公钥加密和利用私钥解密的过程。其核心代码如下:

  1. /** 
  2.     * 初始化密钥 
  3.     * 
  4.     * @return 
  5.     * @throws Exception 
  6.     */  
  7.    public static Map<String, Object> initKey() throws Exception {  
  8.        KeyPairGenerator keyPairGen = KeyPairGenerator  
  9.                .getInstance(KEY_ALGORITHM);  
  10.        keyPairGen.initialize(1024);  
  11.        KeyPair keyPair = keyPairGen.generateKeyPair();  
  12.        // 公钥  
  13.        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();  
  14.        // 私钥  
  15.        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();  
  16.        Map<String, Object> keyMap = new HashMap<String, Object>(2);  
  17.        keyMap.put(PUBLIC_KEY, publicKey);  
  18.        keyMap.put(PRIVATE_KEY, privateKey);  
  19.        return keyMap;  
  20.    }  
 /**
     * 初始化密钥
     *
     * @return
     * @throws Exception
     */
    public static Map<String, Object> initKey() throws Exception {
        KeyPairGenerator keyPairGen = KeyPairGenerator
                .getInstance(KEY_ALGORITHM);
        keyPairGen.initialize(1024);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        // 公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // 私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        Map<String, Object> keyMap = new HashMap<String, Object>(2);
        keyMap.put(PUBLIC_KEY, publicKey);
        keyMap.put(PRIVATE_KEY, privateKey);
        return keyMap;
    }
  1. /** 
  2.     * 取得公钥 
  3.     * 
  4.     * @param keyMap 
  5.     * @return 
  6.     * @throws Exception 
  7.     */  
  8.    public static String getPublicKey(Map<String, Object> keyMap)  
  9.            throws Exception {  
  10.        Key key = (Key) keyMap.get(PUBLIC_KEY);  
  11.        return encryptBASE64(key.getEncoded());  
  12.    }  
 /**
     * 取得公钥
     *
     * @param keyMap
     * @return
     * @throws Exception
     */
    public static String getPublicKey(Map<String, Object> keyMap)
            throws Exception {
        Key key = (Key) keyMap.get(PUBLIC_KEY);
        return encryptBASE64(key.getEncoded());
    }
  1. /** 
  2.     * 取得私钥 
  3.     * 
  4.     * @param keyMap 
  5.     * @return 
  6.     * @throws Exception 
  7.     */  
  8.    public static String getPrivateKey(Map<String, Object> keyMap)  
  9.            throws Exception {  
  10.        Key key = (Key) keyMap.get(PRIVATE_KEY);  
  11.   
  12.        return encryptBASE64(key.getEncoded());  
  13.    }  
 /**
     * 取得私钥
     *
     * @param keyMap
     * @return
     * @throws Exception
     */
    public static String getPrivateKey(Map<String, Object> keyMap)
            throws Exception {
        Key key = (Key) keyMap.get(PRIVATE_KEY);

        return encryptBASE64(key.getEncoded());
    }

  1. /** 
  2.      * 加密<br> 
  3.      * 用公钥加密 
  4.      * 
  5.      * @param data 
  6.      * @param key 
  7.      * @return 
  8.      * @throws Exception 
  9.      */  
  10.     public static byte[] encryptByPublicKey(byte[] data, String key)  
  11.             throws Exception {  
  12.         // 对公钥解密  
  13.         byte[] keyBytes = decryptBASE64(key);  
  14.         // 取得公钥  
  15.         X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);  
  16.         KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);  
  17.         Key publicKey = keyFactory.generatePublic(x509KeySpec);  
  18.         // 对数据加密  
  19.         Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());  
  20.         cipher.init(Cipher.ENCRYPT_MODE, publicKey);  
  21.   
  22.         return cipher.doFinal(data);  
  23.     }  
/**
     * 加密<br>
     * 用公钥加密
     *
     * @param data
     * @param key
     * @return
     * @throws Exception
     */
    public static byte[] encryptByPublicKey(byte[] data, String key)
            throws Exception {
        // 对公钥解密
        byte[] keyBytes = decryptBASE64(key);
        // 取得公钥
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key publicKey = keyFactory.generatePublic(x509KeySpec);
        // 对数据加密
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        return cipher.doFinal(data);
    }

  1. /** 
  2.      * 解密<br> 
  3.      * 用私钥解密 
  4.      * 
  5.      * @param data 
  6.      * @param key 
  7.      * @return 
  8.      * @throws Exception 
  9.      */  
  10.     public static byte[] decryptByPrivateKey(byte[] data, String key)  
  11.             throws Exception {  
  12.         // 对密钥解密  
  13.         byte[] keyBytes = decryptBASE64(key);  
  14.   
  15.         // 取得私钥  
  16.         PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);  
  17.         KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);  
  18.         Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec);  
  19.   
  20.         // 对数据解密  
  21.         Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());  
  22.         cipher.init(Cipher.DECRYPT_MODE, privateKey);  
  23.   
  24.         return cipher.doFinal(data);  
  25.     }  
/**
     * 解密<br>
     * 用私钥解密
     *
     * @param data
     * @param key
     * @return
     * @throws Exception
     */
    public static byte[] decryptByPrivateKey(byte[] data, String key)
            throws Exception {
        // 对密钥解密
        byte[] keyBytes = decryptBASE64(key);

        // 取得私钥
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec);

        // 对数据解密
        Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        return cipher.doFinal(data);
    }

      2.2 状态保持优化

       App 客户端在上一步登录验证阶段会得到服务器返回的Token 信息,并将该Token 保存到本地,以后的每次请求都直接携带该Token 而不是再次使用用户名和密码进行验证,这一阶段称为状态保持或登录保持阶段。前面介绍的Token 认证机制中的Token 会一直保存在App 客户端本地直至用户主动点击退出按钮,如果该Token 被截获,截获者同样可以使用该Token直接访问服务器中的敏感数据。针对Token 这一长时间保存的特点,我们的优化就是为这个Token 设置一个生效时效,具体来说就是在从服务器获得该Token后,在保存的时候在Token 后加上一个当前的时间戳,后面每次发起网络请求时,先取出该Token 后面的时间戳判断有没有超过生效时间,如果没有则将处理后的Token 放入到请求信息中,如果超时了,则重新进行登录认证过程。这种优化过程是以牺牲了一点用户体验为代价。其流程图如下:


       本文章详细探讨了常见的Web 登录认证方式和App登录认证方式, 对现在比较常用的App 登录认证方式Token 认证机制的安全漏洞进行了讨论,在这个基础上提出了改进的Token 认证登录机制, 通过采用RSA 非对称加密优化登录验证阶段, 使用Token 时效机制优化状态保持阶段, 该优化方案在实际生产中得到了检验。同时,该方案还有进一步优化的空间,例如在优化状态保持阶段时采用的是Token 时效机制, 但这样牺牲了App 的使用体验, 因此在以后的研究中可以针对这一阶段做进一步的优化过程。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值