游戏社区App (三):客户端与服务端的加密处理 和 登录

http请求数据无论是GET或者POST都可能会被抓包获取到数据。为了避免用户的敏感数据被窃取(比如密码),需要对数据进行加密处理。

一、相关名词解析

RSA:非对称加密。
会产生公钥和私钥,公钥在客户端,私钥在服务端。公钥用于加密,私钥用于解密。
优势在于 不需要共享私钥,避免了私钥泄露的风险。
劣势在于 加密效率低,数据量大时耗时也大。

AES:对称加密。
客服端和服务器端都使用同一个秘钥来进行加密和解密。
优势在于 加密效率高
缺点在于 秘钥需要共享给客户端,具有泄露的风险

MD5:MD5信息摘要算法
只能加密,不能解密。

Base64编码:对字节数组转换成字符串的一种编码方式。

二、基本逻辑

整体流程:客户端获得数据–>对数据进行加密–>上传加密数据–>服务器获得数据–>对数据进行解密
需要考虑:
1、密码不能明文传输,即需要对明文进行加密。
2、参数不能被中途截取修改,即需要对参数进行验证。

三、对明文进行加密—Cipher

javax.crypto.Cipher 类中提供了对明文的加密和解密功能。

Cipher可以使用RSA加密方式,即非对称加密,所以需要生成公钥与私钥。生成的公钥放到客户端中用于加密,私钥放在服务器上用于解密。
生成公钥和私钥 示例代码:
提示:如果出现找不到Base64.encode方法时,你可能需要引入com.sun.jersey.jersey-core.jar包(提取码:4lvj )

/**
* 生成公和私钥
* */
public static void generater() {
    try {
        java.security.KeyPairGenerator keygen = java.security.KeyPairGenerator.getInstance("RSA");
        SecureRandom secrand = new SecureRandom();
        secrand.setSeed("ye23".getBytes()); // 初始化随机产生器
        keygen.initialize(1024, secrand);
        KeyPair keys = keygen.genKeyPair();

        PublicKey pubkey = keys.getPublic();
        PrivateKey prikey = keys.getPrivate();

        String pubkeySrt = new String(Base64.encode(pubkey.getEncoded()));
        String prikeySrt = new String(Base64.encode(prikey.getEncoded()));

        LocalLogUtils.d("pubKey = " + pubkeySrt,true); //公钥
        LocalLogUtils.d("priKey = " + prikeySrt,true); //私钥
    } catch (java.lang.Exception e) {
        System.out.println("生成密钥对失败");
        e.printStackTrace();
    }
}

客户端明文加密 示例代码:

private static final String ALGORITHM = "RSA"; //所使用算法
private static final String PublicKey = Info.loginPublicKey; //公钥

