java : 实现微信网页授权,超详细!

背景

使用微信公众号实现网页授权。

开始

1.微信网页授权的官方文档

微信网页授权

2.申请微信测试公众号

在这里插入图片描述
从红框进入申请页面。
在这里插入图片描述
填写必要的信息,注意上图红框部分的域名需要可以外网能够访问,微信需要发送请求进行验证。我用的是内网穿透实现的,下面会说。
在这里插入图片描述
在这里插入图片描述
这里需配置授权成功后回调地址的域名,注意不要写http,https!这个坑我踩过。

3.内网穿透

我用的是natapp,免费的很稳定。详细信息可以看这个兄弟写的博文。
可以看这篇博文

4.实现

大体思路简单说一下,每生成一个授权二维码就在redis生成对应的key,用户扫描授权成功后获取用户信息,同时redis对应的key设置对应的value,保证一码扫一人。长轮询实现扫码成功通知前端。

controller

@Controller
@RequestMapping("/wechat")
@Api(tags = "微信相关接口")
public class WechatController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    WechatService wechatService;

    @RequestMapping("/notify")
    @ResponseBody
    public String wechatNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String xml = wechatService.wechatXml(request);
        String result = "";
        String echostr = request.getParameter("echostr");
        // 首次接入
        if (echostr != null && echostr.length() > 1) {
            result = echostr;
        }
        return result;
    }

    @GetMapping("/obtain_login_code")
    @ApiOperation("获取登录二维码")
    @ResponseBody
    public Result<LoginCodeResDTO> obtainLoginCode(@RequestParam Integer proId) {
        return ResultHandler.result(wechatService.obtainLoginCode(proId));
    }

    @GetMapping("/redirect")
    public String redirectUri(@RequestParam String code, @RequestParam String state, @RequestParam Integer pid) {
        try {
            int flag = wechatService.redirectUri(code, state, pid);
            //失效
            if (flag == 0) {
                return "redirect:../pages/lose.html";
            }
            return "redirect:../pages/success.html";
        } catch (Exception e) {
            logger.error("扫码失败----", e);
            return "redirect:../pages/warn.html";
        }
    }

    @PostMapping(value = "/query_code")
    @ApiOperation("长轮询查询是否扫码成功")
    @ResponseBody
    public Result<WechatUserEntity> queryLoginState(@RequestBody QueryLoginStateReqDTO req) {
        return ResultHandler.result(wechatService.queryLoginState(req));
    }
}

service

@Service
public class WechatServiceImpl implements WechatService {
    @Autowired
    WechatBaseManager wechatBaseManager;
    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    WechatUserManager wechatUserManager;
    @Autowired
    ProjectWechatUserDAO projectWechatUserDAO;

    @Value(value = "${wechat.redirect-url}")
    private String redirectUrl;

    @Override
    public String wechatXml(HttpServletRequest request) throws Exception {
        StringBuffer sb = new StringBuffer();
        InputStream is = request.getInputStream();
        InputStreamReader isr = new InputStreamReader(is, "UTF-8");
        BufferedReader br = new BufferedReader(isr);
        String s = "";
        while ((s = br.readLine()) != null) {
            sb.append(s);
        }
        // 微信发送过来的xml数据
        String xml = sb.toString();
        return xml;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public WechatUserEntity queryLoginState(QueryLoginStateReqDTO req) {
        //生成二维码时存储用户信息的key
        String scene = req.getScene();
        if (redisTemplate.hasKey(scene)) {
            LocalDateTime queryTime = LocalDateTime.now();
            return wechatBaseManager.recurseQueryLogin(queryTime, scene);
        } else {
            throw new BusinessException(40010, "Redis不存在:" + scene);
        }
    }

    @Override
    public int redirectUri(String code, String state, Integer pid) {
        JSONObject oauthJson = wechatBaseManager.getOpenIdByCode(code);
        if (oauthJson.containsKey("openid")) {
            String openid = oauthJson.getString("openid");
            JSONObject userJson = wechatBaseManager.getUserInfo(openid);
            if (userJson.containsKey("openid")) {
                //保存用户
                WechatUserEntity wechatUserEntity = wechatBaseManager.handleLoginState(state, openid, userJson);
                if (wechatUserEntity.getId().equals(0)) {
                    return 0;
                }
                //保存项目用户关系
                wechatUserManager.saveWechatUserAndProject(pid, wechatUserEntity.getId());
            } else {
                throw new BusinessException(40005, "通过openid获取用户基本信息失败");
            }
        } else {
            throw new BusinessException(40004, "通过code换取网页授权失败");
        }
        return 1;
    }

    @Override
    public LoginCodeResDTO obtainLoginCode(Integer proId) {
        String redirectUri = redirectUrl + "wechat/redirect?pid=" + proId;
        String state = WechatContants.PROJECTCODES + UUID.randomUUID().toString().replace("-", "");
        String src = wechatBaseManager.obtainLoginCode(redirectUri, state);
        LoginCodeResDTO resDTO = new LoginCodeResDTO();
        resDTO.setSrc(src);
        resDTO.setState(state);
        redisTemplate.opsForValue().set(state, "", 2, TimeUnit.HOURS);
        return resDTO;
    }

}

manager

@Component
public class WechatBaseManager {

