企业微信 网页授权登入

官网地址:https://work.weixin.qq.com/api/doc/90000/90135/91020

企业微信提供了OAuth的授权登录方式,可以让从企业微信终端打开的网页获取成员的身份信息,从而免去登录的环节。
企业应用中的URL链接(包括自定义菜单或者消息中的链接),均可通过OAuth2.0验证接口来获取成员的UserId身份信息。

OAuth2简介

OAuth2的设计背景,在于允许用户在不告知第三方自己的帐号密码情况下,通过授权方式,让第三方服务可以获取自己的资源信息。
详细的协议介绍,开发者可以参考RFC 6749。

下面简单说明OAuth2中最经典的Authorization Code模式,流程如下:

流程图中,包含四个角色。

  • ResourceOwner为资源所有者,即为用户
  • User-Agent为浏览器
  • AuthorizationServer为认证服务器,可以理解为用户资源托管方,比如企业微信服务端
  • Client为第三方服务

调用流程为:
A) 用户访问第三方服务,第三方服务通过构造OAuth2链接(参数包括当前第三方服务的身份ID,以及重定向URI),将用户引导到认证服务器的授权页
B) 用户选择是否同意授权
C) 若用户同意授权,则认证服务器将用户重定向到第一步指定的重定向URI,同时附上一个授权码。
D) 第三方服务收到授权码,带上授权码来源的重定向URI,向认证服务器申请凭证。
E) 认证服务器检查授权码和重定向URI的有效性,通过后颁发AccessToken(调用凭证)

D)与E)的调用为后台调用,不通过浏览器进行

企业微信OAuth2接入流程


图1 企业微信OAuth2流程图

使用OAuth2前须知

关于网页授权的可信域名

REDIRECT_URL中的域名,需要先配置至应用的“可信域名”,否则跳转时会提示“redirect_uri参数错误”。
要求配置的可信域名,必须与访问链接的域名完全一致;若访问链接URL带了端口号,端口号也需要登记到可信域名中。举个例子:

  • 假定重定向访问的链接是:http://mail.qq.com:8080/cgi-bin/helloworld:
配置域名 是否正确 原因
mail.qq.com:8080 配置域名与访问域名完全一致
email.qq.com 配置域名必须与访问域名完全一致
support.mail.qq.com 配置域名必须与访问域名完全一致
*.qq.com 不支持泛域名设置
mail.qq.com 配置域名必须与访问域名完全一致,包括端口号
  • 假定配置的可信域名是 mail.qq.com:
访问链接 是否正确 原因
https://mail.qq.com/cgi-bin/helloworld 配置域名与访问域名完全一致
http://mail.qq.com/cgi-bin/redirect 配置域名与访问域名完全一致,与协议头/链接路径无关
https://exmail.qq.com/cgi-bin/helloworld 配置域名必须与访问域名完全一致

关于UserID机制

UserId用于在一个企业内唯一标识一个用户,通过网页授权接口可以获取到当前用户的UserId信息,如果需要获取用户的更多信息可以调用 通讯录管理 - 成员接口 来获取。

缓存方案建议

通过OAuth2.0验证接口获取成员身份会有一定的时间开销。对于频繁获取成员身份的场景,建议采用如下方案:
1、企业应用中的URL链接直接填写企业自己的页面地址
2、成员操作跳转到步骤1的企业页面时,企业后台校验是否有标识成员身份的cookie信息,此cookie由企业生成
3、如果没有匹配的cookie,则重定向到OAuth验证链接,获取成员的身份信息后,由企业后台植入标识成员身份的cookie信息
4、根据cookie获取成员身份后,再进入相应的页面

实践

maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.1.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.kuang</groupId><artifactId>spring-outh</artifactId><version>0.0.1-SNAPSHOT</version><name>spring-outh</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- redis 缓存操作 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.0</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.4.1</version></dependency><!--常用工具类 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

application.yml

wechat:cp:# 企业IDcorpId: xxxxxxx# 应用的idagentId: xxxx# 应用的凭证密钥secret: xxxxxxxspring:# redis 配置redis:# 地址host: localhost# 端口,默认为6379port: 6379# 密码password:# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms

controller

