做过微信公众号开发的朋友都遇到这样一个实际问题,因为公众号的开发模式只能对接一个服务器接口地址,这就使的如果公司的业务比较多,那么就很难每个业务都直接对接到微信公众平台。而我们日常的需求中,这种情况是很普遍的一个现场。
以我们公司为例,我们是个创业公司,经常会有一些小创意,一般来说这些创意都需要获取用户的微信信息,如姓名、头像等;如果我需要把每个新创意都替换到公众号开发模式的接口的话,那么我之前的业务就没法使用了。这种现场在很多公司都实际存在。
今天这篇文章就来设计个方案解决这个问题。
主体的方案就是,设计一个统一账户系统(下称 account api),公司的所有业务都通过 appId, appSecret 的方式对接到 account api 上,用户 oAuth 认证的业务全部交由 account api 来完成;为了保证业务系统和 account api 之间的安全问题,采用 jsonwebtoken 认证方案。这样,我们仅把 account api 对接到微信公众号即可。
注:本例子使用 NodeJS 作为代码说明,同时下面的代码并没有在进行过测试,并不能直接在生产环境上使用,请读者根据思路自行进行测试和整理
➜  mkdir account_system
➜  cd account_system
➜  express
➜  npm i
➜  npm i --save wechat-oauth jsonwebtoken mongoose mongoose-timestamp request
.
├── README.md
├── app.js
├── bin
├── config
├── controllers  // ctrl 文件,主体的流程在该目录下
├── libs  // 对 mongoose、wechat-oauth 进行了配置
├── models  // 数据库设计
├── node_modules
├── package.json
├── public
├── routes  // 路由
├── services  // services
├── test
└── views
account api 主要提供下面两个接口
const ctrl = require('../controllers/user');
router.get('/account/auth', ctrl.verifyClient, ctrl.verifyToken, toAuth); // 第三方客户端请求的接口
router.get('/account/oauth/callback', ctrl.OAuthCallback, backToThirdClient); // 微信回调接口
我们给业务系统分配 appId, appSecret 并记录在数据库中
``` const tSchema = new Schema({ appId: String, appSecret: String, callbackUrl: String // 业务系统请求时,需要提供回调地址 });
const ThirdClient = mongoose.model('ThirdClient', tSchema); module.exports = thirdClient; ```
业务系统在请求时,使用分配的 appSecret 加密 appId,并提供回调地址
``` const JWT = require('jsonwebtolen');
exports.toLogin = function(req, res, next) {
const token = JWT.sign({
    appId: YOUR_APP_ID
}, YOUR_APP_SECRET);
res.redirect(`${ACCOUNT_URL/account/auth}?token=${token}&redirectUrl=${REDIRECT_URL}&appId=${APP_ID}`);
}; ```
先查找本地业务系统记录
``` exports.verifyClient = function(req, res, next) { const appId = req.query.appId;
ThirdClientModel.findOne({
    appId: appId
}).exec().then(ret => {
    if (ret) {
        req.client = ret;
        next();
    } else {
        res.send({
            code: 0,
            msg: 'your appId not exist, please contact to your system administrator'
        });
    }
}, err => {
    next(err);
})
}; ```
再对业务系统的 token 进行认证
``` exports.verifyToken = function(req, res, next) { const token = req.query.token; const client = req.client; const redirectUrl = req.query.redirectUrl;
JWT.verify(token, client.appSecret, (err, decoded) => {
    if (err) {
        next(err);
    } else {
        // verify passed
        if (decoded.appId === client.appId) {
            client.redirectUrl = redirectUrl;
            client.save().exec().then(ret => {
                req.client = ret;
                next();
            }, err => {
                next(err);
            })
        } else {
            next(new Error('appId not match'));
        }
    }
})
} ```
全部认证通过后,进行微信授权,我们将业务系统的记录 ID 设置为 state, 这样在微信回调后我们就可以找到对应的业务系统了
``` exports.toAuth = function(req, res, next) { const client = req.client;
const url = WechatOAuthService.getOAuthUrl(Config.wechatmp.oAuthCallbackUrl, client.id, Config.wechatmp.scope);
redirect(url);
} ```
通过微信 OAuth 授权,可以获取当前用户的信息。 微信授权主要分为三步:
1. 获取 code
2. 根据 code 获取 access_token, openid
3. 根据第 2 步获取的内容去微信获取用户完整信息
```
    /**
     * 根据 code 换取 token
     */
    getToken() {
        return new Promise((resolve, reject) => {
            const apiUrl = "https://api.weixin.qq.com/sns/oauth2/access_token";
            const requestUrl = ${apiUrl}?appid=${Config.wechatmp.appId}&secret=${Config.wechatmp.appSecret}&code=${this.code}&grant_type=authorization_code;
            Request(requestUrl, (error, response, body) => {
                if (!error && respons.statusCode === 200) {
                    resolve(JSON.parse(body));
                } else {
                    reject(error);
                }
            });
        });
    }
/**
 * 根据 accessToken, openId, lang 换取用户信息
 */
getUser(accessToken, openId, lang) {
    return new Promise((resolve, reject) => {
        const apiUrl = "https://api.weixin.qq.com/sns/userinfo";
        const reqUrl = `${apiUrl}?access_token=${accessToken}&openid=${openId}&lang=${lang}`;
        Request(reqUrl, (error, response, body) => {
            if (!error && response.statusCode === 200) {
                if (body && body.errorcode) {
                    reject(new Error(body));
                } else {
                    resolve(JSON.parse(body));
                }
            } else {
                reject(error);
            }
        });
    });
}
```
微信回调后,我们依次使用上面的方法,获取到当前用户的微信信息,然后我们将其记录(或更新)在数据库中
``` // query user info from wechat server const wechatOAuthSecvice = new WechatOAuthService(code); const accessToken = yield wechatOAuthSecvice.getToken(); const userFromWechat = yield wechatOAuthSecvice.getUser(accessToken.accesstoken, accessToken.openid, 'zhCN');
    // query user info from local database
    const userService = new User(userFromWechat.openid);
    const userFromLocal = userService.getUser();
    // update if exist, else add new one
    if (userFromLocal) {
        return yield UserService.updateUser(userFromLocal, userFromWechat);
    } else {
        return yield UserService.saveUser(userFromWechat);
    }
```
通过 state 我们可以找到对应的业务系统数据,我们使用业务系统的 appSecret 对用户数据进行加密,并回调业务系统
``` // query third client record const client = yield ThirdClientModel.findById(state).exec();
    if (client && client.callbackUrl) {
        const token = yield JWT.sign({
            appId: client.appId,
            user: user
        }, client.appSecret);
        return yield Promise.resolve({
            callbackUrl: client.callbackUrl,
            token: token
        });
    } else {
        return yield Promise.reject(new Error('third client not exist'));
    }
```
至此,我们完成了 account api 的完整设计,因为代码在写的时候比较仓促,仅为说明思路,所以并没有进行测试,读者朋友可以自行进行整理和测试;
详细代码,我已经上传到 Github, 项目地址
感兴趣的朋友也可以直接在微博上找到我进行交流,我的微博