在DailyMart中是如何支持多种登录方式的?

news2024/12/27 12:56:26

欢迎回来,我是飘渺。今天继续更新DDD&微服务的系列文章。

1.  理解DDD中的领域模型职责

在我们开始今天的主题之前,让我们先回答一些读者的疑问。

在上一篇文章 [[DailyMart05:通过用户注册呈现一个完整的DDD开发流程]] 发布以后,我收到不少粉丝的留言和私信。大家主要对我在文章中将一些用户行为放到应用层而非领域层的做法感到疑惑,他们的主要疑问是:“在DDD中,领域模型是充血模型,需要包含对象的属性和行为。那么,为什么用户唯一性校验和持久化保存不收敛在领域对象中呢?”

这是一个很好的问题。在DDD中,领域层的对象(如聚合、实体)确实包含属性和业务逻辑。然而,它们的主要职责是定义业务模型并封装业务逻辑。这意味着领域层应该处理那些直接涉及到业务规则和业务逻辑的操作,例如验证业务规则、保证业务数据的一致性和完整性、执行业务操作和业务流程,以及封装复杂的业务逻辑。

然而,并不是所有的业务逻辑都应该放在领域对象中。以下是一些典型的例子:

  1. 数据持久化操作:如保存、更新、删除等操作应由仓储来完成。领域模型不应该关心数据是如何存储和获取的。数据持久化通常由基础设施层来实现,而由应用服务层来调用。

  2. 查询操作:如果这些查询操作只是为了获取数据,而并非业务规则的一部分,那么它们应该在查询模型或仓储中实现,而不是领域模型中。

  3. 状态无关的计算:如果一项操作或计算并不会改变领域对象的状态,那么它可能更适合作为一个工具类或者领域服务的一部分,而不是领域模型的一部分。

现在,让我们开始今天的主题:如何让DailyMart支持多种登录方式。

2. DailyMart的灵活登录方式

在DailyMart系统中,我们为用户提供了多样化的登录方式。为了注册,用户需要提供用户名、密码、手机号和邮箱。然后,我们通过一个统一的登录接口,为用户提供三种登录方式:

  • 用户名+密码

  • 手机号+手机验证码

  • 邮箱+邮箱验证码

具体来说,如果用户选择用户名+密码方式登录,他们需要提供userName和password参数;如果选择手机号+手机验证码方式登录,他们需要提供phone和smsCode参数;如果选择邮箱+邮箱验证码方式登录,他们需要提供email和emailCode参数。无论用户选择哪种登录方式,一旦登录成功,系统都会返回一个jwt token。在DailyMart系统中进行的所有后续操作都需要携带此token参数。

看到这里,你可能会想到使用if...else 来实现这个功能,伪代码可能如下:

String logType = request.getParameter("logType");
if("USERNAME_PASSWORD".equals(logType)){
   String userName = request.getParameter("userName");
   String password = request.getParameter("password");
   loginWithUserName(userName,password);
}else if("PHONE_SMS".equals(logType)){
  ...
  loginWithPhone(phone,smsCode);
}else if("EMAIL_CODE".equals(logType)){
  ...
  loginWithEMail(email,emailCode);
}
...
# 生成jwt token
generateAccessToken();

虽然这种方法可以实现登录功能,但是代码中充斥着大量的if else的逻辑,显得不够优雅。

2.1 优化:采用策略模式

为了优化我们的代码,可以采用策略模式,这是一种常见的设计模式。

我们可以定义一个抽象的登录接口LoginProvider,并让具体的实现类来处理不同方式的登录逻辑。例如,UserNameLoginProvider负责处理用户名+密码的登录逻辑,PhoneLoginProvider负责处理手机号+手机验证码的登录逻辑,EmailLoginProvider负责处理邮箱+邮箱验证码的登录逻辑。这样,我们就可以避免大量的if else逻辑,使代码更加清晰和优雅。

e8cd761d830ec847145dd8e0d61dbd5e.png
使用策略模式的类图

然而,我们发现在这三种登录方式中,有一些共同的实现流程:

  1. 首先,需要完成用户参数的校验。

  2. 其次,需要实现用户登录的逻辑。例如,用户名密码模式需要校验密码是否正确,手机号+验证码登录逻辑中需要校验验证码是否正确匹配。

  3. 最后,需要生成jwt token。

在这三个步骤中,生成jwt token的逻辑是可以公用的,但现在却要在每个实现类中都实现一遍,这无疑也会带来大量重复的代码。

2.2 进一步优化:采用模板模式