    @Autowired
    RestTemplate restTemplate;
    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    WechatBaseDAO wechatBaseDAO;
    @Autowired
    WechatUserDAO wechatUserDAO;
    @Autowired
    ProjectWechatUserDAO projectWechatUserDAO;

    @Value(value = "${wechat.appid}")
    private String appId;

    @Value(value = "${wechat.appsecret}")
    private String appSecret;

    /**
     * 更新微信accessToken
     */
    public void updateAccessToken() {
        String tokenUrl = String.format(WechatContants.ACCESS_TOKEN_URL, appId, appSecret);
        ResponseEntity<JSONObject> resp = restTemplate.getForEntity(tokenUrl, JSONObject.class);
        JSONObject resultJson = resp.getBody();
        // 如果请求成功
        if (null != resultJson && resultJson.containsKey("access_token")) {
            String accessToken = resultJson.getString("access_token");
            Integer expiresIn = resultJson.getInteger("expires_in");

            String jsTicketUrl = WechatContants.JS_TICKET_URL.replace("ACCESS_TOKEN", accessToken);
            resp = restTemplate.getForEntity(jsTicketUrl, JSONObject.class);
            resultJson = resp.getBody();
            String jsTicket = resultJson.getString("ticket");
            WechatBaseEntity wechatBaseEntity = wechatBaseDAO.findByAppId(appId);
            if (null == wechatBaseEntity) {
                wechatBaseEntity = new WechatBaseEntity();
                wechatBaseEntity.setAppId(appId);
            }
            wechatBaseEntity.setAccessToken(accessToken);
            wechatBaseEntity.setJsTicket(jsTicket);
            wechatBaseEntity.setExpiresIn(expiresIn);
            wechatBaseEntity.setLastUpdateTime(new Timestamp(System.currentTimeMillis()));
            wechatBaseDAO.save(wechatBaseEntity);
            redisTemplate.opsForValue().set(WechatContants.ACCESS_TOKEN, accessToken, 1, TimeUnit.HOURS);
        }
    }

    public String getAccessToken() {
        if (redisTemplate.hasKey(WechatContants.ACCESS_TOKEN)) {
            return redisTemplate.opsForValue().get(WechatContants.ACCESS_TOKEN).toString();
        } else {
            return wechatBaseDAO.findByAppId(appId).getAccessToken();
        }
    }

    public JSONObject createTemporary(String accessToken, int expire, String sceneStr, String type) {
        String requestUrl = String.format(WechatContants.QR_SCENE_URL, accessToken);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("expire_seconds", expire);
        jsonObject.put("action_name", type);
        JSONObject sceneJson = new JSONObject();
        sceneJson.put("scene_str", sceneStr);
        JSONObject actionInfo = new JSONObject();
        actionInfo.put("scene", sceneJson);
        jsonObject.put("action_info", actionInfo);
        ResponseEntity<JSONObject> resp = restTemplate.postForEntity(requestUrl, jsonObject.toString(), JSONObject.class);
        return resp.getBody();
    }

