官网地址:https://work.weixin.qq.com/api/doc/90000/90135/91020
企业微信提供了OAuth的授权登录方式,可以让从企业微信终端打开的网页获取成员的身份信息,从而免去登录的环节。
企业应用中的URL链接(包括自定义菜单或者消息中的链接),均可通过OAuth2.0验证接口来获取成员的UserId身份信息。
OAuth2的设计背景,在于允许用户在不告知第三方自己的帐号密码情况下,通过授权方式,让第三方服务可以获取自己的资源信息。
详细的协议介绍,开发者可以参考RFC 6749。
下面简单说明OAuth2中最经典的Authorization Code模式,流程如下:
流程图中,包含四个角色。
调用流程为:
A) 用户访问第三方服务,第三方服务通过构造OAuth2链接(参数包括当前第三方服务的身份ID,以及重定向URI),将用户引导到认证服务器的授权页
B) 用户选择是否同意授权
C) 若用户同意授权,则认证服务器将用户重定向到第一步指定的重定向URI,同时附上一个授权码。
D) 第三方服务收到授权码,带上授权码来源的重定向URI,向认证服务器申请凭证。
E) 认证服务器检查授权码和重定向URI的有效性,通过后颁发AccessToken(调用凭证)
D)与E)的调用为后台调用,不通过浏览器进行
图1 企业微信OAuth2流程图
REDIRECT_URL中的域名,需要先配置至应用的“可信域名”,否则跳转时会提示“redirect_uri参数错误”。
要求配置的可信域名,必须与访问链接的域名完全一致;若访问链接URL带了端口号,端口号也需要登记到可信域名中。举个例子:
配置域名 | 是否正确 | 原因 |
---|---|---|
mail.qq.com:8080 | 配置域名与访问域名完全一致 | |
email.qq.com | 配置域名必须与访问域名完全一致 | |
support.mail.qq.com | 配置域名必须与访问域名完全一致 | |
*.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信息,如果需要获取用户的更多信息可以调用 通讯录管理 - 成员接口 来获取。
通过OAuth2.0验证接口获取成员身份会有一定的时间开销。对于频繁获取成员身份的场景,建议采用如下方案:
1、企业应用中的URL链接直接填写企业自己的页面地址
2、成员操作跳转到步骤1的企业页面时,企业后台校验是否有标识成员身份的cookie信息,此cookie由企业生成
3、如果没有匹配的cookie,则重定向到OAuth验证链接,获取成员的身份信息后,由企业后台植入标识成员身份的cookie信息
4、根据cookie获取成员身份后,再进入相应的页面
maven依赖
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.1.RELEASEversion>
<relativePath/>
parent>
<groupId>com.kuanggroupId>
<artifactId>spring-outhartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>spring-outhname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.0version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.4.1version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.yml
wechat:
cp:
# 企业ID
corpId: xxxxxxx
# 应用的id
agentId: xxxx
# 应用的凭证密钥
secret: xxxxxxx
spring:
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
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; //自建应用的密钥
@Autowired
private 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")
@ResponseBody
public 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存在session
session.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")
@ResponseBody
public 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
{
@Autowired
public 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; //自建应用的密钥
@Autowired
private 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;
}
}