Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 390|回复: 0

微信小程序Java登录流程(ssm实现具体功能和加解密隐私信息问题解决方案)

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-5-24 06:37:37 | 显示全部楼层 |阅读模式

    文章有不当之处,欢迎指正,如果喜欢微信阅读,你也可以关注我的微信公众号:好好学java,获取优质学习资源。

    一、登录流程图

    20180510145856113.png

    二、小程序客户端

    doLogin:function(callback = () =>{}){
    let that = this;
    wx.login({
      success:function(loginRes){
        if(loginRes){
          //获取用户信息
          wx.getUserInfo({
            withCredentials:true,//非必填  默认为true
            success:function(infoRes){
              console.log(infoRes,'>>>');
              //请求服务端的登录接口
              wx.request({
                url: api.loginUrl,
                data:{
                  code:loginRes.code,//临时登录凭证
                  rawData:infoRes.rawData,//用户非敏感信息
                  signature:infoRes.signature,//签名
                  encrypteData:infoRes.encryptedData,//用户敏感信息
                  iv:infoRes.iv//解密算法的向量
                },
                success:function(res){
                  console.log('login success');
                  res = res.data;
                  if(res.result==0){
                    that.globalData.userInfo = res.userInfo;
                    wx.setStorageSync('userInfo',JSON.stringify(res.userInfo));
                    wx.setStorageSync('loginFlag',res.skey);
                    console.log("skey="+res.skey);
                    callback();
                  }else{
                    that.showInfo('res.errmsg');
                  }
                },
                fail:function(error){
                  //调用服务端登录接口失败
                 // that.showInfo('调用接口失败');
                  console.log(error);
                }
              });
            }
          });
        }else{
     
        }
      }
    });
    }
    

    微信小程序端发起登录请求,携带的参数主要有:

        code:loginRes.code,//临时登录凭证
        rawData:infoRes.rawData,//用户非敏感信息
        signature:infoRes.signature,//签名
        encrypteData:infoRes.encryptedData,//用户敏感信息
        iv:infoRes.iv//解密算法的向量
    

    参数解释:
    code:loginRes.code,//临时登录凭证:必传,通过code来换取后台的sessionKeyopenId
    rawData:infoRes.rawData,//用户非敏感信息
    signature:infoRes.signature,//签名
    encrypteData:infoRes.encryptedData,//用户敏感信息
    iv:infoRes.iv//解密算法的向量

    signature,//签名、encryptedData,//用户敏感信息、iv//解密算法的向量:

    这三个参数是用来解码用户敏感信息的,比如电话号码等信息。

    需要的数据主要有:skey,用于标志用户的唯一性。

    三、Java后台

    /**
         * 登陆接口
         */
        @RequestMapping("/login")
        @ApiResponses({
                @ApiResponse(code = 404, message = "服务器未找到资源"),
                @ApiResponse(code = 200, message = "请求成功"),
                @ApiResponse(code = 500, message = "服务器错误"),
                @ApiResponse(code = 401, message = "没有访问权限"),
                @ApiResponse(code = 403, message = "服务器拒绝访问"),
        })
        @ApiOperation(value = "小程序登录", httpMethod = "POST", notes = "小程序登录")
        public ResponseEntity<LoginDataResult> login(
                @ApiParam(required = true, value = "临时登录凭证code", name = "code") String code,
                @ApiParam(required = true, value = "用户非敏感信息", name = "rawData")
                @RequestParam(value = "rawData", required = true) String rawData,
                @ApiParam(required = true, value = "签名", name = "signature")
                @RequestParam(value = "signature", required = true) String signature,
                @ApiParam(required = true, value = "用户敏感信息", name = "encrypteData")
                @RequestParam(value = "encrypteData", required = true) String encrypteData,
                @ApiParam(required = true, value = "解密算法的向量", name = "iv")
                @RequestParam(value = "iv", required = true) String iv
        ) {
    
            ObjectMapper mapper = new ObjectMapper();
    
            logger.info("signature============================================================="+signature);
            logger.info("encrypteData=========================================================="+encrypteData);
            logger.info("iv========================================================================"+iv);
    
            RawData data = null;
            WxMaJscode2SessionResult session = null;
            String openid = null;
            String sessionKey = null;
            String phoneNumber = null;
    
            try {
                if (rawData != null && !"".equals(rawData)) {
                    //1、获取用户非敏感信息
                    data = mapper.readValue(rawData, RawData.class);
                }
                session = this.wxService.getUserService().getSessionInfo(code);
    
                //获取到openid和sessionkey
                openid = session.getOpenid();
                sessionKey = session.getSessionKey();
    
                logger.info("sessionkey========================================================="+sessionKey);
    
              /*  //2、获取用户手机号
                phoneNumber = phone(code, signature, rawData, encrypteData, iv);
    
                logger.info("phoneNumber========================================="+phoneNumber);
    */
            } catch (IOException e) {
                e.printStackTrace();
                logger.info("获取用户信息失败");
                LoginDataResult loginDataResult = new LoginDataResult();
                loginDataResult.setCode("2");
                loginDataResult.setMsg("请求失败");
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(loginDataResult);
            } catch (WxErrorException e) {
                e.printStackTrace();
                logger.info("获取用户信息失败");
            }
    
            //3、向数据库插入用户信息
            String skey = insertUser(data, openid, phoneNumber);
    
            //4、缓存openid, sessionKey, skey到redis
            redisCache(openid, sessionKey, skey);
    
    
            //5、把新的skey返回给小程序
            LoginDataResult loginDataResult = new LoginDataResult();
            loginDataResult.setSkey(skey);
            loginDataResult.setCode("1");
            loginDataResult.setMsg("请求成功");
    
            return ResponseEntity.status(HttpStatus.OK).body(loginDataResult);
        }
    
        /**
         * 缓存openid,sessionKey,skey等信息
         * @param openid 小程序用户唯一标志
         * @param sessionKey 小程序会话标志
         * @param skey 后台生成的用户唯一标志,会话管理
         */
        private void redisCache(String openid, String sessionKey, String skey) {
            //根据openid查询skey是否存在
            String skey_redis = jedisClient.hget("WEXIN_USER_OPENID_SKEY", openid);
            if (StringUtils.isNotBlank(skey_redis)) {
                //存在 删除 skey 重新生成skey 将skey返回
                jedisClient.hdel("WEXIN_USER_OPENID_SKEY", openid);
                jedisClient.hdel("WEIXIN_USER_SKEY_OPENID", skey_redis);
                jedisClient.hdel("WEIXIN_USER_SKEY_SESSIONKEY", skey_redis);
            }
    
            //  缓存一份新的
            jedisClient.hset("WEXIN_USER_OPENID_SKEY", openid, skey);
            jedisClient.expire("WEXIN_USER_OPENID_SKEY",432000);//设置5天过期
            jedisClient.hset("WEIXIN_USER_SKEY_OPENID", skey, openid);
            jedisClient.expire("WEIXIN_USER_SKEY_OPENID",432000);//设置5天过期
            jedisClient.hset("WEIXIN_USER_SKEY_SESSIONKEY", skey, sessionKey);
            jedisClient.expire("WEIXIN_USER_SKEY_SESSIONKEY",432000);//设置5天过期
        }
    
        /**
         * 将用户信息插入到数据库
         * @param data 用户信息
         * @param openid
         * @param phoneNumber 手机号
         * @return
         */
        private String insertUser(RawData data, String openid, String phoneNumber) {
            //判断用户数据库是否存在,不存在,入库。
            Member user = userService.selectUserByOpenid(openid);
            //uuid生成唯一key
            String skey = UUID.randomUUID().toString();
            if (user == null) {
                //入库
                user = new Member();
                user.setId(skey);
                user.setCountry(data.getCountry());
                user.setCreatedate(new Date());
                user.setDf(1);
                user.setGender(data.getGender().equals("1") ? 1 : 2);//1为男,2为女
                user.setHeadimg(data.getAvatarUrl());
                user.setNickname(data.getNickName());
                user.setOpenid(openid);
                user.setCitycode(data.getCity());
                user.setProvincecode(data.getProvince());
                user.setMobileno(phoneNumber);
                //插入到数据库
                userService.insertUser(user);
            } else {
                //已存在
                logger.info("用户openid已存在,不需要插入");
                return user.getId();//返回用户唯一标志skey
            }
            return skey;
        }
    
        /**
         * 获取用户板绑定的手机号
         * @param sessionKey 小程序session
         * @param signature 签名
         * @param rawData 用户信息
         * @param encryptedData 小程序加密数据
         * @param iv 小程序向量
         * @return
         */
        @ApiOperation(value = "用户手机号获取", httpMethod = "GET", notes = "用户手机号获取")
        public String phone(String sessionKey, String signature, String rawData, String encryptedData, String iv) {
            String phoneNumber = null;
    
            try {
                byte[] bytes = WxMiniappUtils.decrypt(Base64.decodeBase64(sessionKey), Base64.decodeBase64(iv), Base64.decodeBase64(encryptedData));
                String phone = new String(bytes, "UTF8");
                logger.info("phone====================================="+phone);
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (InvalidAlgorithmParameterException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return null;
        }
    

    下面对上面代码进行分析:

    3.1获取openid和sessionKey

    session = this.wxService.getUserService().getSessionInfo(code);
    
    //获取到openid和sessionkey
    openid = session.getOpenid();
    sessionKey = session.getSessionKey();
    

    这段代码是不是十分的简洁,这里用到了一个第三方的sdk(weixin-java-tools),通过这个sdk可以非常简便的获取到openid和sessionKey,具体的demo

    当然,如果你不想用第三方的sdk,也可以自己实现,实现代码如下:

    public static JSONObject getSessionKeyOrOpenId(String code){
        //微信端登录code
        String wxCode = code;
        String requestUrl = "https://api.weixin.qq.com/sns/jscode2session";
        Map<String,String> requestUrlParam = new HashMap<String, String>(  );
        requestUrlParam.put( "appid","你的小程序appId" );//小程序appId
        requestUrlParam.put( "secret","你的小程序appSecret" );
        requestUrlParam.put( "js_code",wxCode );//小程序端返回的code
        requestUrlParam.put( "grant_type","authorization_code" );//默认参数
     
        //发送post请求读取调用微信接口获取openid用户唯一标识
        JSONObject jsonObject = JSON.parseObject( UrlUtil.sendPost( requestUrl,requestUrlParam ));
        return jsonObject;
    }
    

    3.2解密用户敏感数据获取用户信息

    3.2.1controller

    这个部分自己遇到了好多的坑,由于需要获取用户的手机号码,需要解密用户的信息。

      /**
         * 获取用户板绑定的手机号
         * @param sessionKey 小程序session
         * @param signature 签名
         * @param rawData 用户信息
         * @param encryptedData 小程序加密数据
         * @param iv 小程序向量
         * @return
         */
        @ApiOperation(value = "用户手机号获取", httpMethod = "GET", notes = "用户手机号获取")
        public String phone(String sessionKey, String signature, String rawData, String encryptedData, String iv) {
            String phoneNumber = null;
    
            try {
                byte[] bytes = WxMiniappUtils.decrypt(Base64.decodeBase64(sessionKey), Base64.decodeBase64(iv), Base64.decodeBase64(encryptedData));
                String phone = new String(bytes, "UTF8");
                logger.info("phone====================================="+phone);
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (InvalidAlgorithmParameterException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return null;
        }
    
    3.2.2decrypt工具类

    这里调用了WxMiniappUtils.decrypt这个工具类,工具类如下:

     /**
         * 解密用户手机号算法
         * @param sessionkey 小程序登录sessionKey
         * @param iv 向量
         * @param encryptedData
         * @return
         * @throws NoSuchPaddingException
         * @throws NoSuchAlgorithmException
         * @throws InvalidAlgorithmParameterException
         * @throws InvalidKeyException
         * @throws BadPaddingException
         * @throws IllegalBlockSizeException
         */
        public static byte[] decrypt(byte[] sessionkey, byte[] iv, byte[] encryptedData)
                throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
                InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
            AlgorithmParameterSpec ivSpec = new IvParameterSpec(iv);
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(sessionkey, "AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            return cipher.doFinal(encryptedData);
        }
    

    这里用到的Cipher类是 javax.crypto的类。

    3.2.3问题

    但是这里使用这个decrypt工具类的时候,遇到了好多的问题。

    第一:AES解密是报错javax.crypto.BadPaddingException: pad block corrupted

    这个问题是由于,工具类使用了Cipher.getInstance("AES/CBC/PKCS5Padding")

    解决:Cipher cipher = Cipher.getInstance("AES/ECB/ZeroBytePadding");。

    第二:java.security.InvalidAlgorithmParameterException: Wrong IV length: must be 16
    这个问题是由于,解码出来的iv不是16位,好像是15位,这个为什么我也不太清楚。

    解决:这个怎么解决,自己也没有找到方法,如果有大神解决,望告知!

    我的解决方法:其实我发现这个问题并不是这个工具类的问题,我折腾了一天发现,这个工具类并不是不能够解码手机号,有的是可以的,有的解析不到手机号,只有普通的信息,所以我觉得,这个可能是微信用户注册的时候,是不是用手机号注册的,所以会出现有些能够解析,有的不能解析。如果有大神有其他方法,望告知!

    3.2.4解析成功数据
    {"phoneNumber":"13880684012","purePhoneNumber":"13880684012","countryCode":"86","watermark":{"timestamp":1519460296,"appid":"wx6ede2086ee29a89f"}}
    

    如果解析到了这样的json数据,说明是成功了的。

    3.2.5 另外一种方案
    public class AES {
        public static final AES instance = new AES();
    
        public static boolean initialized = false;
    
        /**
         * AES解密
         * @param content 密文
         * @return
         * @throws InvalidAlgorithmParameterException
         * @throws NoSuchProviderException
         */
        public byte[] decrypt(byte[] content, byte[] keyByte, byte[] ivByte) throws InvalidAlgorithmParameterException {
            initialize();
            try {
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
                Key sKeySpec = new SecretKeySpec(keyByte, "AES");
    
                cipher.init(Cipher.DECRYPT_MODE, sKeySpec, generateIV(ivByte));// 初始化
                byte[] result = cipher.doFinal(content);
                return result;
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (NoSuchProviderException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return null;
        }
    
        public static void initialize(){
            if (initialized) return;
            Security.addProvider(new BouncyCastleProvider());
            initialized = true;
        }
    
        //生成iv
        public static AlgorithmParameters generateIV(byte[] iv) throws Exception{
            AlgorithmParameters params = AlgorithmParameters.getInstance("AES");
            params.init(new IvParameterSpec(iv));
            return params;
        }
    }
    

    这个也会有上面的问题,有时候会解析失败!具体方法,还在摸索中,有大神知道方法和原有,望告知!

    3.2.6第三方sdk方法
    WxMaPhoneNumberInfo phoneNoInfo = this.wxService.getUserService().getPhoneNoInfo(sessionKey, encryptedData, iv);
            phoneNumber = phoneNoInfo.getPurePhoneNumber();
    

    这个也会有上面的问题出现,有时候会解析失败!

    四、总结

    1.小程序端发起请求并携带主要参数

    2.java后台接到/login请求后,根据code去调用微信接口获取用户唯一标识openid和sessionKey

    3.根据openid查询mysql数据库,判断该用户是否存在,如果不存在将用户非敏感信息和其他初始化数据存入到数据库中,如果已存在,不操作

    4.根据openid查询redis数据库,判断openid对应的skey是否存在,如果存在则删除原来老的skey以及对应的openid和sessionKey

    5.通过uuid生成唯一的skey,用openid做键,skey做值,存入到redis中

    6.然后把skey做键,openid和sessionKey的json串做值也重新存入到redis中

    7.根据解密算法,参数有encryptedData、sessionKey和iv,获取用户信息userInfo,如果userInfo字段不满足需要,可通过userInfo.put( "balance",user.getUbalance() );添加所需要的字段和值

    8.将微信小程序需要的数据封装到map中,返回给小程序端。

    参考资料(感谢)
    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-3 00:55 , Processed in 0.061594 second(s), 27 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表