写在前面
本文介绍了springboot开发微信小程序后端服务中,用户登录功能的设计与实现,坚持看完相信对你有帮助。
登录流程
如图:
这是微信官方文档中微信小程序登录的流程时序图,我在图中红色序号标注的五步就是完整的微信小程序登录流程。
流程解析
- 小程序端通过
wx.login()
获取用户登录凭证code。- 小程序将code发送到服务端的登录接口。
- 服务端的登录接口使用该code、小程序的AppID和AppSecret向微信服务器发起请求,获取用户的openid。
- 服务端拿到openid后,可以根据业务需求生成用户令牌(Token),通常包括用户信息、权限等,并返回给小程序。
- 小程序在本地缓存该用户令牌。
- 后续小程序发起请求时,在请求头中携带该用户令牌(Token)。
- 服务端接收到请求时,验证用户令牌的有效性,确保用户是经过认证和授权的。
具体实现
登录接口设计思路
- 接收小程序端传递的code参数。
- 使用code、小程序的AppID和AppSecret向微信服务器发起请求,获取用户的openid。
- 在数据库用户表中查询是否存在该openid。
- 如果存在该openid,则使用查询到的用户信息生成令牌(Token)。
- 如果不存在该openid,则进行用户注册操作,将新用户信息插入数据库用户表,并生成令牌(Token)。
- 返回生成的令牌(Token)给小程序端。
相关代码
说明
下面代码基于jdk17,发请求用的时jdk自带的http工具类
Java端
实体类
package com.xad.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* @author cxy
* @create 2024-08-05 14:18:35
*/
@Data
@TableName("user")
public class User {
@Schema(description = "用户id")
@TableField(value = "id")
private Integer id;
@Schema(description = "用户名称")
@TableField(value = "user_name")
private String name;
@Schema(description = "用户负责内容")
@TableField(value = "user_range")
private String range;
@Schema(description = "用户部门")
@TableField(value = "user_department")
private String department;
@Schema(description = "用户类型")
@TableField(value = "user_type")
private Integer type;
@Schema(description = "用户openid")
@TableField(value = "user_open_id")
private String openId;
@Schema(description = "用户头像")
@TableField(value = "user_pic")
private String pic;
}
登录dto
package com.xad.dto;
import com.xad.entity.User;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
/**
* @author cxy
* @create 2024-08-05 14:32:47
*/
@Data
public class UserLoginDTO implements Serializable {
@NotBlank(message = "code不能为空")
private String code;
private User userInfo;//用户信息
}
登录返回vo
package com.xad.vo;
import com.xad.entity.User;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* @author cxy
* @create 2024-08-05 14:33:13
*/
@Data
@Builder
public class UserLoginVO implements Serializable {
private String token;//用户登录凭证
private User userinfo;
}
controller
package com.xad.controller;
import com.xad.dto.UserLoginDTO;
import com.xad.entity.User;
import com.xad.result.Result;
import com.xad.service.UserService;
import com.xad.vo.UserLoginVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* @author cxy
* @create 2024-08-05 14:33:59
*/
@Tag(name = "用户接口")
@RequestMapping("/user")
@RestController
@CrossOrigin(origins = "*")
@Slf4j
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/login")
@Operation(summary = "微信登录")
public Result login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信用户登录:{}", userLoginDTO.getCode());
//微信登录
UserLoginVO user = userService.login(userLoginDTO);//后绪步骤实现
return Result.success(user);
}
}
service
package com.xad.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xad.dto.UserLoginDTO;
import com.xad.entity.User;
import com.xad.vo.UserLoginVO;
public interface UserService extends IService<User> {
UserLoginVO login(UserLoginDTO userLoginDTO);
}
service.impl
获取openid方法及登录
使用的是jdk17自带的HttpClient,也可以使用okhttp,或者糊涂工具包里面的。
package com.xad.service.impl;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xad.dto.UserLoginDTO;
import com.xad.entity.User;
import com.xad.handler.CxyException;
import com.xad.mapper.UserMapper;
import com.xad.result.Result;
import com.xad.result.ResultCodeEnum;
import com.xad.service.UserService;
import com.xad.utils.JwtUtil;
import com.xad.vo.UserLoginVO;
import io.micrometer.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author cxy
* @create 2024-08-05 14:36:47
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public UserLoginVO login(UserLoginDTO userLoginDTO) {
//获取openid
Map<String, String> map = getOpenid(userLoginDTO.getCode());
assert map != null;
if (StringUtils.isBlank(map.get("openid"))) {
throw new CxyException(ResultCodeEnum.USER_OPENID_ERROR); // openid获取失败
}
//查询用户
User user = userMapper.findByUser(map.get("openid"));
Map<String, Object> claims = new HashMap<>();
if (Objects.isNull(user)) {
// 用户不存在则注册
user = new User();
//获取完善信息,用户呢称、头像url
user.setId(userLoginDTO.getUserInfo().getId());
user.setType(userLoginDTO.getUserInfo().getType());
user.setName(userLoginDTO.getUserInfo().getName());
user.setRange(userLoginDTO.getUserInfo().getRange());
user.setDepartment(userLoginDTO.getUserInfo().getDepartment());
user.setOpenId(map.get("openid"));
// 注册用户
if (!this.save(user)) {
throw new CxyException(ResultCodeEnum.FAIL); // 用户注册失败
}
}
return UserLoginVO.builder()
//.id(Long.valueOf(user.getId()))
//.openid(user.getOpenId())
.token(JwtUtil.genToken(claims))
//.session_key(map.get("session_key"))
.userinfo(user)
.build();
}
private Map<String, String> getOpenid(String code) {
HttpClient httpClient = HttpClient.newHttpClient();
// 构建请求参数字符串
String params = String.format("appid=%s&secret=%s&js_code=%s&grant_type=%s",
URLEncoder.encode("appid", StandardCharsets.UTF_8),
URLEncoder.encode("AppSecret(小程序密钥) ", StandardCharsets.UTF_8),
URLEncoder.encode(code, StandardCharsets.UTF_8),
URLEncoder.encode("authorization_code", StandardCharsets.UTF_8));
// 创建 GET 请求
String url = "https://api.weixin.qq.com/sns/jscode2session?" + params;
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
try {
// 发送 GET 请求
HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
Map<String, String> map = new HashMap<>();
// 获取响应结果
int statusCode = httpResponse.statusCode();
String responseBody = httpResponse.body();
JSONObject responseJson = new JSONObject(responseBody);
//JSONObject responseJson = JSONObject.parseObject(responseBody);
String openid = responseJson.getString("openid");
String session_key = responseJson.getString("session_key");
map.put("session_key", session_key);
map.put("openid", openid);
// 处理响应结果
System.out.println("状态代码: " + statusCode);
System.out.println("响应正文: " + responseBody);
return map;
} catch (Exception e) {
//log.error("发送请求时出错: {}", e.getMessage());
return null;
}
}
}
工具类
package com.xad.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
private static final String KEY = "xad_java";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)//添加载荷
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))//添加过期时间
.sign(Algorithm.HMAC256(KEY));//指定算法,配置密钥
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
配置 jwt
package com.xad.config;
import com.xad.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author cxy
* @create 2024-07-22 10:02:08
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录注册接口不拦截
registry.addInterceptor(loginInterceptor)
.excludePathPatterns("/user/login", "/user/register", "/static/**",
"/webjars/**",
"/doc.html",
"/v3/**" );
}
}