package com.kuang.springouth.controller;import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;import com.kuang.springouth.pojo.UserBean;
import com.kuang.springouth.utils.GetAcessTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;/*** @Author: Abe* Date: 2020/12/5 16:27* 企业微信网页授权登入*/
@Controller
@RequestMapping("/wechat")
@Validated
@Slf4j
public class SimpleOuthController {@Value("${wechat.cp.corpId}")private String appid; // 企业的CorpID@Value("${wechat.cp.agentId}")private String agentId; //自建应用的id@Value("${wechat.cp.secret}")private String secret; //自建应用的密钥@Autowiredprivate GetAcessTokenUtil acessTokenUtil;/*** 构造网页授权链接*/@GetMapping("")public Object outhApi(HttpServletRequest request) {//获取项目域名String requestUrl = request.getServerName();String contextPath = request.getContextPath();log.info("domain name: " + requestUrl + " project name: " + contextPath);//拼接微信回调地址String backUrl ="http://" + requestUrl + contextPath + "/oauth2me";//因为我是本地测试,没有真正的域名,所以使用ngrok内网穿透工具获得一个域名,//你要是实际项目,并且有域名的情况下,就用你自己实际能用的域名backUrl = "http://域名/wechat/oauth2me";String redirect_uri = "";try {redirect_uri = java.net.URLEncoder.encode(backUrl, "utf-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();log.error("ecdoe error: " + e.getMessage());}String oauth2Url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + appid + "&redirect_uri=" + redirect_uri+ "&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect";//重定向到构建好的链接return "redirect:" + oauth2Url;}/*** 授权回调请求地址* @return*/@GetMapping("/oauth2me")@ResponseBodypublic Object oAuth2Url(@RequestParam String code, HttpSession session){try {String acessToken = (String) acessTokenUtil.getAcessToken();if(acessToken != null) {//url获取用户信息的请求地址String requestUrl = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo";HashMap<String, Object> paramMap = new HashMap<>();paramMap.put("access_token", acessToken);paramMap.put("code", code);String s = HttpUtil.get(requestUrl, paramMap);UserBean userBean = JSONUtil.toBean(s, UserBean.class);if(userBean.getErrcode().equals("0")) {//调用成功//我这边是把userId存在sessionsession.setAttribute("userId", userBean.getUserId());}//这边一般就是重定向 前台页面首页的地址 这边方便测试我直接返回得到的数据//String url = "http://localhost:6255/#/";//return "redirect:" + url;return userBean;}}catch (Exception e){e.printStackTrace();}return "";}/*** 测试userId是否已经存在sesiion中* @param session* @return*/@GetMapping("/getUserId")@ResponseBodypublic Object getuserId(HttpSession session) {String userId = (String) session.getAttribute("userId");return userId;}}

pojo

package com.kuang.springouth.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;/*** @Author: Abe* Date: 2020/12/5 18:12*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserBean implements Serializable {private String errcode;private String errmsg;private String OpenId;private String DeviceId;private String external_userid;private String UserId;
}
package com.kuang.springouth.pojo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;/*** @Author: Abe* Date: 2020/12/5 17:55*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResutlBean implements Serializable {private String errcode;private String errmsg;private String access_token;private Long expires_in;
}

redis

package com.kuang.springouth.redis;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;/*** spring redis 工具类** @author ruoyi**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @param unit 时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象** @param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public long deleteObject(final Collection collection){return redisTemplate.delete(collection);}/*** 缓存List数据** @param key 缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> long setCacheSet(final String key, final Set<T> dataSet){Long count = redisTemplate.opsForSet().add(key, dataSet);return count == null ? 0 : count;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key Redis键* @param hKey Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** @param key Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 获取多个Hash中的数据** @param key Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}
}

utils

package com.kuang.springouth.utils;import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.kuang.springouth.pojo.ResutlBean;
import com.kuang.springouth.redis.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.HashMap;/*** @Author: Abe* Date: 2020/12/5 16:48*/
@Component
@Slf4j
public class GetAcessTokenUtil {@Value("${wechat.cp.corpId}")private String appid; // 企业的CorpID@Value("${wechat.cp.agentId}")private String agentId; //自建应用的id@Value("${wechat.cp.secret}")private String secret; //自建应用的密钥@Autowiredprivate RedisCache redisCache;private String url =  "https://qyapi.weixin.qq.com/cgi-bin/gettoken";public Object getAcessToken() {try {String access_token = redisCache.getCacheObject("access_token");if(access_token == null) {log.info("重新获取token");HashMap<String, Object> paramMap = new HashMap<>();paramMap.put("corpid", appid);paramMap.put("corpsecret", secret);System.out.println(paramMap);String s = HttpUtil.get(url, paramMap);ResutlBean resutlBean = JSONUtil.toBean(s, ResutlBean.class);if(resutlBean.getErrcode().equals("0")) {//成功//access_token存到redis中redisCache.setCacheObject("access_token", resutlBean.getAccess_token());return resutlBean.getAccess_token();}}else {return access_token;}}catch (Exception e){e.printStackTrace();}return null;}}

总结

  1. 上边配置文件中redis中用lettuce,这个对springboot的版本是有要求的
  2. access_token需要缓存下来,不能频繁调用官网的接口
  3. 每个应用有独立的secret,获取到的access_token只能本应用使用,所以每个应用的access_token应该分开来获取
  4. 需要在自建应用中的网页授权及JS-SDK配置可信域名,必须是域名。我是使用ngrok内网穿透工具实现的。

企业微信 网页授权登入相关推荐

  1. 微信wechat.class.php,laravel使用组件实现微信网页授权登入

    laravel框架中的实现简单实现微信网页授权登入 首先引入基于laravel的easywechat的组件,laravel版本5.8 $ cd laravel $ composer require & ...

