目录
概述
微信登录接口说明
关于获取微信用户的信息
前端代码(uni-app)
后端代码(SpringBoot)
配置文件:application.yml
配置文件:Pom.xml
类:WeChatModel
类:WeChatSessionModel
类:UserInfoController
业务层实现类:UserInfoServiceImpl
工具类:JWTUtils
拦截器配置-自定义拦截器
拦截器配置-注册自定义拦截器
测试(Postman)
总结
本篇博本主要为了记录使用uni-app开发微信小程序时实现微信一键登录功能,并且使用JWT实现身份认证。
可以点击==>官方的登录时序图<== ,看到官方描述登录的流程。
大概描述就是 :uni-app调用login()方法,获取临时凭证code,将此临时凭证发送到我们的后端,后端通过我们传入的临时凭证code,调用微信接口服务获取当前微信用户的唯一标识openid,我们就可以凭借此openid知道哪一个用户在进行登录操作。
值得注意的是:
1.通过login()获取的临时凭证code的有效期为5分钟,并且只能使用一次。
2.后端调用微信凭证验证接口获取openid需要appId和appSecret,两者都可以到微信小程序官网==>开发管理==>开发设置 中获取。
如下是我画的整体大概流程
总体说明 :整个流程就是当用户点击"微信一键登录",传入临时凭证code,后端通过临时凭证code去微信服务接口获取该用户的openid,此openid是唯一的不会变的。 那么我们就可以将openid存储用户数据表中,用来标识此用户。
关于API:uni.getUserInfo(OBJECT) 和 uni.getUserProfile(OBJECT) 接口的说明。
目前两个接口都无法获取用户信息,只能获取到默认用户信息,名称为'微信用户',头像为灰色用户头像。
关于官方描述:==>原文<==
那么解决方案只能是用户登录后,让用户自行上传修改信息。
前端的代码很简单,只是调用uni.login()获取临时凭证code传入后端接口即可。
后端需要接收前端传入的临时凭证code,向微信服务器发送请求获取登录用户的openid。并且操作数据库后返回用户信息,以及响应头返回token。
# JWT配置 jwt: header: "Authorization" #token返回头部 tokenPrefix: "Bearer " #token前缀 secret: "maohe101" #密钥 expireTime: 3600000 #token有效时间 3600000毫秒 ==> 60分钟 # 微信小程序配置码 APPID: 自己的appid APPSECRET: 自己的密匙
添加需要依赖
com.auth0 java-jwt3.19.2 com.google.code.gson gson2.8.9
接收前端传入参数
package com.mh.common; import lombok.Data; /** * Date:2023/5/24 * author:zmh * description: 接收小程序传入参数 **/ @Data public class WeChatModel { /** * 临时登录凭证 */ private String code; /** * 微信服务器上的唯一id */ private String openId; }
接收调用微信验证code后返回的数据。
package com.mh.common; import lombok.Data; /** * Date:2023/5/24 * author:zmh * description: 接收微信服务器返回参数 **/ @Data public class WeChatSessionModel { /** * 微信服务器上辨识用户的唯一id */ private String openid; /** * 身份凭证 */ private String session_key; /** * 错误代码 */ private String errcode; /** * 错误信息 */ private String errmsg; }
接收临时凭证code,调用业务层方法
@Autowired private UserInfoService userInfoService; /** * 微信登录 * @param weChatModel 获取临时凭证code * @param response · * @return 返回执行结果 */ @PostMapping("/login") public RloginCheck(@RequestBody WeChatModel weChatModel, HttpServletResponse response){ // 检查登录 Map resultMap = userInfoService.checkLogin(weChatModel.getCode()); // resultMap大于1为通过,业务层判断正确后返回用户信息和token,所以应该size为2才正确。 if (resultMap.size() > 1){ log.info("创建的token为=>{}", resultMap.get("token")); // 将token添加入响应头以及返回用户信息 response.setHeader(JWTUtils.header, (String) resultMap.get("token")); return R.success(resultMap.get("user").toString()); }else{ // 当返回map的size为1时,即为报错信息 return R.error(resultMap.get("errmsg").toString()); } }
登录验证的逻辑处理
package com.mh.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.gson.Gson; import com.mh.common.R; import com.mh.common.WeChatSessionModel; import com.mh.common.WeChatModel; import com.mh.dao.FansInfoDao; import com.mh.dao.FollowInfoDao; import com.mh.dao.UserInfoDao; import com.mh.pojo.FansInfo; import com.mh.pojo.FollowInfo; import com.mh.pojo.UserInfo; import com.mh.service.UserInfoService; import com.mh.utils.JWTUtils; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * Date:2023/5/24 * author:zmh * description: 用户信息业务层实现类 **/ @Service @Slf4j public class UserInfoServiceImpl extends ServiceImplimplements UserInfoService { @Autowired private UserInfoDao userInfoDao; @Value("${APPID}") private String appid; @Value("${APPSECRET}") private String appsecret; @Autowired private RestTemplate restTemplate; // 用于存储用户信息和token Map map = new HashMap<>(); /** * 登录验证 * @param code 临时登录码 * @return · */ public Map checkLogin(String code){ // 根据传入code,调用微信服务器,获取唯一openid // 微信服务器接口地址 String url = "https://api.weixin.qq.com/sns/jscode2session?appid="+appid+ "&secret="+appsecret +"&js_code="+ code +"&grant_type=authorization_code"; String errmsg = ""; String errcode = ""; String session_key = ""; String openid = ""; WeChatSessionModel weChatSessionModel; // 发送请求 ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.GET, null, String.class); // 判断请求是否成功 if(responseEntity != null && responseEntity.getStatusCode() == HttpStatus.OK) { // 获取主要内容 String sessionData = responseEntity.getBody(); Gson gson = new Gson(); //将json字符串转化为实体类; weChatSessionModel = gson.fromJson(sessionData, WeChatSessionModel.class); log.info("返回的数据==>{}",weChatSessionModel); //获取用户的唯一标识openid openid = weChatSessionModel.getOpenid(); //获取错误码 errcode = weChatSessionModel.getErrcode(); //获取错误信息 errmsg = weChatSessionModel.getErrmsg(); }else{ log.info("出现错误,错误信息:{}",errmsg ); map.put("errmsg",errmsg); return map; } // 判断是否成功获取到openid if ("".equals(openid) || openid == null){ log.info("错误获取openid,错误信息:{}",errmsg); map.put("errmsg",errmsg); return map; }else{ // 判断用户是否存在,查询数据库 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(UserInfo::getOpenid, openid); UserInfo userInfo = userInfoDao.selectOne(queryWrapper); // 不存在,加入数据表 if (userInfo == null){ // 填充初始信息 UserInfo tempUserInfo = new UserInfo(UUID.randomUUID().toString(), openid, "微信用户", 1,"default.png", "",0,0,0); // 加入数据表 userInfoDao.insert(tempUserInfo); // 加入map返回 map.put("user",tempUserInfo); // 调用自定义类封装的方法,创建token String token = JWTUtils.createToken(tempUserInfo.getId().toString()); map.put("token",token); return map; }else{ // 存在,将用户信息加入map返回 map.put("user",userInfo); String token = JWTUtils.createToken(userInfo.getId().toString()); map.put("token",token); return map; } } } }
用于创建,验证和更新token
package com.mh.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.TokenExpiredException; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; /** * Date:2023/5/24 * author:zmh * description:JWT工具类,JWT生成,验证 **/ @Component @Data @ConfigurationProperties(prefix = "jwt") @Slf4j public class JWTUtils { //定义token返回头部 public static String header; //token前缀 public static String tokenPrefix; //签名密钥 public static String secret; //有效期 public static long expireTime; public void setHeader(String header) { JWTUtils.header = header; } public void setTokenPrefix(String tokenPrefix) { JWTUtils.tokenPrefix = tokenPrefix; } public void setSecret(String secret) { JWTUtils.secret = secret; } public void setExpireTime(long expireTime) { JWTUtils.expireTime = expireTime; } /** * 创建TOKEN * * @param sub * @return */ public static String createToken(String sub) { return tokenPrefix + JWT.create() .withSubject(sub) .withExpiresAt(new Date(System.currentTimeMillis() + expireTime)) .sign(Algorithm.HMAC512(secret)); } /** * 验证token * * @param token */ public static String validateToken(String token) { try { return JWT.require(Algorithm.HMAC512(secret)) .build() .verify(token.replace(tokenPrefix, "")) .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(secret)) .build() .verify(token.replace(tokenPrefix, "")) .getExpiresAt(); } catch (TokenExpiredException e) { return true; } catch (Exception e) { log.info("token验证失败"); return false; } //如果剩余过期时间少于过期时常的一般时 需要更新 return (expiresAt.getTime() - System.currentTimeMillis()) < (expireTime >> 1); } }
当用户访问非登录接口时,需要拦截请求,判断用户的请求头是否携带了正确的token,携带了代表登录过了,请求通过,返回数据,若未token验证失败则错误提示。
package com.mh.utils; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Date:2023/5/26 * author:zmh * description: 自定义登录拦截器 **/ @Slf4j public class UserLoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取请求头,header值为Authorization,承载token String token = request.getHeader(JWTUtils.header); //token不存在 if (token == null || token.equals("")) { log.info("传入token为空"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token为空!"); return false; } //验证token String sub = JWTUtils.validateToken(token); if (sub == null || sub.equals("")){ log.info("token验证失败"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token验证失败!"); return false; } //更新token有效时间 (如果需要更新其实就是产生一个新的token) if (JWTUtils.isNeedUpdate(token)){ String newToken = JWTUtils.createToken(sub); response.setHeader(JWTUtils.header,newToken); } return true; } }
package com.mh.config; import com.mh.utils.UserLoginInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * Date:2023/5/26 * author:zmh * description: MVW配置 **/ @Configuration public class WebMvcConfig implements WebMvcConfigurer { /** * 注册自定义拦截器 * @param registry · */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserLoginInterceptor()) .addPathPatterns("/api/**") // 拦截地址 .excludePathPatterns("/api/userInfo/login");// 开放登录路径 } }
1.测试微信一键登录
微信小程序获取临时凭证code
通过返回的code到postman中测试调用后端登录接口
获取到返回,代表登录成功。
2.测试token的验证
调用非登录接口,会被拦截进行token的检查。
后端日志输出:
携带错误或过期的token,验证失败
后端日志输出
携带正确且在有效期内的token,验证成功,测试通过。
对于如上代码,其实微信登录的逻辑是比较简单的,代码更多的是在处理身份验证(token验证),后端设置了请求拦截器,会去拦截所有非登录接口,通过检查token判断是否登录过了。
对于前端发送请求,如上只是使用了Postman进行接口的访问,并没有从代码层面去发送请求,那么,其实前端是比较需要去封装请求方法的,在封装的请求方法中加入请求头携带token,避免每一次请求都需要手动加上请求头携带token。
如博文内容存在不足之处请指出,感谢访问。