【知识点总结】微信登录流程与Java Spring 实现
微信登录流程
微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
步骤分析:
- 微信小程序端, 调用 wx.login 请求微信接口服务, 获得登录临时代码code, 就是授权码;
- 微信小程序端, 调用 wx.request 发送请求并携带 code, 请求开发者服务器(自己编写的后端服务).
- 开发者服务端, 通过 HttpClient 向微信接口服务发送请求, 并携带 appid + appsecret + code 三个参数和默认grant_type.
- 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
- 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
- 小程序端,收到自定义登录态,存储storage。
- 小程序端,后绪通过wx.request()发起业务请求时,携带token。
- 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
- 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
问题思考: 微信登录是什么, 和正常的登录区别在哪里?
- 微信登录是为了获得 openid
- 前端也是可以通过网络请求获得 openid 的, 但是这样会暴露 appid , secret,session_key 等隐私信息
- 所以微信登录的逻辑是前端调用微信登录接口, 后端调用微信接口, 后端返回 openid 给前端
- 后端获得到 openid 后会自动注册用户, 并返回 jwt 令牌给前端, jwt 令牌里面包含用户 id, 可以通过 id 查询数据库获得用户信息
问题思考: openid 是干嘛用的?
微信登录得到的OpenID在微信小程序和公众平台环境中扮演着至关重要的角色。以下是OpenID的具体用途:
-
用户身份验证
- OpenID是微信用户在公众帐号(包括小程序)、H5、APP下的唯一标识。每个应用都有一个appid,不同的appid对应的openid不同。通过OpenID,开发者可以在小程序或公众平台中对用户进行身份验证,确保用户的安全性和唯一性。这有助于防止恶意用户创建多个账户进行不当行为。
-
跨平台登录
- OpenID允许用户使用已有的微信账户信息进行登录,无需在每个小程序或应用中再次创建新的账户。这为用户提供了极大的便利,减少了重复注册和登录的麻烦。
-
用户信息获取
- 小程序在用户登录后会返回一个包含OpenID的用户信息。开发者可以通过OpenID获取用户的基本信息,如用户头像、昵称等。这些信息对于个性化推荐、用户管理等操作非常有用。
-
数据统计与分析
- 通过OpenID,开发者可以对用户的行为进行统计和分析,例如用户的浏览记录、购买记录、搜索记录等。这些数据对于优化产品和服务、提高用户体验具有重要意义。
-
安全性增强
- OpenID采用加密传输技术,保障用户信息的传输安全。同时,OpenID作为用户的唯一标识,在需要进行安全验证的操作时(如对消息进行加密和解密、对数据进行签名等)也非常有用。
-
隐私保护
- OpenID采用非实名制认证方式,用户可以保护自己的隐私信息不被滥用。在获取OpenID的过程中,开发者需要遵循微信小程序平台的相关规定,确保用户隐私和利益的保护。同时,在传输和存储OpenID时,也需要进行加密和解密等安全处理,防止信息泄露。
综上所述,微信登录得到的OpenID在微信小程序和公众平台环境中具有多种重要作用。它是用户身份验证、跨平台登录、用户信息获取、数据统计与分析、安全性增强以及隐私保护等方面的重要工具。
代码开发
1 定义相关配置
配置微信登录所需配置项:
application-dev.yml
sky:wechat:appid: wxffb3637a228223b8secret: 84311df9199ecacdf4f12d27b6b9522d
application.yml
sky:wechat:appid: ${sky.wechat.appid}secret: ${sky.wechat.secret}
配置为微信用户生成jwt令牌时使用的配置项:
application.yml
sky:jwt:# 设置jwt签名加密时使用的秘钥admin-secret-key: itcast# 设置jwt过期时间admin-ttl: 7200000# 设置前端传递过来的令牌名称admin-token-name: tokenuser-secret-key: itheimauser-ttl: 7200000user-token-name: authentication
2 DTO设计
根据传入参数设计DTO类:
在sky-pojo模块,UserLoginDTO.java已定义
package com.sky.dto;import lombok.Data;import java.io.Serializable;/*** C端用户登录*/
@Data
public class UserLoginDTO implements Serializable {private String code;}
3 VO设计
根据返回数据设计VO类:
在sky-pojo模块,UserLoginVO.java已定义
package com.sky.vo;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO implements Serializable {private Long id;private String openid;private String token;}
4 Controller层
根据接口定义创建UserController的login方法:
package com.sky.controller.user;import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.UserService;
import com.sky.utils.JwtUtil;
import com.sky.vo.UserLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {@Autowiredprivate UserService userService;@Autowiredprivate JwtProperties jwtProperties;/*** 微信登录* @param userLoginDTO* @return*/@PostMapping("/login")@ApiOperation("微信登录")public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){log.info("微信用户登录:{}",userLoginDTO.getCode());//微信登录User user = userService.wxLogin(userLoginDTO);//后绪步骤实现//为微信用户生成jwt令牌Map<String, Object> claims = new HashMap<>();claims.put(JwtClaimsConstant.USER_ID,user.getId());String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);UserLoginVO userLoginVO = UserLoginVO.builder().id(user.getId()).openid(user.getOpenid()).token(token).build();return Result.success(userLoginVO);}
}
其中,JwtClaimsConstant.USER_ID常量已定义。
5 Service层接口
创建UserService接口:
package com.sky.service;import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;public interface UserService {/*** 微信登录* @param userLoginDTO* @return*/User wxLogin(UserLoginDTO userLoginDTO);
}
6 Service层实现类
**创建UserServiceImpl实现类:**实现获取微信用户的openid和微信登录功能
package com.sky.service.impl;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.constant.MessageConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.exception.LoginFailedException;
import com.sky.mapper.UserMapper;
import com.sky.properties.WeChatProperties;
import com.sky.service.UserService;
import com.sky.utils.HttpClientUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;@Service
@Slf4j
public class UserServiceImpl implements UserService {//微信服务接口地址public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";@Autowiredprivate WeChatProperties weChatProperties;@Autowiredprivate UserMapper userMapper;/*** 微信登录* @param userLoginDTO* @return*/public User wxLogin(UserLoginDTO userLoginDTO) {String openid = getOpenid(userLoginDTO.getCode());//判断openid是否为空,如果为空表示登录失败,抛出业务异常if(openid == null){throw new LoginFailedException(MessageConstant.LOGIN_FAILED);}//判断当前用户是否为新用户User user = userMapper.getByOpenid(openid);//如果是新用户,自动完成注册if(user == null){user = User.builder().openid(openid).createTime(LocalDateTime.now()).build();userMapper.insert(user);//后绪步骤实现}//返回这个用户对象return user;}/*** 调用微信接口服务,获取微信用户的openid* @param code* @return*/private String getOpenid(String code){//调用微信接口服务,获得当前微信用户的openidMap<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, map);JSONObject jsonObject = JSON.parseObject(json);String openid = jsonObject.getString("openid");return openid;}
}
7 Mapper层
创建UserMapper接口:
package com.sky.mapper;import com.sky.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;@Mapper
public interface UserMapper {/*** 根据openid查询用户* @param openid* @return*/@Select("select * from user where openid = #{openid}")User getByOpenid(String openid);/*** 插入数据* @param user*/void insert(User user);
}
创建UserMapper.xml映射文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.UserMapper"><insert id="insert" 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></mapper>
8 编写拦截器
**编写拦截器JwtTokenUserInterceptor:**统一拦截用户端发送的请求并进行jwt校验
package com.sky.interceptor;import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** jwt令牌校验的拦截器*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {@Autowiredprivate JwtProperties jwtProperties;/*** 校验jwt** @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);Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());log.info("当前用户的id:", userId);BaseContext.setCurrentId(userId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
}
在WebMvcConfiguration配置类中注册拦截器:
@Autowiredprivate JwtTokenUserInterceptor jwtTokenUserInterceptor;/*** 注册自定义拦截器* @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");//.........registry.addInterceptor(jwtTokenUserInterceptor).addPathPatterns("/user/**").excludePathPatterns("/user/user/login").excludePathPatterns("/user/shop/status");}