微信登录原理

429 阅读6分钟

image-20221204211800753.png 步骤分析:

  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. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

代码演示:

建一个Dto用于接受小程序传过来的code。

@Data
public class UserLoginDTO implements Serializable {

    private String code;

}

在开发者创建一个登录端的接口

@RestController
@RequestMapping("/user/user")
@Slf4j
@Api(tags = "C端用户相关接口")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    @ApiOperation("用户登录")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
        log.warn("{}", userLoginDTO);
        return userService.login(userLoginDTO);
    }

}

返回值UserLoginVO用于返回最后的openid和token

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO implements Serializable {

    private Long id;
    private String openid;
    private String token;

}

service层

public interface UserService {

    /**
     * 用户微信登录
     * @param userLoginDTO
     * @return
     */
    Result<UserLoginVO> login(UserLoginDTO userLoginDTO);
}

service层实现类

@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {

    public static final String WX_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session";

    @Autowired
    private WeChatProperties weChatProperties;
    @Autowired
    private JwtProperties jwtProperties;
    @Autowired
    private UserMapper userMapper;

    @Override
    public Result<UserLoginVO> login(UserLoginDTO userLoginDTO) {
        // 一.校验参数
        if(Objects.isNull(userLoginDTO) || Objects.isNull(userLoginDTO.getCode())) {
            throw new ArgsErrorException(MessageConstant.ARGS_ERROR);
        }
        // 二.处理业务
        // 1.请求微信登录服务器,获取openid
        String openid = getOpenid(userLoginDTO.getCode());

        // 2.判断是否有openid
        if(StringUtils.isBlank(openid)) {
            // 3.无,直接响应【非法用户】
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }
        // 4.有,根据openid查询用户表获取用户信息
        User user = userMapper.findByOpenid(openid);
        if(Objects.isNull(user)) {
            // 5.没有用户信息,增加一个用户
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            //主键回填
            userMapper.insert(user);
        }
        // 三.封装数据
        //登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID, user.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getUserSecretKey(),
                jwtProperties.getUserTtl(),
                claims);
        // 有用户信息,根据用户信息生成token,返回给前端
        UserLoginVO vo = new UserLoginVO();
        vo.setId(user.getId());
        vo.setOpenid(user.getOpenid());
        vo.setToken(token);
        return Result.success(vo);
    }

    /**
     * 获取openid
     * @param code
     * @return
     */
    private String getOpenid(String code) {
        Map<String, String> map = new HashMap<>();
        map.put("appid", weChatProperties.getAppid());
        map.put("secret", weChatProperties.getSecret());
        map.put("js_code", code);
        map.put("grant_type", "authorization_code");
        String json = HttpClientUtil.doGet(WX_LOGIN_URL, map);
        JSONObject jsonObject = JSON.parseObject(json);
        return jsonObject.getString("openid");
    }
}

我们主要来分析下获取openid的这个代码:

    /**
     * 获取openid
     * @param code
     * @return
     */
    private String getOpenid(String code) {
        Map<String, String> map = new HashMap<>();
        map.put("appid", weChatProperties.getAppid());
        map.put("secret", weChatProperties.getSecret());
        map.put("js_code", code);
        map.put("grant_type", "authorization_code");
        String json = HttpClientUtil.doGet(WX_LOGIN_URL, map);
        JSONObject jsonObject = JSON.parseObject(json);
        return jsonObject.getString("openid");
    }

如何通过接受前端传来的code获取openid?

image-20230828204026894.png

第一官方给个openid首先它是一个字符串,而获得openid的关键你需要下面四个参数。

image-20230828203156066.png

appid和secret你可以通过配置文件获得:

  wechat:
    appid: wxf663a962bea8a958
    secret: a3039f37ee7301e3749a812557515190
   
    mchid : 1561414331
    mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
    privateKeyFilePath: D:\apiclient_key.pem
    apiV3Key: CZBK51236435wxpay435434323FFDuv3
    weChatPayCertFilePath: D:\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
    notifyUrl: https://7150418d.r11.cpolar.top/notify/paySuccess
    refundNotifyUrl: https://www.weixin.qq.com/wxpay/pay.php