为了解决这个问题,我们可以采用模板模式,这是另一种常见的设计模式。

我们可以定义一个抽象的登录接口AbstractLoginProvider,它实现了LoginProvider接口。在AbstractLoginProvider中,我们定义了登录流程的骨架流程。然后,我们让UserNameLoginProviderPhoneLoginProviderEmailLoginProvider继承AbstractLoginProvider,并分别实现用户校验的差异逻辑。公共的jwt token生成逻辑则直接由父类生成。

此时的类图关系如下所示:

a0058f1256d1dde2836021f7a23f96fb.png
使用模板模式的类图

在成功地应用模板模式和策略模式后,我们将公共的登录流程(例如生成jwt token)抽象到父类中,同时将各种登录方式的特定逻辑(例如用户登录校验)放在各自的子类中。这不仅减少了重复的代码,也使得每种登录方式的实现更加清晰和易于管理。此外,这种设计提高了代码的可扩展性。如果未来我们需要添加更多的登录方式,只需要添加新的子类即可。

2.3 处理不同登录方式的参数问题

最后,我们需要解决的是不同登录方式需要的参数不同的问题。

我们可以定义一个抽象参数基类UserLoginDTO,然后让不同登录方式的接口参数继承这个基类。以用户名密码登录方式为例:

@EqualsAndHashCode(callSuper = true)
@Data
public class UsernamePasswordLoginDTO extends UserLoginDTO {
    private final String username;
    private final String password;

    @Override
    public LoginType getLoginType() {
        return LoginType.USERNAME_PASSWORD;
    }
}

然后,在我们对外提供的登录接口中,我们可以根据登录类型组装不同的参数对象。当然,这部分逻辑我们可以通过简单工厂设计模式来封装,使得代码更加清晰和易于管理

488d0856a2aa4f77c1a9eaae5dc6a9e1.png
简单工厂

3. DailyMart登录的代码实现

接下来,我们将实现多种登录方式的功能。

3.1 定义系统的登录类型

首先,我们定义系统支持的登录类型,如下所示:

public enum LoginType {
    USERNAME_PASSWORD,
    PHONE_SMS,
    EMAIL_CODE;

    /**
     * 提供此方法主要是为了替换原始valueOf方法,捕获参数异常,用于转换成自定义异常
     */
    public static LoginType parseLoginType(String loginTypeStr){
        try {
            return LoginType.valueOf(loginTypeStr.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("不支持此登录方式:" + loginTypeStr);
        }
    }
}

3.2 定义抽象登录接口

接下来,我们定义一个抽象的登录接口LoginProvider,如下所示:

public interface LoginProvider {
    UserLoginRespDTO login(UserLoginDTO loginDTO);
    boolean supports(LoginType loginType);
}

3.3 定义模板接口

然后,我们定义一个模板接口AbstractLoginProvider,它实现了LoginProvider接口。在这个接口中,我们定义了登录流程的骨架流程,并将生成jwt token(基于nimbusds实现)的逻辑抽象到父类中。具体的用户参数校验和用户认证逻辑则由子类来实现。

@Component
@Slf4j
public abstract class AbstractLoginProvider implements LoginProvider{

    @Resource
    protected CustomerUserAssembler customerUserAssembler;

    @Override
    public UserLoginRespDTO login(UserLoginDTO loginDTO) {
        log.info("login start >>>:{}",loginDTO);
        // 1. 校验参数
        preAuthenticationCheck(loginDTO);

        // 2. 认证登录
        CustomerUser customerUser = authenticate(loginDTO);
        UserLoginRespDTO userLoginRespDTO = customerUserAssembler.customerUserToLoginRespDTO(customerUser);

        // 3. 生成accessToken
        String accessToken = generateAccessToken(userLoginRespDTO.getUserName());
        userLoginRespDTO.setAccessToken(accessToken);

        log.info("login end >>>:{}",userLoginRespDTO);

        return userLoginRespDTO;
    }

    protected abstract void preAuthenticationCheck(UserLoginDTO loginDTO);

    protected abstract CustomerUser authenticate(UserLoginDTO loginDTO);

    /**
     * 生成 accessToken
     */
     private String generateAccessToken(String subject) {
         try {
             return JwtUtil.createJWT(subject);
         } catch (JOSEException e) {
             throw new RuntimeException(e);
         }
     }


