1. 官方文档
微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
流程图:
步骤分析:
-
小程序端,调用wx.login()获取code,就是授权码。
-
小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
-
开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
-
开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
-
开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
-
小程序端,收到自定义登录态,存储storage。
-
小程序端,后绪通过wx.request()发起业务请求时,携带token。
-
开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
-
开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
2. 需求分析和设计
2.1 需求分析
用户进入到小程序的时候,首先需要微信授权登录。获取当前微信用户的相关信息,比如昵称、头像等,这样才能够进入到小程序进行其他操作。是基于微信登录来实现小程序的登录功能,没有采用传统账户密码登录的方式。若第一次使用小程序,就是一个新用户,需要把这个新的用户保存到数据库当中完成自动注册。
业务规则:
-
基于微信登录实现小程序的登录功能
-
如果是新用户需要自动完成注册
2.2 表设计
当用户第一次使用小程序时,会完成自动注册,把用户信息存储到user表中。
说明:
手机号字段比较特殊,个人身份注册的小程序没有权限获取到微信用户的手机号。如果是以企业的资质注册的小程序就能够拿到微信用户的手机号。
2.3 接口设计
通过微信登录的流程,如果要完成微信登录的话,最终就要获得微信用户的openid。在小程序端获取授权码后,向后端服务发送请求,并携带授权码,这样后端服务在收到授权码后,就可以去请求微信接口服务。最终,后端向小程序返回openid和token等数据。
基于上述的登录流程,就可以设计出该接口的请求参数和返回数据。
请求参数:微信用户授权码
返回数据:openid、token
3. 代码开发
重点介绍一下,Service层实现类实现获取微信用户的openid和微信登录功能; 以及拦截器中统一拦截用户端发送的请求并进行jwt校验。
3.1 Service层实现类
实现获取微信用户的openid和微信登录功能。
@Service // 标记该类为Spring服务层的Bean
@Slf4j // 引入Lombok的日志记录注解,可以直接使用log对象进行日志记录
public class UserServiceImpl implements UserService {
// 微信服务接口地址,用于获取微信用户的openid
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
@Autowired // 自动注入WeChatProperties配置类,用于获取微信应用的appid和secret等配置信息
private WeChatProperties weChatProperties;
@Autowired // 自动注入UserMapper,用于执行数据库操作
private UserMapper userMapper;
/**
* 微信登录方法
* @param userLoginDTO 登录请求参数对象,其中包含了微信授权码code
* @return User 返回用户对象
*/
public User wxLogin(UserLoginDTO userLoginDTO) {
// 调用getOpenid方法,通过微信接口获取用户的openid
String openid = getOpenid(userLoginDTO.getCode());
// 判断openid是否为空,如果为空则登录失败,抛出业务异常
if (openid == null) {
throw new LoginFailedException(MessageConstant.LOGIN_FAILED); // 抛出登录失败异常
}
// 通过openid查询用户信息,判断当前用户是否为新用户
User user = userMapper.getByOpenid(openid);
// 如果是新用户,自动完成注册
if (user == null) {
user = User.builder() // 使用User的构建器模式创建新用户对象
.openid(openid) // 设置openid
.createTime(LocalDateTime.now()) // 设置注册时间
.build();
userMapper.insert(user); // 将新用户信息插入数据库
}
// 返回用户对象(已存在用户或新注册用户)
return user;
}
/**
* 调用微信接口服务,获取微信用户的openid
* @param code 用户登录凭证code
* @return String 返回微信用户的openid
*/
private String getOpenid(String code) {
// 调用微信接口服务,构建请求参数
Map<String, String> map = new HashMap<>();
map.put("appid", weChatProperties.getAppid()); // 设置微信应用的appid
map.put("secret", weChatProperties.getSecret()); // 设置微信应用的secret
map.put("js_code", code); // 设置登录凭证code
map.put("grant_type", "authorization_code"); // 设置授权类型为授权码模式
// 通过HttpClientUtil的doGet方法发送GET请求,获取微信返回的JSON字符串
String json = HttpClientUtil.doGet(WX_LOGIN, map);
// 解析JSON字符串,将其转换为JSONObject对象
JSONObject jsonObject = JSON.parseObject(json);
// 从JSON对象中获取openid,若不存在则返回null
String openid = jsonObject.getString("openid");
return openid;
}
}
3.2 编写拦截器
编写拦截器JwtTokenUserInterceptor:统一拦截用户端发送的请求并进行jwt校验
3.2.1 自定义拦截器
/**
* jwt令牌校验的拦截器
*/
@Component // 标注该类为Spring的组件,将其作为拦截器Bean注入到Spring上下文中
@Slf4j // 引入Lombok的日志记录注解,可以直接使用log对象进行日志记录
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired // 自动注入JwtProperties配置类,用于获取JWT的配置属性(例如密钥、token名称等)
private JwtProperties jwtProperties;
/**
* 校验JWT令牌
*
* @param request HTTP请求对象,包含请求头、参数等信息
* @param response HTTP响应对象,用于设置响应状态和返回信息
* @param handler 处理器(通常是Controller的方法),表示当前请求将被哪个方法处理
* @return boolean 返回true表示放行,返回false表示拦截
* @throws Exception 抛出可能的异常
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断当前拦截到的是Controller的方法还是其他静态资源
if (!(handler instanceof HandlerMethod)) {
// 如果拦截到的不是Controller方法(可能是静态资源文件),直接放行
return true;
}
// 1. 从请求头中获取JWT令牌,名称由jwtProperties配置中定义
String token = request.getHeader(jwtProperties.getUserTokenName());
// 2. 校验令牌
try {
// 记录日志信息,显示即将校验的JWT令牌
log.info("jwt校验:{}", token);
// 使用JwtUtil工具类解析JWT令牌,验证其合法性并提取载荷信息
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
// 从解析后的Claims对象中获取用户ID
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
// 记录日志信息,显示当前用户的ID
log.info("当前用户的id:{}", userId);
// 将当前用户ID存储到上下文中,供后续业务逻辑使用
BaseContext.setCurrentId(userId);
// 3. 令牌校验通过,放行请求
return true;
} catch (Exception ex) {
// 4. 如果校验失败,捕获异常并设置响应状态码为401(未授权)
response.setStatus(401);
return false;
}
}
}
3.2.2 注册拦截器
需要在WebMvcConfiguration配置类中注册拦截器
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor; // 自动注入自定义的JWT令牌校验拦截器,用于对特定请求路径进行JWT验证
/**
* 注册自定义拦截器
*
* @param registry 拦截器注册对象,用于配置拦截器的路径规则
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器..."); // 记录日志,表示开始注册自定义拦截器
// 使用registry对象将自定义拦截器注册到拦截链中
registry.addInterceptor(jwtTokenUserInterceptor) // 添加自定义的JWT令牌校验拦截器
.addPathPatterns("/user/**") // 指定拦截的路径模式,所有以/user/开头的请求路径都会被拦截器拦截
.excludePathPatterns("/user/user/login") // 排除某些特定路径,不拦截登录接口(通常不需要JWT验证)
.excludePathPatterns("/user/shop/status"); // 排除路径,不拦截/shop/status接口
// 上述路径配置表示,JWT令牌校验拦截器将会对所有以"/user/"开头的路径进行拦截,
// 但会忽略"/user/user/login"和"/user/shop/status"这两个路径,不对它们执行JWT验证。
}