    public WechatUserEntity recurseQueryLogin(LocalDateTime queryTime, String scene) {
        LocalDateTime now = LocalDateTime.now();
        if (queryTime.plusSeconds(30).isAfter(now)) {
            Object object = redisTemplate.opsForValue().get(scene);
            if (object != null && !"".equals(object.toString())) {
                WechatUserEntity member = wechatUserDAO.findById(Integer.parseInt(object.toString())).orElseThrow(() -> new BusinessException(40006, "未找到该用户"));
                return member;
            } else {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                recurseQueryLogin(queryTime, scene);
            }
        } else {
            throw new BusinessException(40001, "重新获取");
        }
        throw new BusinessException(40002, "查询失败");
    }

    public JSONObject getOpenIdByCode(String code) {
        String oauthUrl = String.format(WechatContants.EXCHANGE_URL, appId, appSecret, code);
        ResponseEntity<String> resp = restTemplate.getForEntity(oauthUrl, String.class);
        return JSONObject.parseObject(resp.getBody());
    }

    public JSONObject getUserInfo(String openid) {
        String userUrl = String.format(WechatContants.AUTHINFO_URL, getAccessToken(), openid);
        ResponseEntity<JSONObject> resp = restTemplate.getForEntity(userUrl, JSONObject.class);
        return resp.getBody();
    }

    public WechatUserEntity handleLoginState(String key, String openid, JSONObject userJson) {
        WechatUserEntity member;
        member = wechatUserDAO.findByOpenId(openid);
        if (redisTemplate.hasKey(key)) {
            Object v = redisTemplate.opsForValue().get(key);
            //判断是否二维码已被扫过,扫过返回标识,暂用id代替
            if (!"".equals(v) && v != null) {
                WechatUserEntity entity = new WechatUserEntity();
                entity.setId(0);
                return entity;
            }
            //第一次直接保存,否则更新信息
            if (null == member) {
                member = new WechatUserEntity();
                member.setOpenId(openid);
            }
            if (userJson.containsKey("nickname")) {
                member.setNickName(userJson.getString("nickname"));
            }
            if (userJson.containsKey("headimgurl")) {
                member.setHeadImgUrl(userJson.getString("headimgurl"));
            }
            member = wechatUserDAO.save(member);
            redisTemplate.opsForValue().set(key, member.getId(), 4, TimeUnit.HOURS);
        } else {
            WechatUserEntity entity = new WechatUserEntity();
            entity.setId(0);
            return entity;
        }
        return member;
    }


    public String obtainLoginCode(String redirectUri, String state) {
        return String.format(WechatContants.AUTH_URL, appId, redirectUri, WechatContants.SCOPE, state);
    }
}

utils

public class MessageBean {
	private String ToUserName;
	private String FromUserName;
	private String CreateTime;
	private String MsgType;
	private String content;

	/**
	 * <p>
	 * 私有的构造方法
	 * </p>
	 * <p>
	 * 只允许用其它的构造方法
	 * </p>
	 */
	private MessageBean() {

	}

	public MessageBean(String ToUserName, String FromUserName, String CreateTime, String MsgType) {
		this.ToUserName = ToUserName;
		this.FromUserName = FromUserName;
		this.CreateTime = CreateTime;
		this.MsgType = MsgType;
	}

	public MessageBean(String ToUserName, String FromUserName, String CreateTime, String MsgType, String content) {
		this.ToUserName = ToUserName;
		this.FromUserName = FromUserName;
		this.CreateTime = CreateTime;
		this.MsgType = MsgType;
		this.content = content;
	}

	@Override
	public String toString() {
		StringBuffer sBuffer = new StringBuffer();
		sBuffer.append("<xml>");
		sBuffer.append("<ToUserName><![CDATA[" + this.ToUserName + "]]></ToUserName>");
		sBuffer.append("<FromUserName><![CDATA[" + this.FromUserName + "]]></FromUserName>");
		sBuffer.append("<CreateTime>" + this.CreateTime + "</CreateTime>");
		sBuffer.append("<MsgType><![CDATA[" + this.MsgType + "]]></MsgType>");
		if (this.MsgType.equals(WechatContants.TEXT_MSG_TYPE)) {// 文本消息
			sBuffer.append("<Content><![CDATA[" + this.content + "]]></Content>");
		} else if (this.MsgType.equals(WechatContants.KF_MSG_TYPE)) {// 客服消息
			if (this.content != null && !"".equals(this.content)) {
				sBuffer.append("<TransInfo>");
				sBuffer.append("<KfAccount><![CDATA[" + this.content + "]]></KfAccount>");
				sBuffer.append("</TransInfo>");
			}
		} else if (this.MsgType.equals(WechatContants.IMAGE_MSG_TYPE)) {
			sBuffer.append("<Image>");
			sBuffer.append("<MediaId><![CDATA[" + this.content + "]]></MediaId>");
			sBuffer.append("</Image>");
		}
		sBuffer.append("</xml>");
		return sBuffer.toString();
	}

