前言:还是一如既往的写作前唠叨,作为一个java编程的学习者。我也是在写vue+springboot的前后端分离项目的空余时间简单的写了一个微信小程序的后端代码,于是就有了这一篇文章,主要记录怎么实现,以及出现的问题。
一、了解微信登录
1、微信官方文档
开放能力 / 用户信息 / 小程序登录 (qq.com)https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
(1)官方说明
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key。
2、个人理解
(1)如何判断已经登录
首先微信小程序前端通过调用wx.login()这个接口,来获取用户临时登录code,同时将这个code传递给后端,后端通过这个code去调微信开发官方开放的接口来获取当前用户的唯一标识:openId以及其他信息,并将它们存储到数据库中及进行数据持久化。
(2)如何进行用户权限控制
将前端传递的code拿来利用SpringSecurity安全框架、JWT、Sa-Token等框架生成token进行用户登陆后的授权、鉴权等等。
理论存在,开始实操!开干,冲冲冲。。。。。
二、前端代码实现
1、编码前言
由于本人是后端方向的,对于前端代码不是很会,只能写一个很简略的demo,作为访问后端接口实现登录后传递code的测试。前端的友友们请手下留情!
2、前端语言及开发工具
我是用的是uni-app进行写微信小程序的前端测试,开发工具是HBuilder。
3、实现逻辑
调用官网提供的登录接口uni.login() —> 获取code —> 调用后端接口(返回code)
4、源码
index.vue:
<template> <view> <button @click="login">微信登录</button> </view></template><script>import { loginByWechat } from '@/utils/index.js'export default { methods: { login() { uni.getUserProfile({ desc: '登陆后同步数据', success(ures) { uni.login({ success(lres) { let params = { code: lres.code } console.log("登录获取得code:", lres.code) loginByWechat(params).then(res => { // 未注册过 console.log(res); if (res.code === '200') { console.log('后端传递得数据',res) console.log('未注册过') } else { console.log('已经登录过') } }).catch(err => { console.error(err) }) } }) } }) } }}</script><style></style>
index.js:
//微信一键登录// utils/index.jsexport const loginByWechat = (data) => { return new Promise((resolve, reject) => { uni.request({ url: 'http://localhost:4000/api/user/login', method: 'GET', // 根据您的接口要求修改为相应的请求方法 data: data, success: (res) => { resolve(res.data) }, fail: (err) => { reject(err) } }) })}
我写的登录的前端代码就只有这么多了,确实很简陋。
5、遇见的问题
(1)跨域问题
我这是一台电脑实现的前端调用后端接口,不用考虑跨域的问题。如果出现前后端不在同一个端电脑上会出现无法调用接口;
(2)唯一标识和会话密钥问题
跨域进行调用接口时候,唯一标识UnionID和会话密钥 session_key如果还是用的我自己的,会出现无法调用微信服务器接口,就需要将唯一标识和会话密钥换成前端的,才可以,我自己认为这样很不合理,暂时还没有找到原因。
三、后端代码
1、前端展示VO对象
LoginVO
@Datapublic class LoginVO implements Serializable { @ApiModelProperty("微信登陆用户的信息") private WxUser wxUserInfo; @ApiModelProperty("token") private String Authorization; @ApiModelProperty("错误码") private int errcode; @ApiModelProperty("警告信息") private String errmsg;}
2、Service业务逻辑
(1)LoginService接口
/** * 使用微信端的临时code登录 * @param code * @return */ LoginVO checkLogin(String code);
(2)LoginServiceImpl接口实现层
/** * 微信登录验证 * * @param code 临时登录码 * @return . */ public LoginVO checkLogin(String code) { //防止后面找不到了,官网地址:https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/login/auth.code2Session.html // 根据传入code,调用微信服务器,获取唯一openid // 微信服务器接口地址 String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code"; WeChatSessionModel weChatSessionModel; WxUser wxUser; LoginDTO loginDTO = new LoginDTO();//接口调用成功过后返回的参数 LoginVO loginVO = new LoginVO(); // 发送请求 ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, null, String.class); log.info("调用得微信得接口地址:{}",responseEntity.getBody()); // 判断请求是否成功 if (responseEntity != null && responseEntity.getStatusCode() == HttpStatus.OK) { // 获取主要内容 String sessionData = responseEntity.getBody(); Gson gson = new Gson(); //将json字符串转化为实体类; weChatSessionModel = gson.fromJson(sessionData, WeChatSessionModel.class); //转化为数据库中相同实体 wxUser = gson.fromJson(sessionData, WxUser.class); log.info("返回的数据==>{}", weChatSessionModel); //获取用户的唯一标识openid以及其他信息 BeanUtils.copyProperties(weChatSessionModel,loginDTO); } else { log.info("发现错误,错误信息:{}",loginDTO.getErrmsg() + "错误码:{}",loginDTO.getErrcode()); loginVO.setErrcode(loginDTO.getErrcode()); loginVO.setErrmsg(loginDTO.getErrmsg()); return loginVO; } // 判断是否成功获取到openid if (loginDTO.getOpenid().isEmpty() || loginDTO.getOpenid()==null) { log.info("错误获取openid,错误信息:{}", loginDTO.getErrmsg()); loginVO.setErrcode(loginDTO.getErrcode()); loginVO.setErrmsg(loginDTO.getErrmsg()); } else { // 判断用户是否存在,查询数据库 WxUser userInfo = wxUserMapper.selectOne(new LambdaQueryWrapper<WxUser>().eq(WxUser::getOpenid, loginDTO.getOpenid())); if (userInfo == null) { // 不存在,加入数据表 WxUser tempUserInfo = new WxUser(); BeanUtils.copyProperties(wxUser,tempUserInfo); tempUserInfo.setCreateTime(new Date()); tempUserInfo.setUpdateTime(new Date()); wxUserMapper.insert(tempUserInfo); map.put("user",tempUserInfo); // 创建token String token = JwtUtil.createToken(tempUserInfo.getId().toString()); loginVO.setAuthorization(token); } else { // 存在,将用户信息加入map返回 loginVO.setWxUserInfo(userInfo); String token = JwtUtil.createToken(userInfo.getId().toString()); loginVO.setAuthorization(token); } } return loginVO; }
3、控制器层(controller)
/** * 微信登录 * @param code 获取临时凭证code * @return 返回执行结果 */ @GetMapping("/user/login") @ApiOperation(value = "微信临时code登录") public Result<LoginVO> loginCheck(String code){ LoginVO loginVO = loginService.checkLogin(code); // 判断登录结果是否为空或不存在必要的信息 if (loginVO.getWxUserInfo() != null) { log.info("创建的token为=>{}", loginVO.getAuthorization()); return Result.success(loginVO,"登录成功"); } else { // 登录结果为空或缺少必要的信息,返回错误信息 return Result.error("登录信息验证失败"); } }
4、实体类
@Getter@Setter@TableName("wx_user")@ApiModel(value = "微信用户对象", description = "")public class WxUser implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Integer id; @ApiModelProperty("小程序用户openid,唯一") private String openid; @ApiModelProperty("用户昵称") private String nickname; @ApiModelProperty("性别 0 男 1 女 2 人妖") private Integer gender; @ApiModelProperty("用户头像") private String avatarurl; @ApiModelProperty("手机号码") private String telnum; @ApiModelProperty("创建时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone ="Asia/Shanghai" ) private Date createTime; @ApiModelProperty("更新时间") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone ="Asia/Shanghai" ) private Date updateTime; @ApiModelProperty("逻辑删除(0-未删除,1-已删除)") @TableLogic private Integer deleteFlag;}
四、测试登录
1、HBuilder测试
因为我直接写了前端代码的,就不使用swagger或者postman或者apifox进行模拟前端发送请求了
后台控制台中可以看到已经成功拉,因为我之前登录过,就不需要执行插入数据库操作
微信一键登录就完成了,回过头来向其实也没有好难,主要是之前复写security安全框架底层代码很打脑壳,避免太过复杂,所以我重新用JWT进行了token的生成。
2、问题
(1)yml数据无法获取
调接口的时候出现了有code但是却无法获取yml中的唯一id和会话密钥,导致无法正确访问微信服务器的接口。通过将唯一id和会话密钥抽象成一个类去映射yml中的值,然后将这个类装配到springboot的自动装配中去,进行拿值,不在使用直接用@Value从yml拿数据,问题解决。
(2)微信用户信息获取缺乏
能够获取到openId,但是也只能获取到openid,无法获取用户的相关信息,经过查阅官网发现,现在微信那边已经将用户信息加密,需要调用提供的接口进行解密然后获取用户数据,问题原因已经找到解决方法,还未即使更改。
五、JWT的配置类
@Component@Data@Slf4jpublic class JwtUtil { //静态资源注入到SpringBoot的自动装配中,不再使用@Resource或者@Autowired private static JwtAndWeiXinProperties jwtAndWeiXinProperties; private JwtUtil(JwtAndWeiXinProperties jwtAndWeiXinProperties){ JwtUtil.jwtAndWeiXinProperties = jwtAndWeiXinProperties; } /** * 创建TOKEN * * @param sub * @return */ public static String createToken(String sub) { return JWT.create() .withSubject(sub) .withExpiresAt(new Date(System.currentTimeMillis() + jwtAndWeiXinProperties.getExpireTime())) .sign(Algorithm.HMAC512(jwtAndWeiXinProperties.getSecret())); } /** * 验证token * * @param token */ public static String validateToken(String token) { try { return JWT.require(Algorithm.HMAC512(jwtAndWeiXinProperties.getSecret())) .build() .verify(token) .getSubject(); } catch (TokenExpiredException e) { log.info("token已过期"); return ""; } catch (Exception e) { log.info("token验证失败"); return ""; } } /** * 检查token是否需要更新 * @param token · * @return */ public static boolean isNeedUpdate(String token) { //获取token过期时间 Date expiresAt = null; try { expiresAt = JWT.require(Algorithm.HMAC512(jwtAndWeiXinProperties.getSecret())) .build() .verify(token) .getExpiresAt(); } catch (TokenExpiredException e) { return true; } catch (Exception e) { log.info("token验证失败"); return false; } //如果剩余过期时间少于过期时常的一般时 需要更新 return (expiresAt.getTime() - System.currentTimeMillis()) < (jwtAndWeiXinProperties.getExpireTime() >> 1); }}
自定义的登录拦截器
/** * description: 自定义登录拦截器 **/@Slf4jpublic class JwtAuthenticationTokenInterceptor implements HandlerInterceptor { @Resource private RedisTemplate<String,String> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception { // 获取请求头,header值为Authorization,承载token String token = request.getHeader("Authorization"); //token不存在 if (token == null || token.isEmpty()) { log.info("传入token为空"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token为空!"); return false; } //验证token String sub = JwtUtil.validateToken(token); if (sub == null || sub.isEmpty()){ log.info("token验证失败"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token验证失败!"); return false; } //更新token有效时间 (如果需要更新其实就是产生一个新的token) if (JwtUtil.isNeedUpdate(token)){ String newToken = JwtUtil.createToken(sub); response.setHeader("Authorization",newToken); redisTemplate.opsForValue().set("token",newToken,3600);//更新redis中的token } return true; }}
web拦截器
@Configurationpublic class WebMvcConfigurer extends WebMvcConfigurationSupport { //图片存放根路径 @Value("${file.rootPath}") private String ROOT_PATH; //图片存放根目录下的子目录 @Value("${file.sonPath}") private String SON_PATH; /** * 注册自定义拦截器 * @param registry · */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtAuthenticationTokenInterceptor()) .excludePathPatterns("/api/user/login") .excludePathPatterns("swagger-ui.html","doc.html")// 开放登录路径 .addPathPatterns("/api/**"); // 拦截地址 } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String filePath = "file:" + ROOT_PATH + SON_PATH; registry.addResourceHandler("/**").addResourceLocations( "classpath:/static/"); registry.addResourceHandler("swagger-ui.html", "doc.html").addResourceLocations( "classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**","/template").addResourceLocations( "classpath:/META-INF/resources/webjars/"); registry.addResourceHandler("img//**").addResourceLocations(filePath); super.addResourceHandlers(registry); }}
本文结束,还有很多需要改进的地方,欢迎各位友友指正!