    @Override
    public abstract boolean supports(LoginType loginType) ;
}

需要强调的是,我们并没有将generateAccessToken()生成jwt的操作放在领域对象CustomerUser中,而是选择让应用服务直接调用JwtUtil这个工具类。这主要是因为生成jwt的操作并不涉及特定的业务规则或领域对象状态,因此,让应用服务直接调用工具类来完成这个操作更为合适。

3.4 实现登录逻辑

接下来,我们将实现登录逻辑。这里,我们以用户名密码登录为例:

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserNameLoginProvider extends AbstractLoginProvider{

    private final CustomerUserRepository customerUserRepository;

    @Override
    protected void preAuthenticationCheck(UserLoginDTO loginDTO) {
        UsernamePasswordLoginDTO usernamePasswordLoginDTO = (UsernamePasswordLoginDTO) loginDTO;
        if(StringUtils.isEmpty(usernamePasswordLoginDTO.getUsername()) || StringUtils.isEmpty(usernamePasswordLoginDTO.getPassword())){
            throw new IllegalArgumentException("用户名或密码错误");
        }
    }


    @Override
    protected CustomerUser authenticate(UserLoginDTO loginDTO) {
        UsernamePasswordLoginDTO usernamePasswordLoginDTO = (UsernamePasswordLoginDTO) loginDTO;

        CustomerUser actualUser = customerUserRepository.findByUserName(usernamePasswordLoginDTO.getUsername());

        if(actualUser == null){
            throw new RuntimeException("用户名不存在");
        }

        if(!actualUser.getPassword().matches(usernamePasswordLoginDTO.getPassword())){
            throw new RuntimeException("用户名密码错误");
        }

        return actualUser;
    }

    @Override
    public boolean supports(LoginType loginType) {
        return loginType.equals(LoginType.USERNAME_PASSWORD);
    }

}

在这个类中,我们首先在preAuthenticationCheck()方法中进行了参数校验。然后,在authenticate()方法中,我们根据用户名查找用户,并检查用户输入的密码是否正确。最后,在supports()方法中,我们指定了这个类支持的登录方式。

其他登录方式的实现基本类似。然而,目前在DailyMart中,我们还没有实现用户手机验证码和邮件验证码的发送功能。这是一个待填补的空白,我们将在完成通用消息平台的实现后再来补充这部分内容。

3.5 定义系统的登录类型

public enum LoginType {
    USERNAME_PASSWORD,
    PHONE_SMS,
    EMAIL_CODE;

