企业微信三方开发(三):网页授权登录
前言一
-
企业微信登录分两种:网页授权登录和扫码授权登录。
区别除了字面意思一个需要扫码一个不需要扫码外。还一个重要应用上的区别就是网页授权登录必须在微信客户端完成。
此文是关于如何实现网页授权登录的。 -
登录的整体逻辑分三段,从用户点击登录按钮开始:
- 点击登录按钮,访问OAuth2网页授权链接获取微信授权code【前端处理】
- 通过授权code换取用户成员票据user_ticket在内的用户信息【后端处理】
- 通过user_ticket获取用户敏感信息【后端处理】
-
这里OAuth2网页授权链接获取授权code在前端完成。我们前端使用uni-app框架,其好处是搭建方便、支持跨端、语言为现在很热门的vue。
-
微信企业三方开发访问授权链接不支持本地调试,官方意见是自己搭建线上调试平台,然后通过企业微信访问调试。这里我就只好用我自己线上的服务器和域名进行开发调试。
技术栈及工具
- 前端开发框架:uni-app
- 前端开发工具:HBulider
- 后端开发框架:spring-boot
- 后端开发工具: idea
一、OAuth2到底是什么?
在做授权登录时,无论是微信还企业微信,包括许多其它厂的登录。我们都会碰到一个概念——OAuth
通常也写作OAuth2.0,其中2.0是他的版本号
1.1、首先要对授权登录有个正确的概念
通常说到登录,就涉及两方:用户 和 应用
比如我在使用微信,那么我就是用户,微信app就是应用。
登录逻辑也很简单:我只需要注册用户名及密码到微信服务器,即可用用户名和密码登录
而授权登录则涉及三方: 用户,认证服务器 ,应用
比如我想使用CSDN,需要注册登录。如果我直接选择用我的微信账号登录,这就是一个授权登录过程。其中用户就是我,认证服务器就是微信的服务器,应用则是CSDN
其中微信的授权登录就是采用的OAuth 2.0授权码模式
1.2、OAuth2.0的授权码模式
OAuth是一套授权模式的统称,其中用的最多的就是授权码模式。
上图就是OAuth2.0授权码模式的流程,我们再结合微信授权登录CSDN的情形讲解一下:
- 首先我点击CSDN的微信登录按钮,CSDN服务器会构造OAuth2链接(具体链接参看微信文档,参数包括CSDN的身份ID,以及重定向URI,这个URI也就是CSDN登录成功的页面),进入到微信的授权页
- 我在微信的授权页点击同意授权,微信会跳转到重定向URI,也就是CSDN登录成功页并附上授权码
- 进入登录成功页时,CSDN服务器会拿着授权码以及调用凭证AccessToken(由微信指定接口获取)向微信服务器获取我的基本信息。整个授权过程完成。
可以看到OAuth2.0很明显的优点,CSDN完全不用知道更不需要存储我的微信账号和密码,就能判断我的微信就是我的微信,即我就是我!
二、网页授权
明白了OAuth2.0,下面构建OAuth2.0链接以及其中的参数也就不需要多讲。
2.1、新建uni-app项目
因为正好在用uni-app做小程序,所以这里就选择了这个框架,具体创建方式直接查看官方教程,这里我们已经创建了一个叫easyou-agency的项目。
这里前端用什么框架不重要,关键是需要发送http请求
2.2、 构建网页授权链接
注意参数 scope,主要用于设置手动授权还是静默授权。 区别就是授权时需不需要点击授权按钮。
如果选择手动授权,需给应用配置权限
接下来继续写代码,先新建两个页面,index和login
login页面需要访问构建的URL:
<template>
<view style="width: 96%;margin: 0 auto;">
<view>
<form @submit="doLogin">
<button plain="true" class="loginBtn" lang="zh_CN" form-type="submit">登录</button>
</form>
</view>
</view>
</template>
<script>
export default {
data() {
return {
usercode: '',
password: '',
passwordHidden: true
}
},
onLoad:function(){
},
methods: {
doLogin() {
let _this = this
localStorage.setItem("hasLogin",true)
// 这里放自己的域名
let redirect_uri = encodeURI("www.xxxx.com")
let authorUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=ww8273b1801b97c577&redirect_uri='+redirect_uri+'&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect'
window.location.href = authorUrl
}
}
}
</script>
<style>
.loginBtn{
color: #337bd4!important;
border:1rpx solid #337bd4!important;
font-size: 30rpx;
border-radius: 50rpx;
}
</style>
redirect_uri 用的是我线上的域名,也就是我安装好的应用的主页url。在应用详情里配置:
index页是打开应用时缺省进入的页面,需要做三件事:
- 通过storage判断是否是登录状态,不是回到login页
- 再判断url参数是否带code
- 如果带code说明是网页授权过来的,将code发给后台换取成员票据user_ticket和用户信息
index页如下:
<template>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<view class="text-area">
<text class="title">{{title}}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: this.$route.query.code
}
},
onLoad() {
if(localStorage.getItem("hasLogin")=='false'){
uni.redirectTo({
url: '../login/login'
});
}
if(this.$route.query.code){
// 从静模授权进入首页
localStorage.setItem("auth_code",this.$route.query.code)
this.getuserinfo3rd()
}
},
methods: {
// 获取敏感信息
getuserinfo3rd:function(){
uni.request({
url: 'http://tantan.vaiwan.com/login/getuserinfo3rd.do?code='+this.$route.query.code,
method:'POST',
success: function (res) {
console.log("res:"+res);
}
})
}
}
}
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>
自此前端部分就完成了,生成发行版本并上传至自己服务器,并配置好域名指向index。
企业微信能够成功访问index,由于storage的登录状态为false,所以跳转到login页面
企业微信网页应用有前端调试插件,安装方法查看官方教程
点击登录按键:
成功跳转到首页
请求链接携带code,说明构建网页授权链接成功!
如果是手动授权,则会先跳转到授权页:
此时就要手动点即授权按钮授权
三、获取用户信息及敏感信息
在1.2中我们在index页中有个getuserinfo3rd函数向后台发送ajax请求,参数携带了网页授权成功的code。此时我们就要响应并通过code获取用户票据user_ticket,再通过user_ticket获取用户敏感信息。
新建一个LoginController:
package com.tan.cwp.controller;
import com.tan.cwp.util.HttpHelper;
import com.tan.cwp.util.PropertiesUtil;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@RestController
@RequestMapping("/login")
public class LoginController {
/**
* 获取访问用户userid
*
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/service/getuserinfo3rd?suite_access_token=SUITE_ACCESS_TOKEN&code=CODE
* 请求方式: GET
* @parm1 SUITE_ACCESS_TOKEN
* @parm2 oauth2授权成功返回的code
*/
@RequestMapping(value = "getuserinfo3rd.do" ,method = RequestMethod.POST)
public static void getUserInfo3rd(HttpServletRequest request,HttpServletResponse response) throws IOException, JSONException {
String code = request.getParameter("code");
String suite_access_token = PropertiesUtil.getProperty("suite_access_token");
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/getuserinfo3rd?suite_access_token="+suite_access_token+"&code="+code;
JSONObject jsonObj = HttpHelper.doGet(url);
JSONObject result = getUserDetail3rd((String) jsonObj.get("user_ticket"));
// 将信息发送至前台
PrintWriter out = response.getWriter();
out.print(result);
out.close();
}
/**
* 获取访问用户敏感信息
*
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/service/getuserdetail3rd?suite_access_token=SUITE_ACCESS_TOKEN
* 请求方式: POST
* @parm1 SUITE_ACCESS_TOKEN
* @parm2 getUserInfo3rd获得的user_ticket
*/
public static JSONObject getUserDetail3rd(String user_ticket) throws JSONException, IOException {
String suite_access_token = PropertiesUtil.getProperty("suite_access_token");
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/getuserdetail3rd?suite_access_token="+suite_access_token;
JSONObject jsonParms = new JSONObject();
jsonParms.put("user_ticket", user_ticket);
JSONObject jsonObj = HttpHelper.doPost(url,jsonParms);
return jsonObj;
}
}
通过两个函数先后获取用户普通信息和敏感信息。
保存并重新运行
回到企业微信点击登录按钮发现报错了:
一个标标准准的跨域错误
这里的调用域名是我线上的域名www.xxxx.com,被调用域名是我本地的 http://tantan.vaiwan.com,所以跨域了。解决跨域通常有两种思路:
- 从调用方出发:通过http服务器(nginx或apache)将被调用域名反向代理到同一个域名
- 从被调用方出发:通过过滤器增加请求头,允许指定域名可以跨域
我们选择第二种方法新增过滤器。
新增filter包,加个叫CrosFilter的过滤器:
package com.tan.cwp.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CrosFilter implements javax.servlet.Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse)servletResponse;
// 允许所有域名跨域
res.addHeader("Access-Control-Allow-Origin","*");
res.addHeader("Access-Control-Allow-Methods","GET,POST,PUT,DELETE,OPTIONS");
res.addHeader("Access-Control-Allow-Headers","Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
在 CwpApplication 中将过滤器配上:
package com.tan.cwp;
import com.tan.cwp.filter.CrosFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class CwpApplication {
public static void main(String[] args) {
SpringApplication.run(CwpApplication.class, args);
}
@Bean
public FilterRegistrationBean registerFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
// 过滤所有访问
bean.addUrlPatterns("/*");
bean.setFilter(new CrosFilter());
return bean;
}
}
重启项目,企业微信点击登录:
成功获取到用户普通信息和敏感信息!
总结
在获取用户信息中,有个open_userid返回值。这个是用户在应用内的唯一标识,需存入数据库用作用户登录的凭证。