登录
微信的登录逻辑
说明:
- 调用 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?
对外接口:
1 2 3 4 5
| @Operation(summary = "小程序授权登录") @GetMapping("/login/{code}") public Result<String> wxLogin(@PathVariable String code) { return Result.ok(customerInfoService.login(code)); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Autowired private 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; }
|
乘客微服务接口:
1 2 3 4 5
| @Operation(summary = "小程序授权登录") @GetMapping("/login/{code}") public Result<Long> login(@PathVariable String code) { return Result.ok(customerInfoService.login(code)); }
|
1 2 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
| @Autowired private 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中
1 2 3 4 5 6
| @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface GuiguLogin {
}
|
1 2 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查数据库
对外接口:
1 2 3 4 5 6 7
| @Operation(summary = "获取客户登录信息") @GuiguLogin @GetMapping("/getCustomerLoginInfo") public Result<CustomerLoginVo> getCustomerLoginInfo() { Long customerId = AuthContextHolder.getUserId(); return Result.ok(customerInfoService.getCustomerLoginInfo(customerId)); }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @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; }
|
客户微服务:
这里只写实现类,接口只是传导没有实际逻辑。
1 2 3 4 5 6 7 8 9 10
| @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; }
|
自定义feign全局处理
在Feign调用的过程中,由于全局异常的处理,所有的Feign调用都会返回Result对象,我们还必须判断它的code是否等于200,如果不等于200,那么说明调用结果抛出异常了,我们必须返回异常信息提示给接口,如果返回code等于200,我们又必须判断data是否等于null,处理方式都一致,处理起来很繁琐,有没有好的统一处理方式呢?
答案是肯定的,我们可以通过全局自定义Feign结果解析来处理就可以了。
说明:任何Feign调用Result对象的data我们都必须默认给一个返回值,否则任务数据异常。
自定义解码器:
1 2 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; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Configuration public 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的结构有一定的了解,后续将对这一块进行补充