	public String getToUserName() {
		return ToUserName;
	}

	public void setToUserName(String toUserName) {
		ToUserName = toUserName;
	}

	public String getFromUserName() {
		return FromUserName;
	}

	public void setFromUserName(String fromUserName) {
		FromUserName = fromUserName;
	}

	public String getCreateTime() {
		return CreateTime;
	}

	public void setCreateTime(String createTime) {
		CreateTime = createTime;
	}

	public String getMsgType() {
		return MsgType;
	}

	public void setMsgType(String msgType) {
		MsgType = msgType;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}
}

public class WechatContants {

    /**
     * 存放ACCESS_TOKEN的key
     */
    public final static String ACCESS_TOKEN = "yenep-accesstoken";

    /**
     * 存放JS_TICKET的key
     */
    public final static String JSAPI_TICKET = "jsticket";

    /**
     * 获取access_token的接口地址(GET) 限200(次/天)
     */
    public final static String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";

    /**
     * 获取jsapi_ticket的接口地址,(GET)
     */
    public final static String JS_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi";

    /**
     * 微信 二维码
     */
    public static final String QR_SCENE = "QR_SCENE";
    public static final String QR_STR_SCENE = "QR_STR_SCENE";
    public static final String TICKET = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=";
    public static final String QR_SCENE_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s";

    /**
     * 微信事件
     */
    public static final String LOGIN = "login";

    public static final String PROJECTCODES = "projectcodes";

    /**
     * 消息类型
     */
    public static final String KF_MSG_TYPE = "transfer_customer_service";
    public static final String TEXT_MSG_TYPE = "text";
    public static final String IMAGE_MSG_TYPE = "image";

    /**
     * 弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息
     */
    public static final String SCOPE = "snsapi_userinfo";

    //授权网址
    public static final String AUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect";

    /**
     * 通过code换取网页授权access_token
     */
    public static final String EXCHANGE_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
    /**
     * 获取用户信息
     */
    public static final String AUTHINFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";

}
public class XmlUtil {
    private Document document = null;

    public XmlUtil(String xmlStr) {
        try {
            this.document = DocumentHelper.parseText(xmlStr);
        } catch (DocumentException var3) {
            var3.printStackTrace();
        }

    }

    public String getValueByNote(String targetNode) {
        try {
            Element root = this.document.getRootElement();
            return this.getChildrenNodes(root, targetNode);
        } catch (Exception var3) {
            var3.printStackTrace();
            return null;
        }
    }

    public String getChildrenNodes(Element parentNode, String targetNode) {
        String value = null;
        boolean isFind = false;

        try {
            List<Element> elementList = parentNode.elements();
            Iterator var6 = elementList.iterator();

            while (var6.hasNext()) {
                Element element = (Element) var6.next();
                if (isFind) {
                    break;
                }

                if (element.getName() == targetNode) {
                    isFind = true;
                    value = element.getTextTrim();
                }

                if (!isFind) {
                    this.getChildrenNodes(element, targetNode);
                }
            }
        } catch (Exception var8) {
            var8.printStackTrace();
        }

        return value;
    }

    public Document getDocument() {
        return this.document;
    }

    public void setDocument(Document document) {
        this.document = document;
    }
}

定时器
定时获取AccessToken,获取用户信息时需要。

@Configuration
@EnableScheduling
public class WechatBaseTask {
    @Autowired
    WechatBaseManager wechatBaseManager;

    @PostConstruct
    @Scheduled(cron = "0 0 0/1 * * ?")
    private void tokenTask() {
        wechatBaseManager.updateAccessToken();
    }

}

在这里插入图片描述


版权声明:本文为weixin_45362084原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。