    /**
     * 提供此方法主要是为了替换原始valueOf方法,捕获参数异常,用于转换成自定义异常
     */
    public static LoginType parseLoginType(String loginTypeStr){
        try {
            return LoginType.valueOf(loginTypeStr.toUpperCase());
        } catch (IllegalArgumentException e) {
            throw new RuntimeException("不支持此登录方式" + loginTypeStr);
        }
    }
}

3.6 通过策略模式选择具体的登录类型

最后,我们通过策略模式选择具体的登录类型,如下所示:

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class CustomerUserService {

    private final CustomerUserAssembler customerUserAssembler;

    private final List<LoginProvider> providers;

    public UserLoginRespDTO login(UserLoginDTO loginDTO) {
        return
            providers.stream()
                    .filter(provider -> provider.supports(loginDTO.getLoginType()))
                    .findFirst()
                    .map(provider -> provider.login(loginDTO))
                    .orElseThrow(() -> new IllegalArgumentException("Unsupported login method :" + loginDTO.getClass().getName()));
    }

}

在这个类中,我们首先获取所有的登录提供者,然后根据用户选择的登录类型找到对应的登录提供者,最后调用该提供者的login()方法进行登录。如果没有找到对应的登录提供者,我们将抛出一个异常。

3.7 定义参数转换工厂

在DDD的接口层,我们定义了一个参数转换工厂。这个工厂的作用是根据前端接口传入的登录类型,转换成对应的参数对象。具体实现如下:

public class LoginDTOFactory {
    public static UserLoginDTO getLoginDTO(Map<String, String> parameters) {
        String loginTypeStr = parameters.get("loginType");
        LoginType loginType = Optional.ofNullable(loginTypeStr)
                .map(String::toUpperCase)
                .map(LoginType::parseLoginType)
                .orElseThrow(() -> new RuntimeException("loginType must be provided"));


        return switch (loginType){
            case USERNAME_PASSWORD -> new UsernamePasswordLoginDTO(parameters.get("userName"), parameters.get("password"));
            case PHONE_SMS -> new PhoneCodeLoginDTO(parameters.get("phone"), parameters.get("smsCode"));
            case EMAIL_CODE -> new EmailCodeLoginDTO(parameters.get("email"),parameters.get("emailCode"));
        };

    }
}

3.8 定义登录接口

最后,我们定义了登录接口。在这个接口中,我们首先使用LoginDTOFactory将前端传入的参数转换成对应的参数对象,然后调用customerService.login()方法进行登录。具体实现如下:

@PostMapping("/api/customer/login")
    public UserLoginRespDTO login(@RequestBody Map<String, String> parameters){
        UserLoginDTO loginDTO = LoginDTOFactory.getLoginDTO(parameters);
        return customerService.login(loginDTO);
    }

3.9 登录测试

b967d1e2869436e8a7d607deeb4ec978.png

4. 小结

在本文中,我们探讨了如何在DailyMart系统中实现多种登录方式。

我们采用了策略模式和模板模式,将公共的登录流程抽象出来,同时将特定的登录逻辑放在各自的子类中,使得代码更加清晰和易于扩展。我们还解决了不同登录方式需要的参数不同的问题,通过定义一个抽象参数基类,并让不同登录方式的接口参数继承这个基类。最后,我们实现了登录逻辑,并定义了登录接口。这些设计和实现都遵循了DDD的开发规范,提高了代码的可读性和可维护性。

··············  END  ··············

最后,欢迎关注公众号和加入知识星球,获取最新的文章和源码更新。在公众号回复关键词“知识星球”,获取限量30元优惠券,每天仅需不到3毛钱。

81dee78a5e7d8f8677f2b32786480539.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/674363.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

团体程序设计天梯赛-练习集L1篇①

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;Hello大家好呀&#xff0c;我是陈童学&#xff0c;一个与你一样正在慢慢前行的普通人。 &#x1f3c0;个人主页&#xff1a;陈童学哦CSDN &#x1f4a1;所属专栏&#xff1a;PTA &#x1f381;希望各…

SpringBoot多线程异步任务:ThreadPoolTaskExecutor + CompletableFuture

SpringBoot多线程异步任务 需求 在 SpringBoot 项目中&#xff0c;一个任务比较复杂&#xff0c;执行时间比较长&#xff0c;需要采用 多线程异步 的方式执行&#xff0c;从而缩短任务执行时间。 多线程异步 将任务拆分成多个独立的子任务&#xff0c;每个子任务在独立子线…

Linux基础服务2——NFS

文章目录 一、基本了解二、NFS工作机制2.1 示例 三、NFS配置文件3.1 指定客户端3.2 指定权限3.2.1 访问权限3.2.2 用户映射选项3.2.3 其他选项 四、测试案例4.1 安装nfs服务4.2 客户端查看nfs共享策略4.3 客户端挂载nfs共享目录4.3.1 手动挂载4.3.2 自动挂载4.3.3 exportfs重新…

字符数据的表示

目录 1、 英文字符与字符串的表示 2、中文字符 1、 英文字符与字符串的表示 英文字符&#xff1a;用ASCII码&#xff08;128个字符&#xff09;表示 每个英文字符的ASCII码为一个字节&#xff0c;其中低7位有效&#xff0c;最高位为0&#xff0c;该位可用于别的目的&#x…

云原生下一代-服务治理

服务治理 在这里插入图片描述 目录 什么是服务治理如何防止外部突发流量冲垮服务 限制请求的QPS和并发请求数按照调用方进行限流通过中间件访问限流和提前通知下线节点 如何处理服务超时和限流的问题 设置超时时间并对错误进行分类处理启用服务限流控制请求的流量 如何处理服…

【kubernetes】部署网络组件Calico与CoreDNS

前言:二进制部署kubernetes集群在企业应用中扮演着非常重要的角色。无论是集群升级,还是证书设置有效期都非常方便,也是从事云原生相关工作从入门到精通不得不迈过的坎。通过本系列文章,你将从虚拟机配置开始,到使用二进制方式从零到一搭建起安全稳定的高可用kubernetes集…

【每天40分钟,我们一起用50天刷完 (剑指Offer)】第三天

专注 效率 记忆 预习 笔记 复习 做题 欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09;   文章字体风格&#xff1a; 红色文字表示&#…

Web安全——DIV CSS基础

DIV CSS基础 一、DIV和CSS样式二、样式表类型2.1 嵌入样式表2.2 外部样式2.3 内联样式 三、注释四、样式选择器组合选择器 五、背景六、边框七、文字属性八、文本属性九、列表十、超链接十一、盒子模型十二、Border 边框margin padding 十三、float 脱离文档流浮动十四、块级元…

算法-双指针-秋招算法冲刺

秋招冲刺算法 双指针 数组划分&#xff0c;数组分块 常⻅的双指针有两种形式&#xff0c;⼀种是对撞指针&#xff0c;⼀种是左右指针。 快慢指针 基本思想&#xff1a;使用两个移动速度不同的指针在数组或链表等序列结构上移动。通常处理结构类型&#xff1a;环形链表或数组…

redis协议与异步方式学习笔记

目录 1 交互方式 pipline2 广播机制2.1 概念演示2.2 使用场景 3 redis事物3.1 概念3.2 使用场景3.3 解决的问题3.3.1 背景&#xff1a;多线程竞争出现问题3.3.2 事务3.3.3 安全性事务 3.4两种类型的“事务”3.4.1 watch ... multi exec3.4.2 lua 脚本实现“原子”执行&#xff…

2023-01-11 LightDB高可用常用操作-管理.md

LightDB-高可用常用操作-管理篇 安装环境 操作系统&#xff1a;centos7 服务器IP:1.192.168.121.112 (主)2.192.168.121.113 (从)3.192.168.121.114 (哨兵-可选) 主库重启操作 1.先停止备库的keepalived,在root用户下执行 # 1.获得备库keepalived进 程pid [rootlocalhost ~]#…

闪亮登场!在树莓派上点亮LED灯的简单详细方法

文章目录 树莓派开发与STM32开发的比较原理图以及树莓派引脚展示点灯步骤读取树莓派布局 树莓派开发与STM32开发的比较 树莓派和STM32都是常用的嵌入式设备&#xff0c;都可以使用GPIO来控制LED灯。它们的点灯方式和使用的编程语言以及开发环境略有不同: 相同点&#xff1a; 控…

第五节 Hacker 登录界面

登录框用户界面 1. Entry 输入框 Tkinter中的Entry组件是一个单行文本输入框&#xff0c;用于接收用户在GUI应用程序中输入的信息。它可以被设置为只读或可编辑状态&#xff0c;可以设置输入的文本格式及长度限制等。当用户输入完成后&#xff0c;可以通过调用Entry组件的get(…

软件工程——第3章需求分析知识点整理

本专栏是博主个人笔记&#xff0c;主要目的是利用碎片化的时间来记忆软工知识点&#xff0c;特此声明&#xff01; 文章目录 1.需求分析的基本任务&#xff1f; 2.在需求分析阶段结束前&#xff0c;系统分析员应该做什么&#xff1f; 3.对软件系统有哪些综合要求&#xff1f…

基于Java农家乐信息平台设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

渣土车密闭运输识别算法 yolov7

渣土车密闭运输识别系统通过pythonyolov7网络模型技术&#xff0c;渣土车密闭运输识别算法对渣土车的密闭运输情况进行实时监测&#xff0c;检测到有未密闭的渣土车进入工地区域或者行驶在道路上时&#xff0c;渣土车密闭运输识别算法将自动发出警报提示现场管理人员及时采取措…

【openGauss数据库安全策略配置】--略有小成

【openGauss数据库安全策略配置】--略有小成 &#x1f53b; 一、openGauss数据库安全策略&#x1f530; 1.1 账户安全策略⛳ 1.1.1 自动锁定和解锁帐户&#x1f4a7; 1.1.1.1 配置failed_login_attempts参数&#x1f4a7; 1.1.1.2 配置password_lock_time参数 ⛳ 1.1.2 手动锁定…

MySQL实战解析底层---count(*)这么慢,该怎么办

目录 前言 count(*)的实现方式 用缓存系统保存计数 在数据库保存计数 不同的count用法 前言 在开发系统的时候&#xff0c;你可能经常需要计算一个表的行数&#xff0c;比如一个交易系统的所有变更记录总数这时候你可能会想&#xff0c;一条select count(*) fromt 语句不就…

Spring Boot 如何使用 @ExceptionHandler 注解处理异常消息

Spring Boot 如何使用 ExceptionHandler 注解处理异常消息 在 Spring Boot 应用程序中&#xff0c;异常处理是非常重要的一部分。当应用程序出现异常时&#xff0c;我们需要能够捕获和处理这些异常&#xff0c;并向用户提供有用的错误消息。在 Spring Boot 中&#xff0c;可以…

规则引擎--规则逻辑形如“1 (2 | 3)“的抽象

目录 规则下逻辑表达和条件的抽象表达逻辑的编码和抽象 规则&规则集合条件操作符规则规则执行表达式遍历进行操作符计算添加具体条件的执行 规则执行完成后得到最后的结果 规则下逻辑表达和条件的抽象 对于任何一个规则&#xff0c;包括多个条件&#xff0c;都可以抽象成如…