登录
微信的登录逻辑
说明:
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份
对于业务而言,最首先返回的就是这个code,后端要做的就是把这个code结合自己小程序的appid和appsecret去请求微信后台,给出当前用户的openid。
接口1:GET”/customer/login/{code}”
整体的流程是:
- 对外service拿着这个前端发来的code去远程调用乘客微服务
- 乘客微服务请求微信的后台得到openid,然后去数据库查是否存在这个openid,也就是是否注册过了
- 如果没有注册就进行注册,最后写入操作日志,返回这个openid对应的数据库id,我们的业务是基于自家数据库的。
- 对外service拿到了这个id,将其缓存进redis中,key是UUID,value是在自家数据库中的id,也就是说在redis中有这个键值对那就说明是登录状态。
- 最后redis中的这个UUID作为token给前端,前端每次发请求都携带这个token。优化: 可以使用jwt?
对外接口:
| 12
 3
 4
 5
 
 | @Operation(summary = "小程序授权登录")@GetMapping("/login/{code}")
 public Result<String> wxLogin(@PathVariable String code) {
 return Result.ok(customerInfoService.login(code));
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | @Autowiredprivate CustomerInfoFeignClient customerInfoFeignClient;
 
 @Autowired
 private RedisTemplate redisTemplate;
 
 @Override
 public String login(String code) {
 
 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;
 }
 
 | 
乘客微服务接口:
| 12
 3
 4
 5
 
 | @Operation(summary = "小程序授权登录")@GetMapping("/login/{code}")
 public Result<Long> login(@PathVariable String code) {
 return Result.ok(customerInfoService.login(code));
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 
 | @Autowiredprivate WxMaService wxMaService;
 
 @Autowired
 private CustomerLoginLogMapper customerLoginLogMapper;
 
 
 
 
 
 
 
 
 @Transactional(rollbackFor = {Exception.class})
 @Override
 public Long login(String code) {
 String openId = null;
 try {
 
 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();
 }
 
 | 
接口2:GET”/customer/getCustomerLoginInfo”
当前一个方法执行成功后,返回了一个token,每次请求都会携带这个token。有了这个token后前端会发这个请求获得该用户的详细信息。那既然把UUID和value都放在redis里了,那每一次都从redis取就好了。
AOP+注解+ThreadLocal
这个注解的主要思路是,在每个controller方法调用之前,先去解析request请求的token,token里面是登录时所给的UUID,然后再去访问redis得到当前登录用户的本地数据库id,把它放在一个ThreadLocal保存,这样该方法执行的过程中只用在ThreadLocal中取这个就可以用了。
主要是用于取代网关的prehandle,其实本质是一样的,网关是直接拦截request得到token,进行处理后再放到ThreadLocal中
| 12
 3
 4
 5
 6
 
 | @Documented@Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.METHOD)
 public @interface GuiguLogin {
 
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 
 | @Slf4j@Component
 @Aspect
 @Order(100)
 public class GuiguLoginAspect {
 
 @Autowired
 private RedisTemplate redisTemplate;
 
 
 
 
 
 
 
 
 @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();
 }
 
 }
 
 | 
有了这个注解再进行个人资料的获取,现在直接就在接口上用则个注解,然后再在方法里面取出id就可以了,大致流程是:
- 用注解得到的id请求客户微服务
- 客户微服务用id查数据库
对外接口:
| 12
 3
 4
 5
 6
 7
 
 | @Operation(summary = "获取客户登录信息")@GuiguLogin
 @GetMapping("/getCustomerLoginInfo")
 public Result<CustomerLoginVo> getCustomerLoginInfo() {
 Long customerId = AuthContextHolder.getUserId();
 return Result.ok(customerInfoService.getCustomerLoginInfo(customerId));
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | @Overridepublic 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;
 }
 
 | 
客户微服务:
这里只写实现类,接口只是传导没有实际逻辑。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | @Overridepublic 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;
 }
 
 | 
自定义feign全局处理
在Feign调用的过程中,由于全局异常的处理,所有的Feign调用都会返回Result对象,我们还必须判断它的code是否等于200,如果不等于200,那么说明调用结果抛出异常了,我们必须返回异常信息提示给接口,如果返回code等于200,我们又必须判断data是否等于null,处理方式都一致,处理起来很繁琐,有没有好的统一处理方式呢?
答案是肯定的,我们可以通过全局自定义Feign结果解析来处理就可以了。
说明:任何Feign调用Result对象的data我们都必须默认给一个返回值,否则任务数据异常。
自定义解码器:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 
 | 
 
 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;
 
 if (result.getCode().intValue() != ResultCodeEnum.SUCCESS.getCode().intValue()) {
 throw new DecodeException(result.getCode(), result.getMessage(), response.request());
 }
 
 if (null == result.getData()) {
 throw new DecodeException(ResultCodeEnum.FEIGN_FAIL.getCode(), ResultCodeEnum.FEIGN_FAIL.getMessage(), response.request());
 }
 return result;
 }
 return object;
 }
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | @Configurationpublic class FeignConfig {
 
 
 
 
 
 
 
 
 @Bean
 public Decoder decoder(ObjectFactory<HttpMessageConverters> msgConverters, ObjectProvider<HttpMessageConverterCustomizer> customizers) {
 return new OptionalDecoder((new ResponseEntityDecoder(new FeignCustomDataDecoder(new SpringDecoder(msgConverters, customizers)))));
 }
 
 }
 
 | 
//TODO: 这一块需要对了解openfeign的结构有一定的了解,后续将对这一块进行补充