微信登录流程

小程序登录 | 微信开放文档

步骤分析:

  1. 小程序端,调用wx.login()获取code,就是授权码。
  2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
  3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
  4. 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
  5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
  6. 小程序端,收到自定义登录态,存储storage。
  7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。
  8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
  9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

说明:

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

微信开发者工具如何打开开发者网络功能 微信开发者工具在哪_User

接口分析

微信开发者工具如何打开开发者网络功能 微信开发者工具在哪_微信开发者工具如何打开开发者网络功能_02

提交的参数需要小程序授权码提交到后台,后台需要根据授权码获取用户的唯一标记openid

下图是微信开发者工具抓取的的请求截图

微信开发者工具如何打开开发者网络功能 微信开发者工具在哪_微信_03

表设计

当用户第一次使用小程序时,会完成自动注册,把用户信息存储到user表中。

字段名

数据类型

说明

备注

id

bigint

主键

自增

openid

varchar(45)

微信用户的唯一标识

name

varchar(32)

用户姓名

phone

varchar(11)

手机号

sex

varchar(2)

性别

id_number

varchar(18)

身份证号

avatar

varchar(500)

微信用户头像路径

create_time

datetime

注册时间

**说明:**手机号字段比较特殊,个人身份注册的小程序没有权限获取到微信用户的手机号。如果是以企业的资质
注册的小程序就能够拿到微信用户的手机号。

代码实现

配置文件中声明小程序ID小程序密钥

sky:
  wechat:
    appid: 自己填写
    secret: 自己填写
  jwt: #jwt token
	#...省略admin相关配置
	#用户端 密钥
    user-secret-key: liyinghao
    #用户端 会话有效时间
    user-ttl: 7200000
    #用户端 请求头名称
    user-token-name: authentication

web层

业务分析:

1.service调用登录方法,成功登录则返回用户信息。

2.拿到用户信息后,生成一个token,一起封装到VO中返回给前端。

/**
     * 用户登录
     * @param userLoginDTO
     * @return
     */
@PostMapping("/login")
@ApiOperation("用户登录接口")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
    //登录成功拿到用户信息
    User user = userService.login(userLoginDTO);
    //登录成功后,生成jwt令牌
    Map<String, Object> claims = new HashMap<>();
    claims.put(JwtClaimsConstant.USER_ID, user.getId());
    String token = JwtUtil.createJWT(
        jwtProperties.getUserSecretKey(),
        jwtProperties.getUserTtl(),
        claims);
    //封装VO信息
    UserLoginVO userLoginVO = UserLoginVO.builder()
        .id(user.getId())
        .openid(user.getOpenid())
        .token(token)
        .build();
    return Result.success(userLoginVO);
}

service层

业务逻辑

  1. 根据用户授权码去微信获取用户唯一标识(因为用户唯一标识是不会变的)
  2. 判断openid是否为空,如果为空表示登录失败
  3. 根据openid去数据库查询,判断是否为新用户
  4. 如果是新用户,需要注册
  5. 返回用户对象
private static final String WX_GET_OPENID_URL = "https://api.weixin.qq.com/sns/jscode2session";

/**
     * 用户登录
     * @param userLoginDTO 里面包含微信授权码
     * @return 用户信息
     */
@Override
public User login(UserLoginDTO userLoginDTO) {
    //1.根据授权码拿到openid
    String openid = getOpenid(userLoginDTO.getCode());
    //2.如果没有openid直接抛业务异常
    if (openid == null){
        throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
    }
    //3.根据openid查询用户
    User user = userMapper.findByOpenid(openid);
    //4.如果用户不存在说明第一次登录,将用户添加到表中
    if (user == null){
        //新建用户
        user = User.builder()
            .openid(openid)
            .createTime(LocalDateTime.now())
            .build();
        //添加到用户表中,并且返回主键
        userMapper.add(user);
    }
    //5.返回用户信息
    return user;
}
/**
     * 根据微信授权码获取openid
     * @param code
     * @return
     */
private String getOpenid(String code) {
    try {
        //封装请求信息
        HashMap<String, String> paramMap = new HashMap<>();
        paramMap.put("appid",weChatProperties.getAppid());
        paramMap.put("secret",weChatProperties.getSecret());
        paramMap.put("js_code",code);
        paramMap.put("grant_type","authorization_code");
        //拿到的是json字符串
        String jsonStr = HttpClientUtil.doGet(WX_GET_OPENID_URL, paramMap);
        //将字符串转换成Map
        Map map = new ObjectMapper().readValue(jsonStr, Map.class);
        //拿到openid
        return (String) map.get("openid");
    } catch (JsonProcessingException ex) {
        //catch到异常直接return null
        System.err.println(ex.getMessage());
        return null;
    }
}

mapper层

java代码

/**
     * 根据openid查询用户
     * @param openid
     * @return
     */
User findByOpenid(String openid);

/**
     * 添加用户,并且返回主键
     * @param user
     */
void add(User user);

SQL语句

<!--    添加用户,并且返回主键-->
<insert id="add" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user (openid, name, phone, sex, id_number, avatar, create_time)
    VALUES (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})
</insert>
<!--    根据openid查询用户-->
<select id="findByOpenid" resultType="com.sky.entity.User">
    SELECT * FROM user WHERE openid = #{openid}
</select>

拦截器

拦截器类

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //判断当前拦截到的是Controller的方法还是其他资源
    if (!(handler instanceof HandlerMethod)) {
        //当前拦截到的不是动态方法,直接放行
        return true;
    }

    //1、从请求头中获取令牌
    String token = request.getHeader(jwtProperties.getUserTokenName());

    //2、校验令牌
    try {
        log.info("jwt校验:{}", token);
        Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
        Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
        log.info("当前用户id:", userId);
        //将empId通过ThreadLocal保存到当前线程中,做一个线程绑定
        BaseContext.setCurrentId(userId);
        //3、通过,放行
        return true;
    } catch (Exception ex) {
        //4、不通过,响应401状态码
        response.setStatus(401);
        return false;
    }

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    //将线程变量移除,防止内存泄露
    BaseContext.removeCurrentId();
}

MVC配置

/**
     * 注册自定义拦截器
     *
     * @param registry
     */
protected void addInterceptors(InterceptorRegistry registry) {
    log.info("开始注册自定义拦截器...");
    //管理端烂机器
    registry.addInterceptor(jwtTokenAdminInterceptor)
        .addPathPatterns("/admin/**")
        //登录请求做放行
        .excludePathPatterns("/admin/employee/login");
    //用户端拦截器
    registry.addInterceptor(jwtTokenUserInterceptor)
        .addPathPatterns("/user/**")
        //查看店铺状态做放行
        .excludePathPatterns("/user/shop/status")
        //登录请求做放行
        .excludePathPatterns("/user/user/login");
}