微信扫码登录、支付项目总结

2023-05-16

一、前期准备

项目代码https://gitee.com/lcaicai/xdvideo.git

微信网站应用扫码登录官方文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=7e1296c8174816ac988643825ae16f25d8c7e781&lang=zh_CN

微信扫码支付官方文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5

从官方文档主要汲取交互流程,调用接口或者获得回调时发送、接收的参数规范,错误码的提示。

什么是appid、appsecret、授权码code
            appid和appsecret是 资源所有者向申请人分配的一个id和秘钥
            code是授权凭证,A->B 发起授权,想获取授权用户信息,那a必须携带授权码,才可以向B获取授权信息

二、与微信交互总体流程

扫码登录时序图:

1、用点击微信第三方登录,第三方应用带上appid、回调地址(开放平台重定向地址,第三方应用)、当前页面url请求微信开放平台,微信开放平台返回登录二维码。

2、用户扫码确认,此时微信开放平台根据之前的回调地址带上code码进行回调,第三方应用带上appid、appappsecret、微信回调的code请求微信,获得access_token,此时便可以带上access_token去请求用户公开的基本信息(用户名、头像等)。

扫码支付时序图:

1、用户点击购买对象,请求商户后台系统生成订单(此时的订单的支付状态是未支付,等待微信平台通知支付成功消息再修改订单状态)。

2、商户系统调微信统一下单API,如果签名校验成功,微信平台会返回一个codeurl(二维码url)。

3、商户系统拿到codeurl转成二维码图片呈现给用户。

4、用户扫码支付确认,微信平台回调商户系统,通知用户已经支付成功,商户系统修改用户订单状态,并且通知微信订单处理成功(在收到商户系统确认信息前,微信平台有个策略会按照一定时间间隔一直请求商户系统)。

三、所用技术栈

总体框架:springboot+mybatis+maven+mysql

pageHelper 分页插件、JWT标准解决单点登录、google二维码生成包、httpClient

使用netapp做本地域名映射,对外提供访问,具体使用方法见netapp的官方文档。

四、项目详述

1、微信扫码登录

用户点击微信第三方登录,带上当前页面地址请求商户系统后端接口

//获取微信扫码地址
	function get_wechat_url() {
		//获取当前页面地址
		var current_page = window.location.href;
		//向后端发送请求获取url
		$.ajax({
			type:'get',
			url:host+'/api/v1/wechat/login_url?access_url='+current_page,
			dataType:'json',
			success:function(res){
				$("#login").attr("href",res.data);
				global_login_url = res.data;
			}
		})
	}

后端返回处理拼接好的url

    @RequestMapping("login_url")
    @ResponseBody
    public JsonData loginUrl(@RequestParam(value = "access_url",required = true) String accessUrl) throws UnsupportedEncodingException {
        //拼接上需要微信回调的商户系统url
        String redirectUrl = weChatConfig.getOpenRedirectUrl();//获取开放平台重定向地址

        String callbackUrl = URLEncoder.encode(redirectUrl,"GBK");

//private final  String OPEN_QRCODE_URL= "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect";用占位符替换参数
        String qrcodeUrl = String.format(weChatConfig.getOpenQrcodeUrl(),weChatConfig.getOpenAppId(),callbackUrl,accessUrl);

        return JsonData.buildSuccess(qrcodeUrl);
    }

用户扫码登录,微信平台带上授权code回调redirectUrl接口

    /**
     * 用户扫码后,微信平台自动带上code回调该接口,获取access_token,再带上access_token去获取用户            信息
     * 生成token返回给前端,并且重定向到state页面(用户当前页面)
     * @param code
     * @param state
     * @param response
     * @throws IOException
     */
    @RequestMapping("user_Callback")
    public void weChatUserCallback(@RequestParam(value = "code",required = true)String code,
                                   String state, HttpServletResponse response) throws IOException {

        User user = userService.saveWeChatUser(code);
        if (null != user){
            //生成JWT,返回客户端
            String token = JwtUtils.geneJsonWebToken(user);
            //页面地址返回给前端,并重定向到state URL页面
            //state 当前用户的页面地址,需要拼接 http:// 这样才不会站内跳转
            response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));
        }
    }

根据微信code用HttpClient去请求access_token,来获取用户基本信息,存库。

@Override
    public User saveWeChatUser(String code) {

        try {
            String get_access_token_url = String.format(WeChatConfig.getOpenAccessTokenUrl(),
                    weChatConfig.getOpenAppId(),weChatConfig.getOpenAppSecret(),code);
            //获取acess_token
            Map<String,Object> access_token_map = HttpUtils.doGet(get_access_token_url);

            if (null == access_token_map || access_token_map.isEmpty()){
                return null;
            }

            String accessToken = (String) access_token_map.get("access_token");
            String openid = (String) access_token_map.get("openid");

            //获取用户基本信息
            String userInfoUrl = String.format(WeChatConfig.getOpenUserInfoUrl(),
                    accessToken,openid);
            Map<String,Object> userMap = HttpUtils.doGet(userInfoUrl);

            if (null == userMap || userMap.isEmpty()){
                return null;
            }

            User dbUser = userMapper.findUserByOpenId(openid);
            //如果用户已经存在,不需要再插入该用户信息,只需在其他接口更新信息
            if (null != dbUser){
                return dbUser;
            }

            String nickname = (String) userMap.get("nickname");
            //解决乱码问题
            nickname = new String(nickname.getBytes("ISO-8859-1"), "UTF-8");
            Double sexTemp = (Double) userMap.get("sex");
            int sex = sexTemp.intValue();
            String province = (String) userMap.get("province");
            String city = (String) userMap.get("city");
            String country = (String) userMap.get("country");
            String headimgurl = (String) userMap.get("headimgurl");
            String unionid = (String) userMap.get("unionid");

            //拼接国家省份,用","分割
            StringBuilder stringBuilder = new StringBuilder(country).append(",")
                    .append(province).append(",").append(city);
            String finalAddress = stringBuilder.toString();
            //请求用户地址加入'lang=zh_CN'后乱码,在这里转一次码
            finalAddress = new String(finalAddress.getBytes("ISO-8859-1"), "UTF-8");

            User user = new User();
            user.setName(nickname);
            user.setSex(sex);
            user.setHeadImg(headimgurl);
            user.setCity(finalAddress);
            user.setCreateTime(new Date());

            userMapper.save(user);
            return user;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

利用JWT存储用户session,设置过期时间,之后每次请求前获取cookie中的token,带上token请求

public static final String SUBJECT = "LC666";

    public static final long EXPIRE = 1000*60*60*24*3;

    public static final String  APPSECRET = "LC666";

/**
     * 生成JWT
     * @param user
     * @return
     */
    public static String geneJsonWebToken(User user){
        if (null == user || null == user.getId() || null == user.getName()
                || null == user.getHeadImg()){
            return null;
        }
        String token = Jwts.builder().setSubject(SUBJECT)
                //此时user对象已经插入到User表中,并且mapper中insert插入时,
                // 设置了@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")注解
                //user对象插入表中,mybatis会自动获取自动增长的主键id,并且自动注入到User实体类的id属性中
                            .claim("id",user.getId())
                            .claim("name",user.getName())
                            .claim("img",user.getHeadImg())
                            .setIssuedAt(new Date())
                            .setExpiration(new Date(System.currentTimeMillis()+EXPIRE))
                            .signWith(SignatureAlgorithm.HS256,APPSECRET).compact();
        return token;
    }

后台将token、用户信息返回前端,并且重定向到之前的页面,前端拿到url设置用户昵称,头像等。

//页面地址返回给前端,并重定向到state URL页面
            //state 当前用户的页面地址,需要拼接 http:// 这样才不会站内跳转
            response.sendRedirect(state+"?token="+token+"&head_img="+user.getHeadImg()+"&name="+URLEncoder.encode(user.getName(),"UTF-8"));

2、微信扫码支付

用户点击购买某个对象,后端生成订单,调微信统一下单接口。

/**
 * 订单接口
 */
@RestController
//@RequestMapping("/user/api/v1/order")
@RequestMapping("/api/v1/order")
public class OrderController {

    @Autowired
    private VedioOrderServise vedioOrderServise;


    @GetMapping(value = "add")
    public void saveOrder(@RequestParam(value = "vedio_id", required = true)int videoId,
                              HttpServletResponse response,HttpServletRequest request) throws Exception {

       String ip = IpUtils.getIpAddr(request);

//       int userId = (Integer)request.getAttribute("user_id");

        int userId = 1;//测试先付个值

//        String ip = "120.25.1.43";//测试用的ip

        VideoOrderDto videoOrderDto = new VideoOrderDto();

        videoOrderDto.setUserId(userId);

        videoOrderDto.setVideoId(videoId);

        videoOrderDto.setIp(ip);

        String codeUrl = vedioOrderServise.save(videoOrderDto);

        //生成二维码

        if (null == codeUrl){
            throw new NullPointerException();
        }

        //调用二维码生成工具
        CommonUtils.generateQRByCodeurl(codeUrl,response);
    }
}

生成订单,调统一下单接口

@Override
    @Transactional(propagation = Propagation.REQUIRED) //下单的时候开启一个事物
    public String save(VideoOrderDto videoOrderDto) throws Exception {

        //日志打点
        dataLogger.info("module=video_order`api=save`user_id={}`video_id={}",videoOrderDto.getUserId(),videoOrderDto.getVideoId());

        //查找视频信息
       Video video = videoMapper.findById(videoOrderDto.getVideoId());

       //查找用户信息
       User user = userMapper.findUserByid(videoOrderDto.getUserId());

       //生成订单
        VideoOrder videoOrder = new VideoOrder();
        videoOrder.setTotalFee(video.getPrice());
        videoOrder.setVideoImg(video.getCoverImg());
        videoOrder.setVideoTitle(video.getTitle());
        videoOrder.setCreateTime(new Date());
        videoOrder.setVideoId(video.getId());
        videoOrder.setState(0);
        videoOrder.setUserId(user.getId());
        videoOrder.setHeadImg(user.getHeadImg());
        videoOrder.setNickname(user.getName());

        videoOrder.setDel(0);
        videoOrder.setIp(videoOrderDto.getIp());
        videoOrder.setOutTradeNo(CommonUtils.generateUUID());//生成流水号

        videoOrderMapper.insert(videoOrder);

        //获取codeurl
        String codeUrl = unifiedOrder(videoOrder);

        if (null != codeUrl){
            return codeUrl;
        }


        return null;
    }

统一订单方法(这里需要商户申请的id,具体请看微信扫码支付开发文档),与微信交互方式为xml方式,微信平台提供了map与xml互相转换的方法,也可以使用自己写的,重点是sign签名校验,在微信平台和服务端都保存了key,这个key用与sign签名校验。

/**
     * 统一下单方法
     * @return
     */
    private String unifiedOrder (VideoOrder videoOrder) throws Exception {
        //生成签名
        SortedMap<String,String> params = new TreeMap<>();
        params.put("appid",weChatConfig.getAppId());
        params.put("mch_id", weChatConfig.getMchId());
        params.put("nonce_str",CommonUtils.generateUUID());
        params.put("body",videoOrder.getVideoTitle());
        params.put("out_trade_no",videoOrder.getOutTradeNo());
        params.put("total_fee",videoOrder.getTotalFee().toString());
        params.put("spbill_create_ip",videoOrder.getIp());
        params.put("notify_url",weChatConfig.getPayCallbackUrl());
        params.put("trade_type","NATIVE");

        //sign签名
        String sign = WXPayUtil.createSign(params,weChatConfig.getKey());
        params.put("sign",sign);
        //map转xml
        String payXml = WXPayUtil.mapToXml(params);
//        System.out.println("========支付请求xml"+payXml);

        //统一下单

        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,8000);

        if (null == orderStr){
            return null;
        }

        Map<String,String> unifieOrderMap = WXPayUtil.xmlToMap(orderStr);

//        System.out.println(unifieOrderMap.toString());
        if (null != unifieOrderMap){
            return unifieOrderMap.get("code_url");
        }
        return null;
    }

生成sign签名,key不能暴露!!!微信那边保存着一模一样的key,将你传过去的用户信息使用相同策略加密,最后会和你传过去的sign进行比较,如果相同此次请求才能成功。

签名生成的通用步骤如下:

            第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。

            第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置

可以使用SortedMap将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序)。