通过WeChatProperties来获取配置文件

@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的秘钥
    private String mchid; //商户号
    private String mchSerialNo; //商户API证书的证书序列号
    private String privateKeyFilePath; //商户私钥文件
    private String apiV3Key; //证书解密的密钥
    private String weChatPayCertFilePath; //平台证书
    private String notifyUrl; //支付成功的回调地址
    private String refundNotifyUrl; //退款成功的回调地址

}

通过HttpClientUtil的doGet方法发送Get方式请求,获得openid的json字符串。

String json = HttpClientUtil.doGet(WX_LOGIN_URL, map);

WX_LOGIN_URL是(api.weixin.qq.com/sns/jscode2…

map的键分别对应appid和secret还有js_code和grant_type还有对应的value值,

appid和secret可以读配置文件获得(这两个要写你自己的),js_code是前端传的code,grant_type的值是死的直接填入authorization_code。

Map<String, String> map = new HashMap<>();
        map.put("appid", weChatProperties.getAppid());
        map.put("secret", weChatProperties.getSecret());
        map.put("js_code", code);
        map.put("grant_type", "authorization_code");

获得openid的json字符串,然后使用JSON.parseObject()把json解析成JSONObject对象,然后返回jsonObject.getString("openid")(jsonObject本质就是一个map)。

JSONObject jsonObject = JSON.parseObject(json);
        return jsonObject.getString("openid");

接下来你可以判断有没有openid了。

 // 2.判断是否有openid
        if(StringUtils.isBlank(openid)) {
            // 3.无,直接响应【非法用户】
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }

有,根据openid查询用户表获取用户信息,没有用户信息就在表里新增一个用户信息。

 // 4.有,根据openid查询用户表获取用户信息
        User user = userMapper.findByOpenid(openid);
        if(Objects.isNull(user)) {
            // 5.没有用户信息,增加一个用户
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            //主键回填
            userMapper.insert(user);
        }

yml配置jwt

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token

    #用户端的jwt信息
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication

JwtProperties:秘钥和过期时间是通过配置文件拿到的

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;

    /**
     * 用户端微信用户生成jwt令牌相关配置
     */
    private String userSecretKey;//解析token秘钥
    private long userTtl;//有效期
    private String userTokenName;

}

生成token值

// 三.封装数据
        //登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID, user.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getUserSecretKey(),
                jwtProperties.getUserTtl(),
                claims);
        // 有用户信息,根据用户信息生成token,返回给前端

jwtProperties.getUserSecretKey()`:这是用户的密钥,用于签名生成的令牌。

jwtProperties.getUserTtl()`:这是令牌的过期时间,它是一个长整型表示的时间戳。

claims:这是之前创建的存储声明信息的 Map` 对象。

调用 JwtUtil.createJWT 方法后,将返回生成的 JWT 令牌,存储在 token 变量中。

最后把token和openid封装到UserLoginVO中去,然后返回。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO implements Serializable {

    private Long id;
    private String openid;
    private String token;

}

    UserLoginVO vo = new UserLoginVO();
    vo.setId(user.getId());
    vo.setOpenid(user.getOpenid());
    vo.setToken(token);
    return Result.success(vo);

如何修改拦截器

主要更改工具类这段代码

image-20230829165219542.png

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *  在Handler之前执行。就是Controller中标记了@XxxMapping注解的方式
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    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);
            //当前登录的员工ID
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前员工id:", userId);

            //把登录ID设置到ThreadLocal上
            BaseContext.setCurrentId(userId);

            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }

    /**
     * 渲染视图之后执行。执行释放资源的操作
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //从ThreadLocal上删除登录ID
        BaseContext.removeCurrentId();
    }

}