微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权)
作者:mmseoamin日期:2023-12-14

目录

概述

微信登录接口说明

 关于获取微信用户的信息

前端代码(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,两者都可以到微信小程序官网==>开发管理==>开发设置 中获取。

如下是我画的整体大概流程

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第1张

总体说明 :整个流程就是当用户点击"微信一键登录",传入临时凭证code,后端通过临时凭证code去微信服务接口获取该用户的openid,此openid是唯一的不会变的。 那么我们就可以将openid存储用户数据表中,用来标识此用户。

 关于获取微信用户的信息

关于API:uni.getUserInfo(OBJECT) 和 uni.getUserProfile(OBJECT) 接口的说明。

目前两个接口都无法获取用户信息,只能获取到默认用户信息,名称为'微信用户',头像为灰色用户头像。

关于官方描述:==>原文<==

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第2张

 那么解决方案只能是用户登录后,让用户自行上传修改信息。

前端代码(uni-app)

 前端的代码很简单,只是调用uni.login()获取临时凭证code传入后端接口即可。


后端代码(SpringBoot)

后端需要接收前端传入的临时凭证code,向微信服务器发送请求获取登录用户的openid。并且操作数据库后返回用户信息,以及响应头返回token。


配置文件:application.yml 

# JWT配置
jwt:
  header: "Authorization" #token返回头部
  tokenPrefix: "Bearer " #token前缀
  secret: "maohe101" #密钥
  expireTime: 3600000 #token有效时间 3600000毫秒 ==> 60分钟
# 微信小程序配置码
APPID: 自己的appid
APPSECRET: 自己的密匙

配置文件:Pom.xml 

添加需要依赖 



    com.auth0
    java-jwt
    3.19.2



    com.google.code.gson
    gson
    2.8.9

类:WeChatModel  

接收前端传入参数

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;
}

 类:WeChatSessionModel

接收调用微信验证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;
}

 类:UserInfoController

接收临时凭证code,调用业务层方法

@Autowired
private UserInfoService userInfoService;
/**
 * 微信登录
 * @param weChatModel 获取临时凭证code
 * @param response ·
 * @return 返回执行结果
 */
@PostMapping("/login")
public R loginCheck(@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());
    }
}

业务层实现类:UserInfoServiceImpl

登录验证的逻辑处理 

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 ServiceImpl implements 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;
            }
        }
    }
}

工具类:JWTUtils

 用于创建,验证和更新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");// 开放登录路径
    }
}

测试(Postman)

1.测试微信一键登录 

微信小程序获取临时凭证code

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第3张

 通过返回的code到postman中测试调用后端登录接口

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第4张

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第5张

 获取到返回,代表登录成功。

2.测试token的验证 

调用非登录接口,会被拦截进行token的检查。

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第6张

后端日志输出: 

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第7张 携带错误或过期的token,验证失败

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第8张

 后端日志输出 微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第9张

 携带正确且在有效期内的token,验证成功,测试通过。

微信小程序一键登录功能,使用uni-app和springboot(JWT鉴权),第10张

总结

 对于如上代码,其实微信登录的逻辑是比较简单的,代码更多的是在处理身份验证(token验证),后端设置了请求拦截器,会去拦截所有非登录接口,通过检查token判断是否登录过了。

对于前端发送请求,如上只是使用了Postman进行接口的访问,并没有从代码层面去发送请求,那么,其实前端是比较需要去封装请求方法的,在封装的请求方法中加入请求头携带token,避免每一次请求都需要手动加上请求头携带token。

如博文内容存在不足之处请指出,感谢访问。