登录

微信的登录逻辑

说明:
  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份

对于业务而言,最首先返回的就是这个code,后端要做的就是把这个code结合自己小程序的appid和appsecret去请求微信后台,给出当前用户的openid。

接口1:GET”/customer/login/{code}”

整体的流程是:

  1. 对外service拿着这个前端发来的code去远程调用乘客微服务
  2. 乘客微服务请求微信的后台得到openid,然后去数据库查是否存在这个openid,也就是是否注册过了
  3. 如果没有注册就进行注册,最后写入操作日志,返回这个openid对应的数据库id,我们的业务是基于自家数据库的。
  4. 对外service拿到了这个id,将其缓存进redis中,key是UUID,value是在自家数据库中的id,也就是说在redis中有这个键值对那就说明是登录状态。
  5. 最后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) {
//获取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;
}

乘客微服务接口:

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;

/**
* 条件:
* 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();
}

接口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;

/**
*
* @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();
}

}

有了这个注解再进行个人资料的获取,现在直接就在接口上用则个注解,然后再在方法里面取出id就可以了,大致流程是:

  1. 用注解得到的id请求客户微服务
  2. 客户微服务用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
/**
* 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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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)))));
}

}

//TODO: 这一块需要对了解openfeign的结构有一定的了解,后续将对这一块进行补充