企业微信 网页授权登入

企业微信 网页授权登入

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

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

OAuth2简介

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

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

企业微信 网页授权登入_第1张图片

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

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

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

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

企业微信OAuth2接入流程

img
图1 企业微信OAuth2流程图

使用OAuth2前须知

关于网页授权的可信域名

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

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

关于UserID机制

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;
    }



}

总结

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

你可能感兴趣的:(企业微信)