  2. 微信网页授权登入--laravel组件 laravel-wechat调用

    组件地址:https://github.com/overtrue/laravel-wechat laravel框架中的实现简单实现微信网页授权登入,首先引入基于laravel的easywechat的组 ...

  3. .netMVC企业微信网页授权+注册全局过滤器

    微信网页授权 达到效果:企业应用只能在微信中查看,在浏览器中打开企业页面,显示无权限! 原理,用session记录用户,如果用户已经通过微信授权,记录@Session["UserId&quo ...

  4. 企业微信网页授权及JS-SDK碰到检查域名所有权不通过的问题

    在设置"网页授权及JS-SDK"可信域名时,一直不成功,现在显示:"检查域名所有权不通过" 申请域名校验 点击打开申请域名校验,输入域名,下载对应的文件: 拷贝 ...

  5. uniapp 实现企业微信网页授权登录

    效果图: 需求: 想在企业微信中打开网页获取成员的身份信息,从而免去登录的环节,实现无感登录 官网文档介绍:构造网页授权链接 - 接口文档 - 企业微信开发者中心 具体实现步骤: 1.进入企业微信管理 ...

  6. 企业微信-网页授权登录

    第一步:在企业微信应用里设置构造访问链接获取到code: 第二步:写一个接收处理code的函数,得到访问用户的userid 第三步:您自己做根据获取的userid进行验证处理吧. 备注:我做的企业微信 ...

  7. JustAuth 1.15.9 版发布,支持飞书、喜马拉雅、企业微信网页登录

    新增 修复并正式启用 飞书 平台的第三方登录 AuthToken 类中新增 refreshTokenExpireIn 记录 refresh token 的有效期 PR 合并 Github #101:支 ...

  8. 网页版登录入口_企业微信网页版怎么登录?企业微信客户端和网页版有什么区别?...

    文丨语鹦企服私域管家原创,未经授权不得转载 企业微信有网页版也有客户端,很多小伙伴可能搞不清,今天语鹦企服就带你一起看看,企业微信客户端和网页版有什么区别?以及如何登录使用. ▎企业微信网页版: 与微 ...

  9. Springboot + Spring Security多种登录方式:账号用户名登录+微信网页授权登录

    一.概述 实现账号用户名+微信网页授权登录集成在Spring Security的思路,最重要的一点是要实现微信登录通过Spring Security安全框架时,不需要验证账号.密码. 二.准备工作 要 ...

  10. 使用微信开发者工具调试微信网页授权登录-react

    转:https://www.jianshu.com/p/9ced1a297c95 1.使用localhost本地调试 使用微信开发者工具, 选择微信网页授权, 微信团队为广大的开发者提供了一个测试账号 ...

最新文章

  1. 固态硬盘是什么接口_SATA接口和M.2接口的固态硬盘哪个好?SATA接口和M.2接口的区别...
  2. 基于量子粒子群算法实现天线阵列优化
  3. c++预处理命令 #line 用法
  4. C++Primer学习笔记:第8章 IO库
  5. 前端学习(1504):组件通信的几种情况
  6. cmd窗口使用python提示“Python not found”,可能是环境变量配置的原因
  7. Windows server 2008 R2 服务器安装 MySql 5.6.36 64位绿色版 报系统错误1067
  8. 香港中文大学(深圳)吴保元教授课题组博士后招聘
  9. JAVA读写文件模板
  10. [hdu2243]考研路茫茫——单词情结(AC自动机+矩阵快速幂)
  11. silverlight计时器
  12. 为数据库重新生成log文件
  13. AndroidStudio修改布局文件运行无效
  14. Introduction to Computer Networking学习笔记(十七):Switching and Forwarding 交换与转发
  15. 【微信公众号控制硬件14 】 分享安信可微信公众号定位NB-IoT模组的源码和实现过程,可实现远程查看模组定位位置。(附带源码)
  16. Android鞋店管理系统,鞋店进销管理系统下载
  17. 卡塞格林光学系统_改进型卡塞格林光学系统的设计
  18. 清明祭娭毑_原水_新浪博客
  19. SpotMicro 12自由度四足机器人制作(两套方案)
  20. html图片的title,图片标签IMG内alt和title属性

热门文章

  1. python实现根据前序序列和中序序列求二叉树的后序序列
  2. 手把手教你学五笔打字输入法之1(如何快速巧记五笔字型字根表篇)
  3. Excel作统计图表
  4. VSCODE常用快捷键
  5. 夜神模拟器与mac os之间共享文件
  6. 计算机wmi配置错误,系统没有WMI服务怎么办、WMI错误修复方法
  7. 【系】微信小程序云开发实战坚果商城-云开发之分类数据实现
  8. 前端,你需要掌握的重点!!
  9. 干货!纯干货! 手把手教你做云专线互联网备援接入-上集
  10. linux怎么开启8080端口,Linux中如何开启8080端口供外界访问