微信登录流程
步骤分析:
- 小程序端,调用wx.login()获取code,就是授权码。
- 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
- 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
- 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
- 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
- 小程序端,收到自定义登录态,存储storage。
- 小程序端,后绪通过wx.request()发起业务请求时,携带token。
- 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
- 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
说明:
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
接口分析
提交的参数需要小程序
将授权码
提交到后台,后台需要根据授权码获取用户的唯一标记openid
下图是微信开发者工具抓取的的请求截图
表设计
当用户第一次使用小程序时,会完成自动注册,把用户信息存储到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层
业务逻辑
- 根据用户授权码去微信获取用户唯一标识(因为用户唯一标识是不会变的)
- 判断openid是否为空,如果为空表示登录失败
- 根据openid去数据库查询,判断是否为新用户
- 如果是新用户,需要注册
- 返回用户对象
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");
}