客户管理模块
- 1.认证模块
-
- [1.1 认证方式介绍](#1.1 认证方式介绍)
-
- [1.1.1 小程序认证](#1.1.1 小程序认证)
- [1.1.2 手机验证码登录](#1.1.2 手机验证码登录)
- [1.1.3 账号密码认证](#1.1.3 账号密码认证)
- [1.2 小程序认证](#1.2 小程序认证)
-
- [1.2.1 小程序申请](#1.2.1 小程序申请)
- [1.2.2 创建客户后端工程jzo2o-customer](#1.2.2 创建客户后端工程jzo2o-customer)
- [1.2.3 开发部署前端](#1.2.3 开发部署前端)
- [1.2.4 小程序认证流程](#1.2.4 小程序认证流程)
-
- [1.2.4.1 customer小程序认证接口设计](#1.2.4.1 customer小程序认证接口设计)
- [1.2.5 手机验证码认证](#1.2.5 手机验证码认证)
-
- [1.2.5.1 开发与部署app前端](#1.2.5.1 开发与部署app前端)
- [1.2.5.2 customer短信认证接口设计](#1.2.5.2 customer短信认证接口设计)
1.认证模块
一般情况有用户交互的项目都有认证授权功能,首先要搞清楚两个概念:认证和授权。
认证: 就是校验用户的身份是否合法,常见的认证方式有账号密码登录、手机验证码登录等。
授权:则是该用户登录系统成功后当用户去点击菜单或操作数据时系统判断该用户是否有权限,有权限则允许继续操作,没有权限则拒绝访问。
本项目包括四个端:用户端(小程序)、服务端(app)、机构端(PC)、运营管理端(PC).
分别对应四类用户角色:家政需求方即c端用户,家政服务人员、家政服务公司(机构)、平台运营人员。
1.1 认证方式介绍
1.1.1 小程序认证
用户端通过小程序使用平台,初次使用小程序会进行认证,具体流程如下:
1.1.2 手机验证码登录
服务人员通过app登录采用手机验证码认证方式,输入手机号、发送验证码,验证码校验通过则认证通过,初次认证通过将自动注册服务人员信息。
1.1.3 账号密码认证
机构端认证方式是账号密码认证方式,通过pc浏览器进入登录界面输入账号和密码登录系统
机构端提供单独的注册页面,输入手机号,接收验证码进行注册
1.2 小程序认证
下边测试用户端小程序的认证流程,先参考微信官方提供的小程序登录流程先大概知道小程序认证流程需要几部分,如下图:
从图上可以看出小程序认证流程需要三部分:
小程序:即前端程序
开发者服务器:后端微服务程序。
微信接口服务:即微信服务器。
1.前端调用wx.login()获取登录凭证code
2.前端请求后端进行认证,发送code
3.后端请求微信获取openid,发送appid、app密钥、code参数,微信返回openid
4.后端生成认证成功凭证返回给前端。
5.前端存储用户认证成功凭证
1.2.1 小程序申请
开发小程序首先要申请小程序账号,参考官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/getstart.html#申请账号
通过阅读上述文档并且完成操作,本人已经完成小程序注册并保存好了密匙
1.2.2 创建客户后端工程jzo2o-customer
此处不展开了
具体就是新建工程,同步Gitee,修改nacos配置文件的小程序认证部分等等
1.2.3 开发部署前端
这部分也不再展开
1.2.4 小程序认证流程
整个过程包括三部分:
小程序:即前端程序
开发者服务器:后端微服务程序。
微信接口服务:即微信服务器。
具体的流程如下:
1.前端调用wx.login()获取登录凭证code
2.前端请求后端进行认证,发送code
3.后端请求微信获取openid
4.后端生成认证成功凭证返回给前端。
根据官方的认证流程我们定义本项目小程序认证的交互流程:
customer工程提供认证接口,publics工程作为一个公共服务提供与微信通信的接口。(抽取通用服务)
前端与cutomer交互不与publics交互。
1.2.4.1 customer小程序认证接口设计
首先认证的前端请求如下:
Controller层
java
@RestController("openLoginController")
@RequestMapping("/open/login")
@Api(tags = "白名单接口 - 客户登录相关接口")
public class LoginController {
@Resource
private ILoginService loginService;
@PostMapping("/worker")
@ApiOperation("服务人员/机构人员登录接口")
public LoginResDTO loginForWorker(@RequestBody LoginForWorkReqDTO loginForWorkReqDTO) {
if(UserType.INSTITUTION == loginForWorkReqDTO.getUserType()){
return loginService.loginForPassword(loginForWorkReqDTO);
}else{
return loginService.loginForVerify(loginForWorkReqDTO);
}
}
/**
* c端用户登录接口
*/
@PostMapping("/common/user")
@ApiOperation("c端用户登录接口")
public LoginResDTO loginForCommonUser(@RequestBody LoginForCustomerReqDTO loginForCustomerReqDTO) {
return loginService.loginForCommonUser(loginForCustomerReqDTO);
}
}
Service层
java
@Override
public LoginResDTO loginForCommonUser(LoginForCustomerReqDTO loginForCustomerReqDTO) {
// code换openId
OpenIdResDTO openIdResDTO = wechatApi.getOpenId(loginForCustomerReqDTO.getCode());
if(ObjectUtil.isEmpty(openIdResDTO) || ObjectUtil.isEmpty(openIdResDTO.getOpenId())){
// openid申请失败
throw new CommonException(ErrorInfo.Code.LOGIN_TIMEOUT, ErrorInfo.Msg.REQUEST_FAILD);
}
CommonUser commonUser = commonUserService.findByOpenId(openIdResDTO.getOpenId());
//如果未从数据库查到,需要新增数据
if (ObjectUtil.isEmpty(commonUser)) {
commonUser = BeanUtil.toBean(loginForCustomerReqDTO, CommonUser.class);
long snowflakeNextId = IdUtil.getSnowflakeNextId();
commonUser.setId(snowflakeNextId);
commonUser.setOpenId(openIdResDTO.getOpenId());
commonUser.setNickname("普通用户"+ RandomUtil.randomInt(10000,99999));
commonUserService.save(commonUser);
}else if(CoCummonStatusConstants.USER_STATUS_FREEZE == commonUser.getStatus()) {
// 被冻结
throw new CommonException(ErrorInfo.Code.ACCOUNT_FREEZED, commonUser.getAccountLockReason());
}
//构建token
String token = jwtTool.createToken(commonUser.getId(), commonUser.getNickname(), commonUser.getAvatar(), UserType.C_USER);
return new LoginResDTO(token);
}
调用Publics模块获取openid
其中customer模块的OpenIdResDTO openIdResDTO = wechatApi.getOpenId(loginForCustomerReqDTO.getCode());
代码就是调用publics模块的接口从而获取openid:
java
@Resource
private WechatService wechatService;
@Override
@GetMapping("/getOpenId")
@ApiOperation("获取openId")
@ApiImplicitParams({
@ApiImplicitParam(name = "code", value = "登录凭证", required = true, dataTypeClass = String.class)
})
public OpenIdResDTO getOpenId(@RequestParam("code") String code) {
String openId = wechatService.getOpenid(code);
return new OpenIdResDTO(openId);
}
而在其中调用了第三方模块的wechatService.getOpenid接口,其中内容如下:
java
public String getOpenid(String code) {
Map<String, Object> requestUrlParam = this.getAppConfig();
requestUrlParam.put("js_code", code);
String result = HttpUtil.get("https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code", requestUrlParam);
log.info("getOpenid result:{}", result);
JSONObject jsonObject = JSONUtil.parseObj(result);
if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) {
throw new ServerErrorException(jsonObject.getStr("errmsg"));
} else {
return jsonObject.getStr("openid");
}
}
功能很简单,通过调用微信官方api获取到数据,从数据中解析出openid返回publics模块,最后返回到customer模块
查询数据库构建token
customer模块拿到openid后开始查询数据库:
java
CommonUser commonUser = commonUserService.findByOpenId(openIdResDTO.getOpenId());
假如数据库没查到,说明第一次使用,那么就会自动注册:
java
if (ObjectUtil.isEmpty(commonUser)) {
commonUser = BeanUtil.toBean(loginForCustomerReqDTO, CommonUser.class);
long snowflakeNextId = IdUtil.getSnowflakeNextId();
commonUser.setId(snowflakeNextId);
commonUser.setOpenId(openIdResDTO.getOpenId());
commonUser.setNickname("普通用户"+ RandomUtil.randomInt(10000,99999));
commonUserService.save(commonUser);
}
那么如果查到了,说明注册过了,就会生成token:
java
//构建token
String token = jwtTool.createToken(commonUser.getId(), commonUser.getNickname(), commonUser.getAvatar(), UserType.C_USER);
return new LoginResDTO(token);
保存token到微服务的ThreadLocal中
通过上述流程,前端就获得了token:
当拿到了token后,就会携带token去访问网关:
java
// 2.获取token解析工具JwtTool工具
// 2.1.获取token
String token = GatewayWebUtils.getRequestHeader(exchange, HEADER_TOKEN);
if (StringUtils.isEmpty(token)) {
return GatewayWebUtils.toResponse(exchange,
HttpStatus.FORBIDDEN.value(),
ErrorInfo.Msg.REQUEST_FORBIDDEN);
}
// 2.2.获取tokenKey
String tokenKey = applicationProperties.getTokenKey().get(JwtTool.getUserType(token) + "");
if (StringUtils.isEmpty(token)) {
return GatewayWebUtils.toResponse(exchange,
HttpStatus.FORBIDDEN.value(),
ErrorInfo.Msg.REQUEST_FORBIDDEN);
}
如果网关校验token通过,就会放行到微服务,微服务就会将token放入ThreadLocal中,由于所有的微服务都需要走这个流程(将token放入ThreadLocal中),因此抽取这部分到MVC模块的拦截器:
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.尝试获取头信息中的用户信息
String userInfo = request.getHeader(HeaderConstants.USER_INFO);
// 2.判断是否为空
if (userInfo == null) {
return true;
}
try {
// 3.base64解码用户信息
String decodeUserInfo = Base64Utils.decodeStr(userInfo);
CurrentUserInfo currentUserInfo = JsonUtils.toBean(decodeUserInfo, CurrentUserInfo.class);
// 4.转为用户id并保存
UserContext.set(currentUserInfo);
return true;
} catch (NumberFormatException e) {
log.error("用户身份信息格式不正确,{}, 原因:{}", userInfo, e.getMessage());
return true;
}
}
其中ThreadLocal定义如下:
java
public class UserContext {
private static final ThreadLocal<CurrentUserInfo> THREAD_LOCAL_USER = new ThreadLocal<>();
/**
* 获取当前用户id
*
* @return 用户id
*/
public static Long currentUserId() {
return THREAD_LOCAL_USER.get().getId();
}
public static CurrentUserInfo currentUser() {
return THREAD_LOCAL_USER.get();
}
/**
* 设置当前用户id
*
* @param currentUserInfo 当前用户信息
*/
public static void set(CurrentUserInfo currentUserInfo) {
THREAD_LOCAL_USER.set(currentUserInfo);
}
/**
* 清理当前线程中的用户信息
*/
public static void clear(){
THREAD_LOCAL_USER.remove();
}
}
那么微服务之后获取token只需要从ThreadLocal获取即可
之后相信大家对于本项目的认证流程很熟悉了:
1.2.5 手机验证码认证
1.2.5.1 开发与部署app前端
开发和部署细节省略