public static String encrypt(String content) {
    try {
        PublicKey pubkey = getPublicKeyFromX509(ALGORITHM, PublicKey);
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");//生成Cipher对象
        cipher.init(Cipher.ENCRYPT_MODE, pubkey);//操作模式为加密(Cipher.ENCRYPT_MODE),pubkey为公钥
        byte plaintext[] = content.getBytes("UTF-8");
        byte[] output = cipher.doFinal(plaintext);//得到加密后的字节数组
        String s = new String(android.util.Base64.encode(output, android.util.Base64.DEFAULT));
        return s;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

private static PublicKey getPublicKeyFromX509(String algorithm, String bysKey)
        throws NoSuchAlgorithmException, Exception {
    byte[] decodedKey = android.util.Base64.decode(bysKey, android.util.Base64.DEFAULT);
    X509EncodedKeySpec x509 = new X509EncodedKeySpec(decodedKey);
    KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
    return keyFactory.generatePublic(x509);
}

服务端解密 示例代码:

private static final String PrivateKey = Info.loginPrivateKey; //私钥
public static String reEncrypt(String encryptStr) {
    try {
        PrivateKey privateKey = getPrivateKey(PrivateKey);
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] output =  cipher.doFinal(android.util.Base64.decode(encryptStr, android.util.Base64.DEFAULT));
        return new String(output);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

private static PrivateKey getPrivateKey(String priKey) throws NoSuchAlgorithmException,
        InvalidKeySpecException, NoSuchProviderException {
    byte[] keyBytes = android.util.Base64.decode(priKey, android.util.Base64.DEFAULT);
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
    return privateKey;
}

四、对参数进行验证–请求参数的签名sign

避免请求参数被修改,保证了请求数据的一致性。需要客户端和服务端约定一个签名生成算法。
客户端在请求接口之前调用签名算法,根据参数生成sign值。然后把sign和请求参数一并传给服务器。
服务器收到到参数和签名之后,使用同样的签名算法和参数 生成sign,并比对两个sign是否一致。如果一致就说明参数未被修改。
签名算法示例代码:

/**
* 使用MD5加密
* 对键进行升值排序
* 把生成的md5统一转成大写
*/
public static String md5Decode32(Map<String, String> data, String time) {
    String key=Info.md5secretkey; // key,和后台保持一致
    String content = "";
    for (String keyname:data.keySet()) {
        content=content+keyname+ data.get(keyname);
    }
    content=content+time+ key; //注意:time一定要传进来,不可以在方法中直接生成。不然时间是对不上的


    byte[] hash;
    try {
        hash = MessageDigest.getInstance("MD5").digest(content.getBytes("UTF-8"));
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("NoSuchAlgorithmException",e);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException("UnsupportedEncodingException", e);
    }
    //对生成的16字节数组进行补零操作
    StringBuilder hex = new StringBuilder(hash.length * 2);
    for (byte b : hash) {
        if ((b & 0xFF) < 0x10){
            hex.append("0");
        }
        hex.append(Integer.toHexString(b & 0xFF));
    }
    return hex.toString().toUpperCase();//返回纯大写的MD5
}

五、登录

登录可以分为账密登录和自动登录。
第一次账密登录成功之后,后台可以生成并返回一个token。后续的一段时间内,可以直接通过token自动登录。免去输入账号密码的麻烦。
账号登录示例代码(客户端):

@OnClick(R2.id.btn_sign_in)
void onClickSignIn(){
    if(checkForm()){ //检查账密格式是否正确
        //使用Cipher对密码进行加密
        String encryptPassword;
        encryptPassword = CipherUtil.encrypt(password);

        //生成MD5 sign,避免请求参数被恶意修改。保证了请求数据的一致性
        Map<String, String> MD5_Parameter_Map = new TreeMap<String, String>(); //TreeMap为有序集合,默认是升序
        MD5_Parameter_Map.put("userName", name);
        MD5_Parameter_Map.put("userPassword", encryptPassword);
        String time = System.currentTimeMillis()+"";
        String sign = MD5Sign.md5Decode32(MD5_Parameter_Map,time);

        //上传数据
        RestClient.builder()
                .url(AppInfo.API_LOGIN)
                .params("userName",name)
                .params("userPassword",encryptPassword)
                .params("signTime",time) //要把时间也传上后台,不能直接在后台生成时间,不然生成的sign不一致
                .params("sign",sign)
                .success(new ISuccess() {
                    @Override
                    public void onSuccess(String response) {
                    }
                })
                .failure(new IFailure() {
                    @Override
                    public void onFailure(String msg) {
                    }
                })
                .error(new IError() {
                    @Override
                    public void onError(int code, String msg) {
                    }
                })
                .build()
                .post();
    }
}

账号登录示例代码(服务端):

@RequestMapping("/login")
public ApiResult login(String userName,String userPassword,String signTime,String sign) {
    int code = 0;
    String status = "unknown error";
    UserDao data = new UserDao();
    try {
        //生成sign对请求数据的一致性进行校验
        Map<String, String> MD5_Parameter_Map = new TreeMap<String, String>();
        MD5_Parameter_Map.put("userName", userName);
        MD5_Parameter_Map.put("userPassword", userPassword);
        String checkSign = MD5SignUtil.md5Decode32(MD5_Parameter_Map,signTime);
        //校验不通过直接返回错误
        if(!checkSign.equals(sign)){
            ApiResult apiResult = new ApiResult();
            apiResult.setCode(ApiCallback.SignIsError);
            apiResult.setStatus(status);
            return apiResult;
        }

        //判断sign是否超过有效时间
        long time= System.currentTimeMillis();
        long signTimeToLong = Long.parseLong(signTime);
        long interval = Math.abs(time-signTimeToLong);
        if(interval > validityTime){
            ApiResult apiResult = new ApiResult();
            apiResult.setCode(ApiCallback.SignIsTimeout);
            apiResult.setStatus("Sign is timeout");
            return apiResult;
        }

        //调用Cipher对密码进行解密
        userPassword = CipherUtil.reEncrypt(userPassword);

        UserController.getInstance().init(userRepository);

        if(userName.isEmpty()){ //userName不能为空
            code = ApiCallback.InputUserIsNull;
            status = "User name can not be null";
        }else if(userPassword.isEmpty()){ //userPassword也不能为空
            code = ApiCallback.InputPasswordIsNull;
            status = "Password can not be null";
        } else if(!UserController.getInstance().isUserNameExist(userName)){//判断名字是否存在
            code = ApiCallback.UserIsNotExist;
            status = "User name is not exist.";
        }else if(!UserController.getInstance().getPassword(userName).equals(userPassword)){//判断密码是否正确
            code = ApiCallback.EntryPasswordError;
            status = "Input password is error.";
        }else {//一切正常,更新数据库登录信息,并返回token等信息
            data = UserController.getInstance().doLogin(userName);
            code = ApiCallback.Success;
            status = "Register success";
        }
    }catch (Exception e){
        //报错了,返回错误信息
        status=String.valueOf(e);
    }

    ApiResult apiResult = new ApiResult();
    apiResult.setCode(code);
    apiResult.setStatus(status);
    apiResult.setData(data);//返回数据
    return apiResult;
}

上一篇:游戏社区App (二):网络请求框架的封装
下一篇:游戏社区App (四):底部导航栏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值