共计 5181 个字符,预计需要花费 13 分钟才能阅读完成。
引言
用户登陆时,原设计是使用工号加密码进行登陆,只是工号不好记,为了推广,设计了企业微信登陆。
企业微信中可以设置自建应用,其实就是内嵌了一个Chrome
,点击左侧的自建应用,会在右侧浏览器显示相关应用,所有工作都放在企业微信中,需实现当前企业微信账号自动登陆系统。
开发的过程很坎坷。让微信折腾的一点脾气都没有。
当时不会调试,因为企业微信中的自建应用要求设置成线上地址,写好了,打包,传给服务器,然后再测试。
五点,觉得还有十分钟就写完了,写完了就去吃饭。
六点、八点,改到九点半,都要改哭了,还是不好使,最后放弃了。
后来邢彦年师兄帮我梳理流程,潘老师教我调试方法,才完成这个功能。
感谢邢彦年师兄和潘老师。
实现
文档
找文档一定要找对地方,两个API
,一个服务端,一个客户端。
最开始我以为是使用客户端的 API
呢?点进去学了学,企业微信小程序可用的 API
接口,这个用不了,此应用不是小程序。然后 JS-SDK
不支持登陆授权。
相关文档在服务端 API
中的身份认证一节中。
OAuth 2.0
当我们用微信登陆某个网站时,会出现类似这样的授权页面。
点击确认登陆,该应用就能获取到用户相关的信息。
用户通过用户名和密码的方式进行微信的使用,第三方应用想获取微信用户信息,不是通过用户名密码,而是微信提供的令牌,这就是OAuth 2.0
,既可以让应用获取不敏感的信息,又可以保证账户的安全性。
更多内容可学习阮一峰的博客,写得非常好:OAuth 2.0 的一个简单解释 – 阮一峰的网络日志
登陆流程
用户点开应用,实际上是访问当前系统微信授权的入口
微信网页授权地址:https://open.weixin.qq.com/connect/oauth2/authorize
参数 | 说明 |
---|---|
appid |
企业的CorpID
|
redirect_uri |
授权后的回调地址,需使用 urlencode 处理
|
response_type |
回调数据返回类型 |
scope |
应用授权作用域。企业自建应用固定填写:snsapi_base
|
state |
回调时额外的参数,非必选 |
#wechat_redirect |
终端使用此参数判断是否需要带上身份信息 |
看这个表格也是在无聊,下面是我配置好的微信授权链接,大家只需将相应参数改写即可。注:回调的 url
一定要使用 encodeURIComponent
处理!
https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxxxxxxx&redirect_uri=https%3A%2F%2Falice.dgitc.net%2Fsetup%2Fwechat&response_type=code&scope=snsapi_base#wechat_redirect
用户静默授权成功,跳转到回调地址
用户授权成功后,会带着 code
参数重定向到回调地址。
类似这样:
https://alice.dgitc.net/setup/wechat?code=xxxxxx
前台的组件就通过路由获取到了 code
,然后通过code
去进一步获取用户信息。
const code = this.route.snapshot.queryParamMap.get('code');
this.userService.enterpriseWeChatLogin(code).subscribe(...);
后台通过 code
找微信后台获取用户信息
这个是分成两次获取,先获取 access_token
,再通过access_token
和code
获取用户信息。
GET 请求 https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET
这个是获取 access_token
的地址,获取 access_token
的过程是重点!
上面传的参数名是 corpid
和corpsecret
,企业 id
和密钥。
这是企业微信的设计,企业 id
是一个,标识这个企业,每一个功能模块都有相应的secret
。
然后企业 id
和secret
配对,获取到只能访问这个功能模块的一个access_token
。
就拿当前 Alice
系统来举例,自建应用 Alice
存在 secret
,通过此secret
和corpid
获取到 access_token
,即相当于拿到了受保护API
的访问权限。
因为这个 access_token
是通过 Alice
应用的 secret
获取到的,所以再用它访问其他的功能,是不合法的。
access_token
有访问频率限制,所以设计了一套缓存方案。
@Override
public String getAccessTokenBySecret(String secret) {logger.debug("从缓存中获取令牌");
String access_token = this.cacheService.get(secret);
logger.debug("如果获取到了,直接返回");
if (null != access_token) {return access_token;}
logger.debug("否则,发起 HTTP 请求");
logger.debug("获取企业验证信息");
String url = enterpriseInformation.getAccessTokenUrl();
String corpId = enterpriseInformation.getCorpid();
logger.debug("获取认证令牌");
ResponseEntity<EnterpriseAuth> responseEntity = restTemplate.getForEntity(url + "?corpid=" + corpId + "&corpsecret=" + secret, EnterpriseAuth.class);
logger.debug("如果请求成功");
if (responseEntity.getStatusCode().is2xxSuccessful()) {logger.debug("获取响应体");
EnterpriseAuth enterpriseAuth = responseEntity.getBody();
Assert.notNull(enterpriseAuth, "微信令牌为空");
logger.debug("如果微信请求成功");
if (enterpriseAuth.successed()) {logger.debug("存储缓存,返回令牌");
access_token = enterpriseAuth.getAccessToken();
this.cacheService.put(secret, access_token, enterpriseAuth.getExpiresIn(), TimeUnit.SECONDS);
return access_token;
}
}
logger.debug("请求失败,返回空令牌");
return "";
}
缓存是通过把 Redis
工具类包装了一下实现的,很简单。
@Service
public class CacheServiceImpl implements CacheService {
/**
* Redis 操作模版
*/
private final StringRedisTemplate redisTemplate;
private final ValueOperations<String, String> valueOperations;
@Autowired
public CacheServiceImpl(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.valueOperations = redisTemplate.opsForValue();}
@Override
public void put(String key, String value, Long time, TimeUnit timeUnit) {this.valueOperations.set(key, value, time, timeUnit);
}
@Override
public String get(String key) {return this.valueOperations.get(key);
}
@Override
public void clear(String key) {this.redisTemplate.delete(key);
}
}
access_token
和 code
都有了,终于可以获得用户信息了。
GET 请求 https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
这个简单了,问题出在数据不规范的问题上,大写的 json
,如果按这样建,实体就不符合java
规范了。
{
"errcode": 0,
"errmsg": "ok",
"UserId":"USERID",
"DeviceId":"DEVICEID"
}
最后在字段添加 JsonAlias
注解。
@JsonAlias("UserId")
private String userId;
这里获取到的信息是用户的 userId
,这个userId
和我们熟知的 openId
是不一样的。
userId
就是管理员为用户在企业内设置的唯一标识,企业内唯一。
具体的后台获取 userId
的细节:
@Override
public Boolean enterpriseWeChatLogin(String code) {logger.debug("构造参数");
String secret = this.enterpriseInformation.getAgentSecret();
String access_token = this.aliceCommonService.getAccessTokenBySecret(secret);
String url = this.enterpriseInformation.getUserInfoUrl() + "?code=" + code + "&access_token=" + access_token;
logger.debug("请求用户信息");
ResponseEntity<EnterpriseUser> responseEntity = this.restTemplate.getForEntity(url, EnterpriseUser.class);
logger.debug("如果请求成功");
if (responseEntity.getStatusCode().is2xxSuccessful()) {logger.debug("获取响应体");
EnterpriseUser enterpriseUser = responseEntity.getBody();
Assert.notNull(enterpriseUser, "企业用户不能为空");
logger.debug("如果企业微信端也成功了");
if (enterpriseUser.successed()) {logger.debug("获取 userId");
String wxId = enterpriseUser.getUserId();
logger.debug("如果有 userId,说明当前用户存在此企业中");
if (null != wxId) {// 请自行填充登陆的具体细节}
} else if (enterpriseUser.tokenInvalided()) {logger.debug("token 不合法,清空缓存,重新获取");
this.cacheService.clear(secret);
return this.enterpriseWeChatLogin(code);
}
}
logger.debug("其他一律返回 false");
return false;
}
前台跳转,登陆成功
前台在订阅中添加跳转方法,登陆成功后,跳转到首页,登陆失败,401
走拦截器,跳转到登陆页。
this.userService.enterpriseWeChatLogin(code)
.subscribe(() => {this.userService.setCurrentLoginUser();
this.router.navigateByUrl('main');
}, () => {console.log('network error');
});
改Host
一定要记住,非常重要!!!
以后再碰到只能访问线上地址的情况,想在本地调试,一定要改Host
!!!
总结
最后的总结就是多想想潘老师评论的这句话,有些路总不过去,很大的可能是方法错了。