学习目标:
乘客端只要登录,没有注册,登录方式为微信小程序登录,乘客登录我们根据微信接口拿到微信OpenId(全局唯一),到客户表(客户即乘客)查询OpenId是否存在,如果不存在即为注册,添加一天乘客记录;存在则根据用户信息生成token返回。
官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。
说明:
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
官方文档:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html
wx.login(Object object)调用接口获取登录凭证(code)。通过凭证进而换取用户登录态信息,包括用户在当前小程序的唯一标识(openid)
详情见官方文档
官方文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。
详情见官方文档
文档:https://gitee.com/binary/weixin-java-tools
微信Java
开发工具包,支持包括微信支付、开放平台、公众号、企业微信/企业号、小程序等微信功能模块的后端开发。
微信小程序:weixin-java-miniapp
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
<version>4.5.5.B</version>
</dependency>
操作模块:service-customer
引入依赖
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
</dependency>
说明:在父工程已经对改依赖进行了版本管理,这里直接引入即可
第三方账号加到公共账号配置文件里面
wx:
miniapp:
#小程序授权登录
appId: wxcc651fcbab275e33 # 小程序微信公众平台appId
secret: 5f353399a2eae7ff6ceda383e924c5f6 # 小程序微信公众平台api秘钥
package com.atguigu.daijia.customer.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxMaProperties {
private String appId;
private String secret;
}
配置WxMaService,通过WxMaService可以快速获取微信OpenId
package com.atguigu.daijia.customer.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Component
public class WxMaConfig {
@Autowired
private WxMaProperties wxMaProperties;
@Bean
public WxMaService wxMaService() {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
config.setAppid(wxMaProperties.getAppId());
config.setSecret(wxMaProperties.getSecret());
WxMaService service = new WxMaServiceImpl();
service.setWxMaConfig(config);
return service;
}
}
@Operation(summary = "小程序授权登录")
@GetMapping("/login/{code}")
public Result<Long> login(@PathVariable String code) {
return Result.ok(customerInfoService.login(code));
}
Long login(String code);
@Autowired
private WxMaService wxMaService;
@Autowired
private CustomerLoginLogMapper customerLoginLogMapper;
/**
* 条件:
* 1、前端开发者appid与服务器端appid一致
* 2、前端开发者必须加入开发者
* @param code
* @return
*/
@Transactional(rollbackFor = {Exception.class})
@Override
public Long login(String code) {
String openId = null;
try {
//获取openId
WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code);
openId = sessionInfo.getOpenid();
log.info("【小程序授权】openId={}", openId);
} catch (Exception e) {
e.printStackTrace();
throw new GuiguException(ResultCodeEnum.WX_CODE_ERROR);
}
CustomerInfo customerInfo = this.getOne(new LambdaQueryWrapper<CustomerInfo>().eq(CustomerInfo::getWxOpenId, openId));
if(null == customerInfo) {
customerInfo = new CustomerInfo();
customerInfo.setNickname(String.valueOf(System.currentTimeMillis()));
customerInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
customerInfo.setWxOpenId(openId);
this.save(customerInfo);
}
//登录日志
CustomerLoginLog customerLoginLog = new CustomerLoginLog();
customerLoginLog.setCustomerId(customerInfo.getId());
customerLoginLog.setMsg("小程序登录");
customerLoginLogMapper.insert(customerLoginLog);
return customerInfo.getId();
}
/**
* 小程序授权登录
* @param code
* @return
*/
@GetMapping("/customer/info/login/{code}")
Result<Long> login(@PathVariable String code);
@Autowired
private CustomerService customerInfoService;
@Operation(summary = "小程序授权登录")
@GetMapping("/login/{code}")
public Result<String> wxLogin(@PathVariable String code) {
return Result.ok(customerInfoService.login(code));
}
String login(String code);
@Autowired
private CustomerInfoFeignClient customerInfoFeignClient;
@Autowired
private RedisTemplate redisTemplate;
@Override
public String login(String code) {
//获取openId
Result<Long> result = customerInfoFeignClient.login(code);
if(result.getCode().intValue() != 200) {
throw new GuiguException(result.getCode(), result.getMessage());
}
Long customerId = result.getData();
if(null == customerId) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
String token = UUID.randomUUID().toString().replaceAll("-", "");
redisTemplate.opsForValue().set(RedisConstant.USER_LOGIN_KEY_PREFIX+token, customerId.toString(), RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
return token;
}
启动:server-gateway、web-customer、service-customer
测试效果:
测试结果:微信授权登录接口已经能够正常返回登录toekn,但是页面没有调整,是这样的,登录成功后,小程序又请求了一个获取当前登录用户信息接口,只要这个接口请求成功了它才会跳转页面。
乘客登录成功,小程序会回调“获取客户登录信息”接口,获取当前乘客基本信息
@Operation(summary = "获取客户登录信息")
@GetMapping("/getCustomerLoginInfo/{customerId}")
public Result<CustomerLoginVo> getCustomerLoginInfo(@PathVariable Long customerId) {
return Result.ok(customerInfoService.getCustomerLoginInfo(customerId));
}
CustomerLoginVo getCustomerLoginInfo(Long customerId);
@Override
public CustomerLoginVo getCustomerLoginInfo(Long customerId) {
CustomerInfo customerInfo = this.getById(customerId);
CustomerLoginVo customerInfoVo = new CustomerLoginVo();
BeanUtils.copyProperties(customerInfo, customerInfoVo);
//判断是否绑定手机号码,如果未绑定,小程序端发起绑定事件
Boolean isBindPhone = StringUtils.hasText(customerInfo.getPhone());
customerInfoVo.setIsBindPhone(isBindPhone);
return customerInfoVo;
}
/**
* 获取客户登录信息
* @param customerId
* @return
*/
@GetMapping("/customer/info/getCustomerLoginInfo/{customerId}")
Result<CustomerLoginVo> getCustomerLoginInfo(@PathVariable("customerId") Long customerId);
@Autowired
private RedisTemplate redisTemplate;
@Operation(summary = "获取客户登录信息")
@GetMapping("/getCustomerLoginInfo")
public Result<CustomerLoginVo> getCustomerLoginInfo(@RequestHeader(value="token") String token) {
String customerId = (String)redisTemplate.opsForValue().get(RedisConstant.USER_LOGIN_KEY_PREFIX+token);
return Result.ok(customerInfoService.getCustomerLoginInfo(Long.parseLong(customerId)));
}
CustomerLoginVo getCustomerLoginInfo(Long customerId);
@Override
public CustomerLoginVo getCustomerLoginInfo(Long customerId) {
Result<CustomerLoginVo> result = customerInfoFeignClient.getCustomerLoginInfo(customerId);
if(result.getCode().intValue() != 200) {
throw new GuiguException(result.getCode(), result.getMessage());
}
CustomerLoginVo customerLoginVo = result.getData();
if(null == customerLoginVo) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
return customerLoginVo;
}
用户登录成功,后续小程序的接口都会带上token,要想获取登录用户的信息,都得在controller方法里面解析,很是繁琐,有没有更好的解决方案呢?
当然是有的,我们可以自定义一个登录标签,需要登录的controller方法加上自定义标签,写一个全局AOP拦截器处理登录业务即可,下面来看怎么实现吧。
在service-util定义登录自定义标签
package com.atguigu.daijia.common.login;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GuiguLogin {
}
自定义AOP切面类,拦截被@GuiguLogin标识的controller方法
package com.atguigu.daijia.common.login;
@Slf4j
@Component
@Aspect
@Order(100)
public class GuiguLoginAspect {
@Autowired
private RedisTemplate redisTemplate;
/**
*
* @param joinPoint
* @param guiguLogin
* @return
* @throws Throwable
*/
@Around("execution(* com.atguigu.daijia.*.controller.*.*(..)) && @annotation(guiguLogin)")
public Object process(ProceedingJoinPoint joinPoint, GuiguLogin guiguLogin) throws Throwable {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
String token = request.getHeader("token");
if(!StringUtils.hasText(token)) {
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}
String userId = (String)redisTemplate.opsForValue().get(RedisConstant.USER_LOGIN_KEY_PREFIX+token);
if(StringUtils.hasText(userId)) {
AuthContextHolder.setUserId(Long.parseLong(userId));
}
return joinPoint.proceed();
}
}
AuthContextHolder封装了ThreadLocal,用于保存当前线程的用户id,这样后面controller方法就能够获取当前用户id了。
乘客端web接口改造
@Operation(summary = "获取客户登录信息")
@GuiguLogin
@GetMapping("/getCustomerLoginInfo")
public Result<CustomerLoginVo> getCustomerLoginInfo() {
Long customerId = AuthContextHolder.getUserId();
return Result.ok(customerInfoService.getCustomerLoginInfo(customerId));
}
简洁、清晰、明了
乘客下单代驾,司机会电话联系乘客,因此乘客必须绑定手机号码,微信小程序可以申请获取用户微信平台手机号码。
说明:获取手机号必须是企业级微信公众号,个人版获取不到。
只要没有绑定手机号码,小程序登录后就会弹出提示,如果下图
官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
该能力旨在帮助开发者向用户发起手机号申请,并且必须经过用户同意后,开发者才可获得由平台验证后的手机号,进而为用户提供相应服务。
收费说明
自2023年8月28日起,手机号快速验证组件将需要付费使用。标准单价为:每次组件调用成功,收费0.03元
体验额度:每个小程序账号将有1000次体验额度,用于开发、调试和体验。
详情参考官方文档
该接口需配合手机号快速验证或手机号实时验证能力一起使用,当用户同意后,可以通过 bindgetphonenumber
或 bindrealtimegetphonenumber
事件回调获取到动态令牌code
,再调用该接口将code
换取用户手机号。
注意:每个code只能使用一次,code的有效期为5min。
详情参考官方文档
该接口同样使用:微信Java开发工具包
@Operation(summary = "更新客户微信手机号码")
@PostMapping("/updateWxPhoneNumber")
public Result<Boolean> updateWxPhoneNumber(@RequestBody UpdateWxPhoneForm updateWxPhoneForm) {
return Result.ok(customerInfoService.updateWxPhoneNumber(updateWxPhoneForm));
}
Boolean updateWxPhoneNumber(UpdateWxPhoneForm updateWxPhoneForm);
@SneakyThrows
@Transactional(rollbackFor = {Exception.class})
@Override
public Boolean updateWxPhoneNumber(UpdateWxPhoneForm updateWxPhoneForm) {
// 调用微信 API 获取用户的手机号
WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(updateWxPhoneForm.getCode());
String phoneNumber = phoneInfo.getPhoneNumber();
log.info("phoneInfo:{}", JSON.toJSONString(phoneInfo));
CustomerInfo customerInfo = new CustomerInfo();
customerInfo.setId(updateWxPhoneForm.getCustomerId());
customerInfo.setPhone(phoneNumber);
return this.updateById(customerInfo);
}
/**
* 更新客户微信手机号码
* @param updateWxPhoneForm
* @return
*/
@PostMapping("/customer/info/updateWxPhoneNumber")
Result<Boolean> updateWxPhoneNumber(@RequestBody UpdateWxPhoneForm updateWxPhoneForm);
@Operation(summary = "更新用户微信手机号")
@GuiguLogin
@PostMapping("/updateWxPhone")
public Result updateWxPhone(@RequestBody UpdateWxPhoneForm updateWxPhoneForm) {
updateWxPhoneForm.setCustomerId(AuthContextHolder.getUserId());
return Result.ok(customerInfoService.updateWxPhoneNumber(updateWxPhoneForm));
}
微信公众号为个人版的需调整为如下:
@Operation(summary = "更新用户微信手机号")
@GuiguLogin
@PostMapping("/updateWxPhone")
public Result updateWxPhone(@RequestBody UpdateWxPhoneForm updateWxPhoneForm) {
updateWxPhoneForm.setCustomerId(AuthContextHolder.getUserId());
//customerInfoService.updateWxPhoneNumber(updateWxPhoneForm);
return Result.ok(true);
}
说明:个人版的获取不到手机号,直接跳过
Boolean updateWxPhoneNumber(@RequestBody UpdateWxPhoneForm updateWxPhoneForm);
@Override
public Boolean updateWxPhoneNumber(UpdateWxPhoneForm updateWxPhoneForm) {
customerInfoFeignClient.updateWxPhoneNumber(updateWxPhoneForm);
return true;
}
在Feign调用的过程中,由于全局异常的处理,所有的Feign调用都会返回Result
答案是肯定的,我们可以通过全局自定义Feign结果解析来处理就可以了。
说明:任何Feign调用Result
OpenFeign 自定义结果解码器
package com.atguigu.daijia.common.feign;
import com.atguigu.daijia.common.result.Result;
import com.atguigu.daijia.common.result.ResultCodeEnum;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import java.io.IOException;
import java.lang.reflect.Type;
/**
* OpenFeign 自定义结果解码器
*/
public class FeignCustomDataDecoder implements Decoder {
private final SpringDecoder decoder;
public FeignCustomDataDecoder(SpringDecoder decoder) {
this.decoder = decoder;
}
@Override
public Object decode(Response response, Type type) throws IOException {
Object object = this.decoder.decode(response, type);
if (null == object) {
throw new DecodeException(ResultCodeEnum.FEIGN_FAIL.getCode(), ResultCodeEnum.FEIGN_FAIL.getMessage(), response.request());//"数据解析失败"
}
if(object instanceof Result<?>) {
Result<?> result = ( Result<?>)object;
//返回状态!=200,直接抛出异常,全局异常捕获异常,接口提示
if (result.getCode().intValue() != ResultCodeEnum.SUCCESS.getCode().intValue()) {
throw new DecodeException(result.getCode(), result.getMessage(), response.request());//"数据解析失败"
}
//远程调用必须有返回值,具体调用中不用判断result.getData() == null,这里统一处理
if (null == result.getData()) {
throw new DecodeException(ResultCodeEnum.FEIGN_FAIL.getCode(), ResultCodeEnum.FEIGN_FAIL.getMessage(), response.request());//"数据解析失败"
}
return result;
}
return object;
}
}
Feign配置类
package com.atguigu.daijia.common.feign;
import feign.codec.Decoder;
import feign.optionals.OptionalDecoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.HttpMessageConverterCustomizer;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Feign配置类
*
*/
@Configuration
public class FeignConfig {
/**
* 自定义解析器
*
* @param msgConverters 信息转换
* @param customizers 自定义参数
* @return 解析器
*/
@Bean
public Decoder decoder(ObjectFactory<HttpMessageConverters> msgConverters, ObjectProvider<HttpMessageConverterCustomizer> customizers) {
return new OptionalDecoder((new ResponseEntityDecoder(new FeignCustomDataDecoder(new SpringDecoder(msgConverters, customizers)))));
}
}
改造乘客端web接口方法
@Override
public String login(String code) {
Long customerId = customerInfoFeignClient.login(code).getData();
String token = UUID.randomUUID().toString().replaceAll("-", "");
redisTemplate.opsForValue().set(RedisConstant.USER_LOGIN_KEY_PREFIX+token, customerId.toString(), RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
return token;
}
@Override
public CustomerLoginVo getCustomerLoginInfo(Long customerId) {
return customerInfoFeignClient.getCustomerLoginInfo(customerId).getData();
}
swg测试小程序登录接口(code随便输入,让他报错),在FeignCustomDataDecoder类设置断点,查看是否正确解析,改造前与改造后返回结果是否一致;如果一致说明全局统一处理成功。
推荐阅读: