一、需求背景
最近公司来个新需求需要加一个钉钉扫码自动登录的功能,考虑到公司目前权限使用的框架是Shiro,开发过程中存在部分问题记录一下。
钉钉开放平台链接:钉钉开放平台
二、步骤
1.引入依赖
代码如下:
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10</version>
</dependency>
<dependency>
<groupId>com.alibaba.openplatform.shared</groupId>
<artifactId>sdk.client</artifactId>
<version>1.2.0</version>
</dependency>
备注:需要提前下载钉钉开发文档中的SDK(sdk.client)上传到公司的私服中。
下载地址:SDK下载地址
joda-time如果导入的话会报错。
2.熟悉扫码登录流程
3.获取appKey和appSecret
获取方式:获取appKey和appSecret
4.获取 access_token
获取方式:获取access_token
获取AccessToken我这里使用的是定时任务来获取每两小时获取一次,防止造成系统资源浪费。
/**
* 每两小时获取一次token存放在redis中
*/
@Scheduled(cron = "0 2 0/2 * * ?")
private void syncDingAccessToken(){
String accessToken = dingUtils.getAccessToken();
redisTemplate.opsForValue().set(redisKeyPrefix+dataCache+RedisKeyConstants.DING_ACCESS_TOKEN,accessToken,7200, TimeUnit.SECONDS);
}
@Resource
private DingConfigProperties properties;
/**
* 初始化Client
* @return
*/
private ExecutableClient initClient(){
//properties是@ConfigurationProperties(prefix = "ding")注解实现的配置绑定类,可以自己 //实现下。除非你是注解驱动开发小白。。
ExecutableClient executableClient = ExecutableClient.getInstance();
executableClient.setAccessKey(properties.getAppKey());
executableClient.setSecretKey(properties.getAppSecret());
executableClient.setDomainName(properties.getGetTokenDomain());
executableClient.setProtocal(properties.getProtocal());
executableClient.init();
//executableClient要单例,并且使用前要初始化,只需要初始化一次
return executableClient;
}
/**
* 获取accessToken
*/
public String getAccessToken() {
ExecutableClient executableClient = initClient();
//获取token的配置地址
String api = properties.getGetTokenApi();
PostClient postClient = executableClient.newPostClient(api);
String result = postClient.post();
log.debug("accessToken:{}",result);
JSONObject resultObject = JSONObject.parseObject(result);
JSONObject content = resultObject.getJSONObject("content");
if(!"0".equals(content.getString("responseCode"))){
throw new BusinessException("登录失败,未正常获取token");
}
return content.getJSONObject("data").getString("accessToken");
}
5.构建扫码登录界面
Web系统可以通过两种方式实现政务钉钉扫码登录。
方式一 使用政务钉钉提供的扫码登录页面
在企业Web系统里,用户点击使用钉钉扫码登录,第三方Web系统跳转到如下地址:
https://login.dg-work.cn/oauth2/auth.htm?response_type=code&client_id=应用标识&redirect_uri=回调地址&scope=get_user_info&authType=QRCODE
URL中的client_id和redirect_uri两个参数的值填入第三方web系统的应用标识和回调地址。政务钉钉用户扫码登录并确认后,会302到你指定的redirect_uri,并向url参数中追加临时授权码code(此code非authcode)及state两个参数。
注意事项:
参数"redirect_uri=回调地址"涉及的域名,需和创建扫码登录应用授权时填写的回调域名一致,否则会提示无权限访问。
生成二维码大小固定为200*200px,不支持修改。
方式二 支持网站将政务钉钉登录二维码内嵌到自己页面中
步骤1:在页面中通过iframe嵌入页面
通过方式一构造的地址增加embedMode=true的参数
https://login.dg-work.cn/oauth2/auth.htm?response_type=code&client_id=应用标识&redirect_uri=回调地址&scope=get_user_info&authType=QRCODE&embedMode=true
步骤2:扫码成功后需要在页面中监听扫码结果
<script type="application/javascript">
window.addEventListener('message', function(event) {
// 这里的event.data 就是登录成功的信息
// 数据格式:{ "code": "aaaa", "state": "bbbb" }
alert(JSON.stringify(event.data));
});
</script>
6.通过钉钉获取登录用户信息
/**
* 获取用户信息
* code:临时授权码。上一步获取的,主要是前端传给你。后端开发不用关心这个从哪里来。
*/
public JSONObject getUserInfo(String code,String accessToken){
ExecutableClient executableClient = initClient();
String api = properties.getGetUserInfoApi();
PostClient postClient = executableClient.newPostClient(api);
postClient.addParameter("access_token",accessToken);
postClient.addParameter("code",code);
String result = postClient.post();
log.debug("userInfo:{}",result);
JSONObject resultObject = JSONObject.parseObject(result);
JSONObject content = resultObject.getJSONObject("content");
if(!"0".equals(content.getString("responseCode"))){
throw new BusinessException(content.getString("responseMessage"));
}
return content;
}
7.Shiro免密登录功能配置
新建自定义UserNamePasswordToken类,继承UsernamePasswordToken
/**
* @类描述:钉钉扫码登录实现免密登录自定义类
* @Author:
*/
public class UserNamePasswordToken extends UsernamePasswordToken {
//用于判断是否需要免密登录
private boolean pwdCheck = true;
public void setPwdCheck(boolean pwdCheck) {
this.pwdCheck = pwdCheck;
}
public boolean getPwdCheck() {
return this.pwdCheck;
}
//构造方法调用父类初始化方法
public UserNamePasswordToken(String username,String password){
super(username,password);
}
public UserNamePasswordToken(String username){
super(username,"");
}
}
新建自定义密码匹配器类,集成HashedCredentialsMatcher类
/**
* 自定义密码认证匹配
*/
public class SysCredentialsMatch extends HashedCredentialsMatcher {
public SysCredentialsMatch() {
super();
}
public SysCredentialsMatch(String hashAlgorithmName) {
super(hashAlgorithmName);
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UserNamePasswordToken userNamePasswordToken = (UserNamePasswordToken) token;
//如果免密登录则直接返回true
if(!userNamePasswordToken.getPwdCheck()){
return true;
}
//否则按照之前验证密码
return super.doCredentialsMatch(token, info);
}
}
最后将Bean放到Spring容器中
@Bean
public SysCredentialsMatch customCredentialsMatch() {
//根据自身项目情况配置,不可完全复制
SysCredentialsMatch customCredentialsMatch = new SysCredentialsMatch();
customCredentialsMatch.setHashAlgorithmName("md5");
//此处配置修改需注意,会导致密码验证失败
customCredentialsMatch.setHashIterations(1024);
customCredentialsMatch.setStoredCredentialsHexEncoded(true);
return customCredentialsMatch;
}
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
//在自定义Realm中使用自定义的密码匹配器customCredentialsMatch
myShiroRealm.setCredentialsMatcher(customCredentialsMatch());
return myShiroRealm;
}
然后登录钉钉返回的用户信息在自己系统中进行判断,是否允许登录即可。