赞
踩
一般情况有用户交互的项目都有认证授权功能,首先要搞清楚两个概念:认证和授权。
认证: 就是校验用户的身份是否合法,常见的认证方式有账号密码登录、手机验证码登录等。
授权:则是该用户登录系统成功后当用户去点击菜单或操作数据时系统判断该用户是否有权限,有权限则允许继续操作,没有权限则拒绝访问。
本项目包括四个端:用户端(小程序)、服务端(app)、机构端(PC)、运营管理端(PC).
分别对应四类用户角色:家政需求方即c端用户,家政服务人员、家政服务公司(机构)、平台运营人员。
用户端通过小程序使用平台,初次使用小程序会进行认证,具体流程如下:
服务人员通过app登录采用手机验证码认证方式,输入手机号、发送验证码,验证码校验通过则认证通过,初次认证通过将自动注册服务人员信息。
机构端认证方式是账号密码认证方式,通过pc浏览器进入登录界面输入账号和密码登录系统
机构端提供单独的注册页面,输入手机号,接收验证码进行注册
下边测试用户端小程序的认证流程,先参考微信官方提供的小程序登录流程先大概知道小程序认证流程需要几部分,如下图:
从图上可以看出小程序认证流程需要三部分:
小程序:即前端程序
开发者服务器:后端微服务程序。
微信接口服务:即微信服务器。
1.前端调用wx.login()获取登录凭证code
2.前端请求后端进行认证,发送code
3.后端请求微信获取openid,发送appid、app密钥、code参数,微信返回openid
4.后端生成认证成功凭证返回给前端。
5.前端存储用户认证成功凭证
开发小程序首先要申请小程序账号,参考官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/getstart.html#%E7%94%B3%E8%AF%B7%E8%B4%A6%E5%8F%B7
通过阅读上述文档并且完成操作,本人已经完成小程序注册并保存好了密匙
此处不展开了
具体就是新建工程,同步Gitee,修改nacos配置文件的小程序认证部分等等
这部分也不再展开
整个过程包括三部分:
小程序:即前端程序
开发者服务器:后端微服务程序。
微信接口服务:即微信服务器。
具体的流程如下:
1.前端调用wx.login()获取登录凭证code
2.前端请求后端进行认证,发送code
3.后端请求微信获取openid
4.后端生成认证成功凭证返回给前端。
根据官方的认证流程我们定义本项目小程序认证的交互流程:
customer工程提供认证接口,publics工程作为一个公共服务提供与微信通信的接口。(抽取通用服务)
前端与cutomer交互不与publics交互。
首先认证的前端请求如下:
@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); } }
@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); }
其中customer模块的OpenIdResDTO openIdResDTO = wechatApi.getOpenId(loginForCustomerReqDTO.getCode());
代码就是调用publics模块的接口从而获取openid:
@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接口,其中内容如下:
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模块
customer模块拿到openid后开始查询数据库:
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);
}
那么如果查到了,说明注册过了,就会生成token:
//构建token
String token = jwtTool.createToken(commonUser.getId(), commonUser.getNickname(), commonUser.getAvatar(), UserType.C_USER);
return new LoginResDTO(token);
通过上述流程,前端就获得了token:
当拿到了token后,就会携带token去访问网关:
// 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模块的拦截器:
@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定义如下:
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获取即可
之后相信大家对于本项目的认证流程很熟悉了:
开发和部署细节省略
首先散户打开app,会输入手机号,点击获取验证码
之后会发出这个请求:
散户输入验证码后,点击登录即可进入主界面
短信验证流程如下:
那么下面我去实现这个功能
发送验证码:
@RestController
@RequestMapping("/sms-code")
@Api(tags = "验证码相关接口")
public class SmsCodeController {
@Resource
private ISmsCodeService smsCodeService;
@PostMapping("/send")
@ApiOperation("发送短信验证码")
public void smsCodeSend(@RequestBody SmsCodeSendReqDTO smsCodeSendReqDTO) {
smsCodeService.smsCodeSend(smsCodeSendReqDTO);
}
}
验证验证码:
@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);
}
}
当用户点击了发送验证码后,首先会调用Public模块的服务,生成一个随机的验证码存储在redis中为验证做准备:
@Override
public void smsCodeSend(SmsCodeSendReqDTO smsCodeSendReqDTO) {
if(StringUtils.isEmpty(smsCodeSendReqDTO.getPhone()) || StringUtils.isEmpty(smsCodeSendReqDTO.getBussinessType())) {
log.debug("不能发送短信验证码,phone:{},bussinessType:{}", smsCodeSendReqDTO.getPhone(), smsCodeSendReqDTO.getBussinessType());
return;
}
String redisKey = String.format(CommonRedisConstants.RedisKey.VERIFY_CODE, smsCodeSendReqDTO.getPhone(), smsCodeSendReqDTO.getBussinessType());
// 取6位随机数
// String verifyCode = (int)(Math.random() * 1000000) + "";
String verifyCode = "123456";//为方便测试固定为123456
log.info("向手机号{}发送验证码{}",smsCodeSendReqDTO.getPhone(),verifyCode);
//todo调用短信平台接口向指定手机发验证码...
//短信验证码有效期5分钟
redisTemplate.opsForValue().set(redisKey, verifyCode, 300, TimeUnit.SECONDS);
}
当用户输入验证码后,点击登录按钮则进行校验:
这次是调用Customer模块的服务
发送的请求参数是手机号,验证码以及验证身份的代码,收到请求后,先判空,若不为空,则交由Public模块去验证验证码的正确性,假如验证码没错,那么检查用户是否冻结或者没有注册,若没有注册,则自动注册,存入数据库,生成并返回token:
@Override public LoginResDTO loginForVerify(LoginForWorkReqDTO loginForWorkReqDTO) { // 数据校验 if(StringUtils.isEmpty(loginForWorkReqDTO.getVeriryCode())){ throw new BadRequestException("验证码错误,请重新获取"); } //远程调用publics服务校验验证码是否正确 boolean verifyResult = smsCodeApi.verify(loginForWorkReqDTO.getPhone(), SmsBussinessTypeEnum.SERVE_STAFF_LOGIN, loginForWorkReqDTO.getVeriryCode()).getIsSuccess(); if(!verifyResult) { throw new BadRequestException("验证码错误,请重新获取"); } // 登录校验 // 根据手机号和用户类型获取服务人员或机构信息 ServeProvider serveProvider = serveProviderService.findByPhoneAndType(loginForWorkReqDTO.getPhone(), loginForWorkReqDTO.getUserType()); // 账号禁用校验 if(serveProvider != null && CommonStatusConstants.USER_STATUS_FREEZE == serveProvider.getStatus()) { throw new CommonException(ErrorInfo.Code.ACCOUNT_FREEZED, serveProvider.getAccountLockReason()); } // 自动注册 if(serveProvider == null) { serveProvider = serveProviderService.add(loginForWorkReqDTO.getPhone(), UserType.WORKER, null); } // 生成登录token String token = jwtTool.createToken(serveProvider.getId(), serveProvider.getName(), serveProvider.getAvatar(), loginForWorkReqDTO.getUserType()); return new LoginResDTO(token); }
Public模块校验代码如下:
首先根据手机号和人员类型拼接出key,从Redis取出相应的验证码,之后进行一个校验,由于验证码只能用一次,通过校验则立马删除Redis的该条记录
@Override
public boolean verify(String phone, SmsBussinessTypeEnum bussinessType, String verifyCode) {
// 1.验证前准备
String redisKey = String.format(CommonRedisConstants.RedisKey.VERIFY_CODE, phone, bussinessType.getType());
String verifyCodeInRedis = redisTemplate.opsForValue().get(redisKey);
// 2.短验验证,验证通过后删除code,code只能使用一次
boolean verifyResult = StringUtils.isNotEmpty(verifyCode) && verifyCode.equals(verifyCodeInRedis);
if(verifyResult) {
redisTemplate.delete(redisKey);
}
return verifyResult;
}
所以大家应该理解了下图:
@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);
}
}
首先会进行判空,若不为空,则根据账号去查数据库,以及进行密码校验(BCrypt加密),若通过则生成并返回token
@Override public LoginResDTO loginForPassword(LoginForWorkReqDTO loginForWorkReqDTO) { // 1.数据校验 if(StringUtils.isEmpty(loginForWorkReqDTO.getPassword())) { throw new BadRequestException("请输入密码"); } // 2.登录校验 // 2.1.根据手机号和用户类型获取服务人员或机构信息 ServeProvider serveProvider = serveProviderService.findByPhoneAndType(loginForWorkReqDTO.getPhone(), loginForWorkReqDTO.getUserType()); // 账号禁用校验 if(serveProvider != null && CommonStatusConstants.USER_STATUS_FREEZE == serveProvider.getStatus()) { throw new CommonException(ErrorInfo.Code.ACCOUNT_FREEZED, serveProvider.getAccountLockReason()); } //密码校验 if(serveProvider == null || !passwordEncoder.matches(loginForWorkReqDTO.getPassword(), serveProvider.getPassword())){ throw new BadRequestException("账号或密码错误,请重新输入"); } // 3.生成登录token String token = jwtTool.createToken(serveProvider.getId(), serveProvider.getName(), serveProvider.getAvatar(), loginForWorkReqDTO.getUserType()); return new LoginResDTO(token); }
BCrypt是一种密码哈希函数,通常用于存储用户密码的安全性。它是基于 Blowfish 密码算法的一种单向哈希函数
使用很简单,如下:
public static void main(String[] args) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); /** $2a$10$1sp7I0OdYH3Azs/2lK8YYeuiaGZzOGshGT9j.IYArZftsGNsXqlma $2a$10$m983E2nmJ7ITlesbXzjbzO/M7HL2wP8EgpgX.pPACDm1wG38Lt.na $2a$10$rZvathrW98vVPenLhOnl0OMpUtRTdBkWJ45IkIsTebITS9AFgKqGK $2a$10$2gaMKWCRoKdc42E0jsq7b.munjzOSPOM4yr3GG9M6194E7dOH5LyS $2a$10$I/n93PIKpKL8m4O3AuT5kuZncZhfqV51bfx5sJrplnYoM7FimdboC */ for (int i = 0; i < 5; i++) { //对密码进行哈希 String encode = passwordEncoder.encode("11111"); System.out.println(encode); } //校验哈希串和密码是否匹配 boolean matches = passwordEncoder.matches("11111", "$2a$10$m983E2nmJ7ITlesbXzjbzO/M7HL2wP8EgpgX.pPACDm1wG38Lt.na"); System.out.println(matches); }
用户点击注册按钮:
输入手机号获取验证码,再输入密码进行注册,接口设计如下:
接口地址:POST/customer/open/serve-provider/institution/register
@RestController("openServeProviderController")
@RequestMapping("/open/serve-provider")
@Api(tags = "白名单接口 - 服务人员或机构相关接口")
public class ServeProviderController {
@Autowired
private IServeProviderService iServeProviderService;
@PostMapping("/register")
public void institutionRegister(@RequestBody InstitutionRegisterReqDTO institutionRegisterReqDTO){
iServeProviderService.registerInstitution(institutionRegisterReqDTO);
}
}
首先校验验证码,假如验证码没问题就进行注册:
/**
* 机构注册
* @param institutionRegisterReqDTO
*/
@Override
public void registerInstitution(InstitutionRegisterReqDTO institutionRegisterReqDTO) {
//1.验证验证码
boolean verifyResult = smsCodeApi.verify(institutionRegisterReqDTO.getPhone(),SmsBussinessTypeEnum.INSTITION_REGISTER,institutionRegisterReqDTO.getVerifyCode()).getIsSuccess();
if(verifyResult){
throw new BadRequestException("验证码校验失败");
}
//2.新增机构
owner.add(institutionRegisterReqDTO.getPhone(),UserType.INSTITUTION,institutionRegisterReqDTO.getPassword());
}
其中注册接口如下,先验证手机号是否已经被注册,没有则写入数据库:
@Override @Transactional(rollbackFor = Exception.class) public ServeProvider add(String phone, Integer type, String password) { // 校验手机号是否存在 ServeProvider existServeProvider = lambdaQuery().eq(ServeProvider::getPhone, phone) .one(); if (existServeProvider != null) { if(existServeProvider.getType().equals(UserType.WORKER)){ throw new BadRequestException("该账号已被服务人员注册"); }else { throw new BadRequestException("该账号已被机构注册"); } } //新增服务人员/机构信息 ServeProvider serveProvider = new ServeProvider(); serveProvider.setPhone(phone); serveProvider.setPassword(password); serveProvider.setType(type); serveProvider.setStatus(CommonStatusConstants.USER_STATUS_NORMAL); serveProvider.setCode(IdUtils.getSnowflakeNextIdStr()); baseMapper.insert(serveProvider); //新增服务人员/机构配置信息同步表信息,方便后期对配置项进行配置 serveProviderSettingsService.add(serveProvider.getId(), type); return serveProvider; }
@Override
@Transactional(rollbackFor = {Exception.class})
public void add(Long id, Integer serveProviderType) {
ServeProviderSettings serveProviderSettings = new ServeProviderSettings();
serveProviderSettings.setId(id);
if(baseMapper.insert(serveProviderSettings) <= 0 || serveProviderSyncService.add(id, serveProviderType) <= 0){
throw new DBException("请求失败");
}
}
首先用户忘记密码,点击找回密码:
全部输入后发出请求,接口如下:
POST/customer/agency/serve-provider/institution/resetPassword
首先校验验证码是否正确。
校验手机号是否存在数据库。
通过校验最后修改密码,密码的加密方式参考机构注册接口。
@RestController("agencyServeProviderController") @RequestMapping("/agency/serve-provider") @Api(tags = "机构端 - 服务人员或机构相关接口") public class ServeProviderController { @Resource private IServeProviderService serveProviderService; @GetMapping("/currentUserInfo") @ApiOperation("获取当前用户信息") public ServeProviderInfoResDTO currentUserInfo() { return serveProviderService.currentUserInfo(); } @PostMapping("/institution/resetPassword") @ApiOperation("机构登录密码重置接口") public void resetPassword(@RequestBody InstitutionResetPasswordReqDTO institutionResetPasswordReqDTO){ serveProviderService.resetPassword(institutionResetPasswordReqDTO); } }
/** * c机构重置密码 * @param institutionResetPasswordReqDTO */ @Override public void resetPassword(InstitutionResetPasswordReqDTO institutionResetPasswordReqDTO) { //1.校验验证码 Boolean verifyResult = smsCodeApi.verify(institutionResetPasswordReqDTO.getPhone(),SmsBussinessTypeEnum.INSTITUTION_RESET_PASSWORD,institutionResetPasswordReqDTO.getVerifyCode()).getIsSuccess(); if(!verifyResult){ throw new BadRequestException("验证码校验错误"); } //2.校验数据库是否已经存在这个手机号 ServeProvider serveProvider = lambdaQuery().eq(ServeProvider::getPhone, institutionResetPasswordReqDTO.getPhone()).one(); if(serveProvider == null){ throw new BadRequestException("手机号不存在"); } //3.修改密码 lambdaUpdate().eq(ServeProvider::getPhone, institutionResetPasswordReqDTO.getPhone()) .set(ServeProvider::getPassword,passwordEncoder.encode(institutionResetPasswordReqDTO.getPassword())).update(); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。