若依框架前后端分离版基础上扩展微信扫码登录功能
# 前言
最近在学习java相关知识,朋友推荐看一下若依框架,于是在若依框架的基础上扩展了一下微信登录功能。
# 实现思路
标准的扫码登录需要申请微信开放平台账号,并拥有一个已审核通过的网站应用,即:网站应用微信登录,流程比较麻烦。这里为了省事就基于微信公众号网页授权来进行登录操作,同样也可以学习一下扫码登录的流程。
基于公众号网页授权进行扫码登录我们只需要准备一个公众号(需要认证过的服务号或者直接申请测试公众号)。
因为若依框架已经自带了用户表,所以我们这里的扫码登录要拆分成两部分来开发:
- 若依用户微信绑定
- 若依用户微信扫码登录
# 实现过程
# 微信绑定开发
微信绑定的场景是我们需要先登录若依系统,然后打开个人信息页面,在页面上面点击微信绑定按钮弹出二维码,然后手机微信扫码完成绑定。
绑定部分的扫码流程大致如下:
PC 端点击微信绑定按钮后从当前登录token获取到用户的key,基于用户的key拼接链接在前端弹出一个二维码,二维码地址(附带了生成的用户key)是网页授权链接,微信扫码打开后直接进行公众号网页授权,获取 code 后重定向给后端接口来拿到用户 openid 更新到用户表中完成绑定。
基于此流程结合若依框架我们需要开发如下几个接口并修改页面:
- 微信openid绑定
- 用户uuid查询绑定状态及openid
- 个人信息页追加微信绑定功能
因为需要存取用户微信信息,所以修改代码之前我们需要先向 sys_user
用户表追加两个字段:wx_nick_name、openid。
# 微信openid绑定开发
- 在
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/
下面新建WxController.java
用于处理微信相关请求。然后在WxController
中追加微信openid绑定方法:
/**
* 获取openid
*/
@GetMapping("/bind-openid")
public AjaxResult getOpenid(@RequestParam("code") String code, @RequestParam("key") String key) throws IOException
{
AjaxResult ajax = AjaxResult.success();
SysUser u = userService.getOpenid(code);
String openid = u.getOpenid();
LoginUser userCache = redisCache.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + key);
SysUser user = new SysUser();
user.setUserId(userCache.getUserId());
user.setOpenid(openid);
user.setWxNickName(u.getWxNickName());
userService.updateUserOpenid(user);
ajax.put("openid", openid);
ajax.put("wxnickname", u.getWxNickName());
return ajax;
}
方法里主要完成了微信公众号授权code的接收,并请求微信接口获取到用户openid,从当前用户登录信息redis缓存中拿到用户id然后把微信用户openid更新到用户表完成绑定。
这里为了方便,直接把code重定向给到了接口地址。
- 在
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
中追加微信code获取用户openid方法:
/**
* 通过code获取用户openid
*
* @param code 微信公众号网页授权码
* @return 用户openid
*/
@Override
public SysUser getOpenid(String code) {
RestTemplate restTemplate = new RestTemplate();
JSONObject jsonData = null;
// 构建获取access_token的URL
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?"
+ "appid=" + appId
+ "&secret=" + secret
+ "&code=" + code
+ "&grant_type=authorization_code";
System.out.println("url: " + url);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
System.out.println("responseEntity: " + responseEntity);
if (responseEntity.getStatusCodeValue() == 200 && responseEntity.getBody() != null) {
jsonData = JSONObject.parseObject(responseEntity.getBody());
}
String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo?lang=zh_CN"
+ "&access_token=" + jsonData.getString("access_token")
+ "&openid=" + jsonData.getString("openid");
ResponseEntity<String> responseUserEntity = restTemplate.getForEntity(userInfoUrl, String.class);
if (responseUserEntity.getStatusCodeValue() == 200 && responseUserEntity.getBody() != null) {
JSONObject jsonUserData = JSONObject.parseObject(new String(responseUserEntity.getBody().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
System.out.println("jsonUserData: " + jsonUserData);
SysUser user = new SysUser();
user.setOpenid(jsonUserData.getString("openid"));
user.setWxNickName(jsonUserData.getString("nickname"));
return user;
}
return null;
}
因为这里用到了公众号appid和secret,所以我们要在 ruoyi-admin/src/main/resources/application.yml
配置文件中追加相应配置:
# 公众号配置
wechat:
# 应用ID
appid:
# 应用密钥
secret:
追加配置之后需要在 SysUserServiceImpl
中补充对应私有属性:
@Value("${wechat.appid}")
private String appId;
@Value("${wechat.secret}")
private String secret;
- 在
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
中追加修改用户openid的方法:
/**
* 修改用户openid
*
* @param user 用户信息
* @return 结果
*/
@Override
public int updateUserOpenid(SysUser user)
{
return userMapper.updateUser(user);
}
这里需要我们在 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
中追加对应的方法声明:
/**
* 修改用户信息
*
* @param user 用户信息
* @return 结果
*/
public int updateUser(SysUser user);
并且在 ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
中补充更新对应的sql,即向更新用户sql的set部分追加微信openid及昵称的更新判断:
<update id="updateUser" parameterType="SysUser">
...
<if test="openid != null and openid != ''">openid = #{openid},</if>
<if test="wxNickName != null and wxNickName != ''">wx_nick_name = #{wxNickName},</if>
...
</update>
- 因为新增了微信openid及昵称的信息,所以需要修改一下
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
文件,追加微信昵称及openid属性:
/** 用户微信昵称 */
@Excel(name = "用户微信昵称")
private String wxNickName;
/** openid */
@Excel(name = "openid")
private String openid;
public String getOpenid()
{
return openid;
}
public void setOpenid(String openid)
{
this.openid = openid;
}
public String getWxNickName()
{
return wxNickName;
}
public void setWxNickName(String wxNickName)
{
this.wxNickName = wxNickName;
}
这样我们就完成了微信openid绑定接口的开发。
# 用户uuid查询绑定状态及openid
下面就是要查询微信绑定的信息了。
- 在
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/WxController.java
中追加户uuid查询openid方法:
/**
* 已登录用户uuid查询openid
*/
@GetMapping("/uuid")
public AjaxResult getCode(@RequestParam("uuid") String uuid) throws IOException
{
AjaxResult ajax = AjaxResult.success();
LoginUser userLogin = redisCache.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + uuid);
SysUser user = userService.selectUserById(userLogin.getUserId());
System.out.println("user-openid: " + user.getOpenid());
System.out.println("user-wxnickname: " + user.getWxNickName());
if (user.getOpenid() != null) {
ajax.put("openid", user.getOpenid());
ajax.put("wxnickname", user.getWxNickName());
}
ajax.put("status", user.getOpenid() != null ? 1: 0);
return ajax;
}
- 在
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
的selectUserVo查询sql中追加openid、微信昵称查询:
<sql id="selectUserVo">
select ...,
u.openid, u.wx_nick_name,
...
from sys_user u
</select>
这样查询到的用户信息里面就会包含绑定的微信信息了。
# 接口访问权限修改
因为扫码绑定的时候用户在手机上是没有登录的所有要在 ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
中把绑定接口加入到允许列表中:
requests.antMatchers("/login", "/register", "/captchaImage", "/bind-openid").permitAll()
到这里绑定需要开发的接口就修改完了,我们接着来修改页面。
# 个人信息页追加微信绑定
前端项目依赖追加。因为扫码需要生成二维码,所以这里为了方便使用前端插件
vue-qr
生成,在前端项目根目录npm install vue-qr
即可。追加接口。在
ruoyi-ui/src/api/system/user.js
中追加我们上面写好的查询绑定状态接口:
export function getUUid(data) {
return request({
url: '/uuid?uuid=' + data.uuid,
method: 'get'
})
}
- 页面修改。修改个人信息页
ruoyi-ui/src/views/system/user/profile/userInfo.vue
。
3.1 增加微信绑定按钮及弹窗:
<el-form-item label="微信">
<span class="mr10">{{form.wxnickname || '未绑定'}}</span>
<el-button v-if="form.openid == '' || form.openid == null" :loading="bindloading" size="medium" type="primary" class="btn" @click.stop="wxBind()">绑 定</el-button>
<el-button v-else :loading="bindloading" size="medium" type="primary" class="btn" @click.stop="unBind()">解
绑</el-button>
</el-form-item>
<el-dialog title="绑定微信" custom-class="bind-dialog" class="new-common-dialog" :visible.sync="bindWxVisible" :close-on-click-modal="false" :close-on-press-escape="false" @close="wxLoginClose" width="320px">
<div class="qr-code">
<vue-qr :text="qrUrl" :size="280"></vue-qr>
</div>
</el-dialog>
3.2 导入依赖接口及相关方法:
import {
updateUserProfile,
getUUid
} from "@/api/system/user";
import {
getToken
} from '@/utils/auth'
import vueQr from 'vue-qr'
3.3 引用二维码组件:
components: {
vueQr,
},
3.4 data中新增属性:
timer: null,
bindWxVisible: false,
bindloading: false,
qrUrl: '',
3.5 修改user监听方法,使页面能够回显微信昵称:
user: {
handler(user) {
console.log('user', this.user)
if (user) {
this.form = {
nickName: user.nickName,
wxnickname: user.wxNickName,
phonenumber: user.phonenumber,
email: user.email,
sex: user.sex,
openid: user.openid
};
}
},
immediate: true
}
3.6 增加微信绑定相关方法:
wxBind() {
let token = getToken()
const key = JSON.parse(atob(token.split('.')[1]))['login_user_key'];
const redirect_uri = `http://web-hub.uat.kai12.cn/auth.html?backUrl=${本机IP}:8080/bind-openid?key=${key}`
const codeUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=公众号appid&redirect_uri=${encodeURIComponent(redirect_uri)}&response_type=code&scope=snsapi_userinfo&state=123456#wechat_redirect`
this.qrUrl = codeUrl
console.log(this.qrUrl)
this.bindWxVisible = true
let that = this
this.timer = setInterval(function() {
getUUid({
uuid: key
})
.then((res) => {
console.log(res)
if (res.status === 1) {
that.bindWxVisible = false
that.form.openid = res.openid
that.form.wxnickname = res.wxnickname
that.$message({
type: 'success',
message: '操作成功',
})
clearTimeout(this.timer)
}
})
.catch((err) => {
clearTimeout(that.timer)
})
}, 1000)
},
wxLoginClose() {
this.timer && clearTimeout(this.timer)
},
这里主要是通过获取浏览器缓存的token,从中解析出若依用户的 login_user_key
来实现关联绑定的。
然后整个微信绑定流程就大致完成了。
# 微信扫码登录
实现思路
PC 端点击微信登录时生成一个 uuid 存入 redis 并弹出一个二维码,二维码地址(附带了生成的 uuid)是移动端的网页,微信扫码后打开的是配置好的网页授权链接,通过网页授权的方式获取 code 拿到用户 openid 后存入redis中,PC 端通过轮询方式根据生成的 uuid 查询用户 openid 进行登录。
基于以上实现思路结合若依框架需要开发如下几个接口并修改页面:
- uuid 生成
- uuid 绑定 openid
- openid/uuid 登录
- 登录页追加扫码登录功能
# uuid 生成
在 ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/WxController.java
中追加uuid 生成方法:
/**
* 扫码登录用uuid生成
*/
@GetMapping("/uuid/get")
public AjaxResult getUUID() throws IOException
{
AjaxResult ajax = AjaxResult.success();
String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.WX_OPENID_KEY + uuid;
redisCache.setCacheObject(verifyKey, null, 1, TimeUnit.MINUTES);
ajax.put("uuid", uuid);
return ajax;
}
方法里主要利用若依自带的方法生成UUID并存储到redis里(有效期设置为1分钟),需要在 ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java
中增加一个 WX_OPENID_KEY
常量用于存放openid:
/**
* 微信openid redis key
*/
public static final String WX_OPENID_KEY = "wx_openid:";
# uuid 绑定用户openid
同样的,在 WxController.java
中追加uuid绑定用户openid的方法:
/**
* uuid绑定openid
*/
@GetMapping("/uuid/bind/openid")
public AjaxResult bindOpenid(@RequestParam("code") String code, @RequestParam("uuid") String uuid) throws IOException
{
AjaxResult ajax = AjaxResult.success();
SysUser user = userService.getOpenid(code);
String openid = user.getOpenid();
String wxNickName = user.getWxNickName();
String verifyKey = CacheConstants.WX_OPENID_KEY + uuid;
long expire = redisCache.getExpire(verifyKey);
redisCache.setCacheObject(verifyKey, openid);
if (expire > 0) {
redisCache.expire(verifyKey, expire, TimeUnit.SECONDS);
}
ajax.put("openid", openid);
ajax.put("wxNickName", wxNickName);
return ajax;
}
该方法用于微信扫码时接收微信重定向过来的code以及点击扫码登录时产生的随机UUID,调用上一篇中 SysUserServiceImpl
追加的 getOpenid
方法获取到用户的openid和微信昵称,并把openid更新到对应UUID的redis缓存中。
# uuid 登录
在 WxController.java
中继续追加UUID登录方法:
/**
* uuid登录
*/
@GetMapping("/uuid/login")
public AjaxResult loginByOpenId(@RequestParam("uuid") String uuid) throws IOException
{
AjaxResult ajax = AjaxResult.success();
String verifyKey = CacheConstants.WX_OPENID_KEY + uuid;
String openid = redisCache.getCacheObject(verifyKey);
ajax.put("status", 0);
System.out.println("openid:{}" + openid);
if(openid != null) {
SysUser user = userService.selectUserByOpenId(openid);
System.out.println("用户:{}" + user);
if (user == null)
{
System.out.println("用户不存在");
return error("用户不存在");
}
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
// 生成token
String token = tokenService.createToken(loginUser);
ajax.put("token", token);
ajax.put("status", 1);
redisCache.deleteObject(verifyKey);
}
return ajax;
}
该方法用于在前端轮询随机生成的UUID,如果在缓存中查询到 openid
就使用 openid
查询用户信息,所以需要在 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
中补充通过openid查询用户的方法:
/**
* 通过openid查询用户
*
* @param openId 用户名
* @return 用户对象信息
*/
@Override
public SysUser selectUserByOpenId(String openId)
{
return userMapper.selectUserByOpenId(openId);
}
同时要在ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java
中追加对应的方法声明:
/**
* 通过openId查询用户
*
* @param openId openid
* @return 用户对象信息
*/
public SysUser selectUserByOpenId(String openId);
并且在 ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
中补充对应的查询sql,根据微信openid获取用户:
<select id="selectUserByOpenId" parameterType="String" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.openid = #{openid} and u.del_flag = '0'
</select>
如果能查询到用户就校验一下用户的权限并生成登录token一起返回给前端。
# 接口访问权限修改
扫码登录的接口都是在没有登录的时候调用的,所以要在 ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
中把相关接口加入到允许列表中:
requests.antMatchers("/login", "/register", "/captchaImage", "/bind-openid", "/uuid/get", "/uuid/login", "/uuid/bind/openid").permitAll()
到这里我们就完成了微信扫码登录所需接口的开发,我们接着来修改页面。
# 登录页追加微信扫码登录
- 追加接口。在
ruoyi-ui/src/api/system/user.js
中追加我们上面写好的接口:
// 查询登录用uuid
export function getLoginUUid() {
return request({
url: '/uuid/get',
method: 'get'
})
}
// uuid登录
export function uuidLogin(data) {
return request({
url: '/uuid/login?uuid=' + data.uuid,
method: 'get'
})
}
- 页面修改。修改登录页
ruoyi-ui/src/views/login.vue
。
2.1增加微信登录按钮及弹窗:
<div class="text-center">
<img class="mt10 wechat-icon" src="@/assets/images/wx-login.png" alt="微信登录" @click.stop="wxlogin()">
</div>
<el-dialog title="扫码登录" custom-class="bind-dialog" class="new-common-dialog" :visible.sync="bindWxVisible" :close-on-click-modal="false" :close-on-press-escape="false" @close="wxLoginClose" width="320px">
<div class="qr-code">
<vue-qr :text="qrUrl" :size="280"></vue-qr>
<div v-if="bindTimeout" class="tip text-center">
二维码已失效,请点击
<i class="el-icon-refresh" @click="wxlogin"></i> 刷新
</div>
</div>
</el-dialog>
2.2导入依赖接口及相关方法:
import {
getToken
} from "@/utils/auth";
import {
getLoginUUid,
getUUid,
uuidLogin
} from "@/api/system/user";
import vueQr from 'vue-qr'
2.3引用二维码组件:
components: {
vueQr,
},
2.4data中新增属性:
timer: null,
bindWxVisible: false,
bindTimeout: false,
qrUrl: '',
2.5 ruoyi-ui/src/store/modules/user.js
文件增加登录缓存方法:
// uuid登录
uuidLogin({
commit
}, userInfo) {
setToken(userInfo.token)
commit('SET_TOKEN', userInfo.token)
},
2.6登录页增加微信登录相关方法:
wxlogin() {
getLoginUUid().then(response => {
console.log(response.uuid)
const uuid = response.uuid
const redirect_uri = `http://web-hub.uat.kai12.cn/auth.html?backUrl=${本机IP}:8080/uuid/bind/openid?uuid=${uuid}`
const codeUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=公众号appid&redirect_uri=${encodeURIComponent(redirect_uri)}&response_type=code&scope=snsapi_userinfo&state=123456#wechat_redirect`
this.qrUrl = codeUrl
console.log(this.qrUrl)
this.bindWxVisible = true
let that = this
let counter = 1
this.timer && clearTimeout(this.timer)
this.timer = setInterval(function() {
uuidLogin({
uuid: uuid
})
.then((res) => {
console.log(res)
counter++
console.log(counter)
if (counter === 60) {
clearTimeout(that.timer)
that.bindTimeout = true
}
if (res.status === 1) {
clearTimeout(that.timer)
that.bindWxVisible = false
that.$message({
type: 'success',
message: '登录成功',
})
clearTimeout(that.timer)
that.$store.dispatch("uuidLogin", res)
setTimeout(() => {
that.$router.push({
path: that.redirect || "/"
}).catch(() => {});
}, 1500)
}
})
.catch((err) => {
that.bindWxVisible = false
clearTimeout(that.timer)
})
}, 1000)
});
},
wxLoginClose() {
this.timer && clearTimeout(this.timer)
},
这里主要是通过轮询随机生成的uuid后,手机扫码将code以重定向的方式给到后端的 uuid/bind/openid
接口同时完成UUID传参来实现登录。
然后微信扫码登录部分我们就完成了。