注册登录请求中RSA加密,PHP服务器和Android客户端实现
来源:互联网 发布:51单片机蜂鸣器程序 编辑:程序博客网 时间:2024/05/22 16:43
前言
客户端利用Http协议进行注册和登录等操作时,如果不做特殊处理,请求中携带的密码等敏感信息是明文传输的,有可能会被截获。解决这个问题最好的方法当然是使用Https协议,但是Https协议需要像权威机构申请证书才能保证足够的安全性,在没有证书的情况下,可以考虑自己来实现加密解密处理。
我们现在的场景只考虑在Http请求中加密,Http响应中没有敏感信息,暂时不考虑加密。首先考虑下对称加密的方式,这种方式客户端和服务器保存了一份相同的密钥,客户端加密和服务器解密是都需要用到,但是客户端程序有被破解的可能性,密钥有可能被泄露,攻击者拿到密钥再去截获请求报文,就可以解密出敏感信息来。因此最后选择了非对称加密RSA算法,客户端使用公钥加密,服务端再利用私钥解密,这样客户端只需要保存公钥了,即使泄露了也没法用来加密。可以实现敏感信息和密钥都处于相对安全的状态。
Github地址:
服务器:https://github.com/zhongchenyu/jokes-laravel
Android: https://github.com/zhongchenyu/jokes
PHP服务端实现
1. 生成密钥
可以利用 openssl 命令来生成RSA的密钥,一般linux系统都预装了。另外项目用的是 Laravel 框架,可以利用框架生成命令,将创建密钥的命令封装起来。
首先在项目路径下执行命令 php artisan make:command GenerateRSAKey
然后编辑GenerateRSAKey类的handle函数:
public function handle() { // $keyDir = 'sec'; if(!is_dir($keyDir)) mkdir($keyDir); echo getcwd() . "\n"; chdir($keyDir); echo getcwd() . "\n"; //生成原始 RSA私钥文件 rsa_private_key.pem shell_exec('openssl genrsa -out rsa_private_key.pem 1024'); //将原始 RSA私钥转换为 pkcs8格式 shell_exec('openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out private_key.pem'); //生成RSA公钥 rsa_public_key.pem shell_exec('openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem'); chdir('..'); echo getcwd() . "\n"; return; }
执行命令后就会在项目的 sec 目录下生成公钥和私钥文件了。
2. 加密解密方法封装
创建一个RSAUtils类来专门处理加密和解密,实际上服务器只需要利用私钥解密就够了,不过为了以后的可扩展性,把所有的加解密方法都先写了:
class RsaUtils { public static function enPublic($data) { $path = base_path(); $publicKey = openssl_get_publickey(file_get_contents($path.'/sec/rsa_public_key.pem')); openssl_public_encrypt($data,$encrypted,$publicKey); $base64Encoded = base64_encode($encrypted); return $base64Encoded; } public static function dePrivate($data) { $path = base_path(); $privateKey = openssl_get_privatekey(file_get_contents($path.'/sec/rsa_private_key.pem')); openssl_private_decrypt(base64_decode($data), $decrypted, $privateKey); return $decrypted; } public static function enPrivate($data) { $path = base_path(); $privateKey = openssl_get_privatekey(file_get_contents($path.'/sec/rsa_private_key.pem')); openssl_private_encrypt($data, $encrypted, $privateKey); $base64Encoded = base64_encode($encrypted); return $base64Encoded; } public static function dePublic($data) { $path = base_path(); $publicKey = openssl_get_publickey(file_get_contents($path.'/sec/rsa_public_key.pem')); openssl_public_decrypt(base64_decode($data), $decrypted, $publicKey); return $decrypted; }}
以私钥解密的方法dePrivate()来说明下,其余的类似。
$path = base_path();$privateKey = openssl_get_privatekey(file_get_contents($path.'/sec/rsa_private_key.pem'));
首先通过 file_get_contents() 读取密钥文件的内容,这里要注意PHP 的 file_get_contents()参数只能是绝对路径,所以先要获取项目路径,补充到密钥相对路径前面。
openssl_get_privatekey()则从字符串中获取密钥,如果密钥格式不合法,则会出错。
openssl_private_decrypt(base64_decode($data), $decrypted, $privateKey);
考虑网络中的数据经过base64编码的情况,这里首先对$data 进行base64解密,客户端也要配合进行base64编码。
然后用openssl_private_decrypt()
函数将base64解密后的$data, 利用 $privateKey
进行RSA私钥解密,解密后的数据存在$decrypted
中并返回。
3. 测试加解密接口
为了验证下加密解密方法是否可行,写了一个测试用的接口,对请求数据加密再解密:
public function rsaTest(Request $request) { $data = $request->get('data','abcdef'); echo 'base64:'.base64_encode($data).'<br>'; echo $data . '<br>'; echo 'encrypted by public key<br>'; $encrypted = RsaUtils::enPublic($data); echo $encrypted . '<br>'; $decrypted = RsaUtils::dePrivate($encrypted); echo $decrypted . '<br>'; echo 'encrypted by private key<br>'; $encrypted = RsaUtils::enPrivate($data); echo $encrypted . '<br>'; $decrypted = RsaUtils::dePublic($encrypted); echo $decrypted . '<br>'; }
访问结果,可以看出加密解密没有问题:
base64:YWJjZGVmabcdefencrypted by public keyVnW8RNMCoqVzDUuvjwwmNAv4MX5kpbIvCgaPdQNE+xeJO4wBaD7PHpMKv/Ts7ry8ANg3uDnh10owalPQLy4d8RcX3g30z/Npf3RB1zFGtKB06YlN/O6zWgFkBr6X1EBubc7xmsoAeXBPn06/pjBn3xiYHWYlj7U5V7chaC0gh6k=abcdefencrypted by private keyb0KVZcXPOWaZtLpN5MAjV87ikMynEV4j96aehypzJVwSZUTf+gIY4zQf4/KJnxPxleK4sHOKXhiaQ/QdrsSA1tmnKv8YMW7s1iAlreYB9KjfVgNI2agPhYA5pISXPpUv3BByo9wNWc2Ef5/5w6/Hzs30jrp1wwgCbZcu/jkPf8Q=abcdef
4. 加密的注册和登录接口:
只对密码进行加密处理。
注册接口:
public function encryptedRegister(Request $request) { $this->validator($request->all())->validate(); $params = $request->all(); $password = RsaUtils::dePrivate($params['password']); if($password == null) { return response()->json(['message' => 'Encryption error'], 400); } $params['password'] = $password; $user = $this->create($params); $token = JWTAuth::fromUser($user); return ["token" => $token]; }
登录接口:
public function encryptedAuthenticate(Request $request) { $credentials['email'] = $request->get('email'); $credentials['password'] = RsaUtils::dePrivate( $request->get('password')); try { // attempt to verify the credentials and create a token for the user if (! $token = JWTAuth::attempt($credentials)) { return response()->json(['error' => 'invalid_credentials'], 401); } } catch (JWTException $e) { // something went wrong whilst attempting to encode the token return response()->json(['error' => 'could_not_create_token'], 500); } $user = User::where('email', $credentials['email'])->first(); $userTransform = new UserTransformer(); return ['user'=> $userTransform->transform($user), 'token' => $token]; }
关键的一步,向将请求参数中的 password 进行解密: $password = RsaUtils::dePrivate($params['password']);
解密之后的处理和没有加密机制时基本一致。
注册时要注意增加判断,如果$password
没有经过正确的加密,这是解密返回的是null,必须返回错误:
if($password == null) { return response()->json(['message' => 'Encryption error'], 400); }
否则可能会创建一个密码为 null 的用户,以后凡是用任何一未正确加密的密码都可以登录了。
5. 接口测试用例
因为项目托管在 Github 上,可以对接 Travis 进行自动化测试。
首先编辑.travis.yml 增加一条生成密钥的命令- php artisan rsa:generate
:
language: phpphp: - 7.1service: - mysqlbefore_script: - composer install - composer dump-autoload - cp .env.travis .env - php artisan jwt:generate - php artisan key:generate - php artisan vendor:publish - mysql -e 'CREATE DATABASE IF NOT EXISTS jokes ;' - php artisan migrate - php artisan rsa:generatescript: phpunit
测试用例:
class EncryptedLoginTest extends TestCase { use DatabaseTransactions; public function testEncryptedLogin() { $user = User::create([ 'name' => 'TestUser12345', 'email' => 'TestUser12345@TestUser.com', 'password' => bcrypt('123456'), ]); $userId = $user->id; $encryptedPassword = RsaUtils::enPublic('123456'); $response = $this->json('POST', '/api/encrypted_login', [ 'name' => 'TestUser12345', 'email' => 'TestUser12345@TestUser.com', 'password' => $encryptedPassword ]); $response->assertStatus(200)->assertJsonStructure(['token']) ->assertJson(['user' => ['id'=>$userId, 'name'=>'TestUser12345', 'email' =>'TestUser12345@TestUser.com']]); }}
这样执行 git push
到Github后将自动执行环境部署和测试,通过:
$ phpunitPHPUnit 6.4.4 by Sebastian Bergmann and contributors............. 12 / 12 (100%)Time: 3.08 seconds, Memory: 28.00MBOK (12 tests, 22 assertions)Generating code coverage report in Clover XML format ... doneThe command "phpunit" exited with 0.Done. Your build exited with 0.
服务器搞定,下面开始客户端编码。
Android 客户端
1. 加解密工具代码
密钥获取:
private static PublicKey getPublicKey(String publicKeyString) throws Exception{ byte[] keyBytes = Base64.decode(publicKeyString.getBytes(), Base64.DEFAULT); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePublic(keySpec); } private static PrivateKey getPrivateKey(String privateKey) throws Exception { byte[] keyBytes = Base64.decode(privateKey.getBytes(), Base64.DEFAULT); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePrivate(keySpec); }
加密和解密:
private static byte[] encrypted(byte[] content, PublicKey publicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return cipher.doFinal(content); } private static byte[] decrypt(byte[] content, PrivateKey privateKey) throws Exception{ Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); return cipher.doFinal(content); }
最终要用到的公开接口,利用公钥加密,并进行base64编码:
public static String base64Encrypted(String data) { byte[] encryptedBytes = {}; try { PublicKey publicKey=getPublicKey(publicKeyString); encryptedBytes = encrypted(data.getBytes(), publicKey); } catch (Exception e) { e.printStackTrace(); } return Base64.encodeToString(encryptedBytes, Base64.NO_WRAP); }
这里讲下中间遇到的几个坑,要注意下。客户端和服务器,一个加密一个解密,要能对接起来,除了要求用相同的算法,配套的公钥密钥,编码方式等,还要注意算法和编码中用到的一些细节参数,否则可能出现客户端加密,发送到服务端无法解密的情况。主要有这些:
Base64编码换行的问题
PHP里用base64_decode($data)
和 base64_encode($data)
进行Base64编解码,不需要额外参数。
Java中可以使用 java.util.Base64
库的 Base64.getEncoder().encode(data)
和 Base64.getDecoder().decode(data)
进行编解码,也不需要额外参数,并且是可以和PHP对接的,即一方编码,另一方解密没有问题。
Android虽然用的是Java 语言,但是却没有java.util.Base64
库,但是有专门的 android.util.Base64
库,使用函数 Base64.encode(byte[] bytes, int flag)
,有一个参数flag,使用默认的flag:Base64.DEFAULT
是无法和PHP的base64对接的,必须用Base64.NO_WRAP
,因为 Android 的默认方式,会在编码过长是添加换行符,而PHP和Java则不会,因此需要使用Base64.NO_WRAP。
RSA分组和填充方式
PHP中用 openssl_private_decrypt($data, $decrypted, $privateKey)
不需要额外参数, Java中RSA
默认就是RSA/ECB/PKCS1Padding
,但Android的话虽然用的和Java是一个库,但是必须制定RSA/ECB/PKCS1Padding
:
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
其中RSA是加密算法,RSA是块算法,可以有不同的分组方式,ECB是其中最简单的一种,PKCS1Padding则是填充方式,这里需要指定。
2. 请求处理
public void register(String name, String email, String password) { mName = name; mEmail = email; mPassword = RSAUtil.base64Encrypted(password); start(REGISTER); } public void login(String email, String password) { mEmail = email; mPassword = RSAUtil.base64Encrypted(password); start(LOGIN); }
发送注册和登录前先将password加密,其余处理和不加密一样。
看下登录log,password被加密了:
11-26 17:04:21.891 4103-4185/chenyu.jokes D/OkHttp: email=zcy.gr%40qq.com&password=yJppgVBAbnG6yt8gIcTAKQ%2FRP7MdBU4Pg21b8V9KIys%2B6c8mJAUSAsBDu6bjqKLG****************
- 注册登录请求中RSA加密,PHP服务器和Android客户端实现
- RSA加密登录请求
- java和php实现RSA加密互通
- Android RSA加密解密,用于和服务器交互时的请求
- 自学php,用php服务端和swift客户端实现验证登录和注册功能 1
- android RSA 加密实现
- 利用简易Tomcat服务器结合MysqL实现Android手机注册与登录(客户端部分)
- android(客户端)和PC(服务器端)通信RSA 加密解密
- SVN客户端界面完工+与服务器交互注册登录实现
- Android PHP JSON 登录注册功能实现
- Android中使用 SQLite 创建数据库实现登录和注册
- php实现登录和注册功能
- Android 中 非对称(RSA)加密和对称(AES)加密
- Android 中 非对称(RSA)加密和对称(AES)加密
- Android开发简单登录服务器,客户端实现登录服务器
- PHP实现注册登录
- 解决Android和PHP通信RSA加密问题
- Post请求登录笔记(服务器和客户端示例源码)
- Android搜索框存储搜索记录
- 剑指offer---用两个栈实现队列(7)
- Leedcode 算法习题 第十一周
- php读取网络文件 curl, fsockopen ,file_get_contents 几个方法的效率对比
- STM32系统学习——USART(串口通信)
- 注册登录请求中RSA加密,PHP服务器和Android客户端实现
- CentOS 7 上安装vim(默认未安装)
- java实现定时任务的三种方法
- DevExpress控件使用API
- Java Email的发送工具类及相关530问题解决
- POJ 3176.Cow Bowling
- JAVA main方法的格式讲解
- Springboot实现热部署
- FPN(Linux下的实验和Windows平台的移植)