生成签名后,通过工具去校验(这里有个坑,参数params里面的值不能有空格,否则会校验不成功
                https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1

/**
     * 生成微信支付sign
     * @param params
     * @param key
     * @return
     */
    public static String createSign(SortedMap<String,String> params,String key){
        StringBuilder sb = new StringBuilder();
        Set<Map.Entry<String, String>> entries = params.entrySet();
        Iterator<Map.Entry<String, String>> it = entries.iterator();
        while (it.hasNext()){
            Map.Entry<String, String> entry = (Map.Entry<String, String>)it.next();
            String k = entry.getKey();
            String v = entry.getValue();
            if (null != k && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
                sb.append(k+"="+v+"&");
            }
        }

        sb.append("key=").append(key);
        String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
        return sign;
    }

准备好sign和排序好的Map转的xml后,调微信统一下单接口,微信校验sign成功返回支付码url,通过google工具生成二维码图片。

/**
     * 统一下单url
     */
    private static final String UNIFIED_ORDER_URL = "https://api.xdclass.net/pay/unifiedorder";
        
//统一下单

        String orderStr = HttpUtils.doPost(WeChatConfig.getUnifiedOrderUrl(),payXml,8000);
//生成二维码

        if (null == codeUrl){
            throw new NullPointerException();
        }

        //调用二维码生成工具
        CommonUtils.generateQRByCodeurl(codeUrl,response);
/**
     * 将支付码链接通过google二维码工具生成
     * @param codeUrl
     * @param response
     */
    public static void generateQRByCodeurl(String codeUrl, HttpServletResponse response){
        try {

            //生成二维码配置
            Map<EncodeHintType,Object> hints = new HashMap<>();

            //设置纠错等级
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
            //编码类型
            hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
            //将链接及配置放入google二维码生成类 初始化
            BitMatrix bitMatrix = new MultiFormatWriter()
                    .encode(codeUrl, BarcodeFormat.QR_CODE,400,400,hints);
            //将二维码以流的形式输出浏览器
            OutputStream outputStream = response.getOutputStream();
            MatrixToImageWriter.writeToStream(bitMatrix,"png",outputStream);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

用户扫码,微信平台回调商户系统(这里使用BufferedReader效率更高),使用相同策略校验sign,校验成功后获取支付状态,如果为SUCCESS,即用户支付成功,商户后台修改订单状态,这里要注意幂等,由于网络原因微信平台没有收到商户系统的响应,会每隔一段时间回调一次该接口,所以在修改订单状态前需要判断下订单state,如果state等于未支付状态才回去数据库中修改,否则不修改。

@RequestMapping("/order/callback1")
    public void orderCallback(HttpServletRequest request,HttpServletResponse response) throws Exception {

        //将参数转成流的形式
        InputStream inputStream = request.getInputStream();

        //BufferedReader是包装设计模式,性能更高
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));

        //拼接微信回调的xml
        StringBuffer sb = new StringBuffer();

        String line ;
        while (null != (line = bufferedReader.readLine())){
            sb.append(line);
        }

        bufferedReader.close();
        inputStream.close();

        Map<String,String> callMap = WXPayUtil.xmlToMap(sb.toString());
//        System.out.println(callMap.toString());

        SortedMap<String,String> sortedMap = WXPayUtil.getSortedMap(callMap);
        //校验签名是否正确
        boolean flag = WXPayUtil.isCorrectSign(sortedMap,weChatConfig.getKey());

        if (flag == true){
            if ("SUCCESS".equals(sortedMap.get("result_code"))){

                String outTradeNo = sortedMap.get("out_trade_no");
                //此时订单已经在扫码之前插入订单表,生成了订单
                VideoOrder dbvideoOrder = vedioOrderServise.findByOutTradeNo(outTradeNo);

                //注意幂等性,防止多次相同请求调用
                if (null != dbvideoOrder && dbvideoOrder.getState() == 0){

                    VideoOrder videoOrder = new VideoOrder();
                    videoOrder.setOpenid(sortedMap.get("openid"));
                    videoOrder.setOutTradeNo(outTradeNo);
                    videoOrder.setNotifyTime(new Date());
                    videoOrder.setState(1);
                    int row = vedioOrderServise.updateVideoOderByOutTradeNo(videoOrder);

                    if (row == 1){//通知微信订单处理成功

                        response.setContentType("text/xml");
                        response.getWriter().println("success");
                    }

                }

            }
        }
        response.setContentType("text/xml");
        response.getWriter().println("fail");
    }

token校验

拦截器

/**
     * 登录时对用户进行拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        String token = request.getHeader("token");

        if (null == token){
            token = request.getParameter("token");
        }

        if (null != token){
            Claims claims = JwtUtils.checkJWT(token);
            if (null != claims){
                Integer userid = (Integer) claims.get("id");
                String name = (String) claims.get("name");
                //setAttribute 以便controller可以获取到用户信息
                request.setAttribute("user_id",userid);
                request.setAttribute("name",name);
                return true;
            }
        }
        sendJsonMessage(response, JsonData.buildError("请登录"));
        return false;
    }

JWT校验token,并且把用户基本信息放入request中,方便于用户下单获取用户昵称等信息。

/**
     * 校验token
     * @param token
     * @return
     */
    public static Claims checkJWT(String token){
        try{
           final Claims claims = Jwts.parser().setSigningKey(APPSECRET)
                   .parseClaimsJws(token).getBody();
            return claims;
        }catch (Exception e){}
        return null;
    }

五、总结

        总体比较简单,该项目非常适合拿来练手,主要学习的点有微信的交互流程、业务,之前没有用过JWT,可以学习下,也很简单。项目中的各种工具类都可以在以后的项目中直接复用,个人建议把所有工具类都抽出来。当然,也会有些小坑,最好根据微信开发官方文档的错误码提示来定位。代码在gitte上,地址在博客最上面,后期还会做些优化。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

微信扫码登录、支付项目总结 的相关文章

随机推荐