前言

最近在做一个项目的用户模块的认证功能,考虑到以后的扩展,于是想到了使用 设计模式 中的 策略模式 来实现。之前也看过不少使用 策略模式 的案例,但是感觉不是特别优雅,所以本文基于 枚举 配合 策略模式 来实现更灵活的功能扩展。

策略模式

策略模式定义

策略模式(Strategy Pattern)是一种行为设计模式,旨在定义一系列算法,将每一个算法封装起来,并使它们可以互换。策略模式允许算法的独立变化,而不需要修改使用这些算法的客户端代码。它的核心思想是将算法封装在独立的策略类中,并通过上下文类(Context)来使用这些策略。

策略模式组成

  1. 策略接口(Strategy): 定义一系列算法的共同接口。
  2. 具体策略(ConcreteStrategy): 实现策略接口的具体算法。
  3. 上下文(Context): 维护对某一策略对象的引用,并且可以使用这个策略来执行具体的算法。

策略模式结构

image-20240815113335006

策略模式优缺点

优点

  1. 开闭原则: 策略模式使得系统对扩展开放,对修改封闭。你可以通过增加新的策略类来扩展系统的功能,而不需要修改现有的代码。
  2. 代码复用: 通过将算法封装到策略类中,可以避免代码的重复,并且可以在不同的上下文中重用这些策略。
  3. 减少条件语句: 在使用策略模式时,所有的条件语句都被移到了策略类中,从而使上下文类的代码更加清晰。

缺点

  1. 增加类的数量: 每个策略都需要一个新的类,这可能会增加系统中的类的数量,增加系统的复杂性。
  2. 客户端必须知道所有的策略: 客户端需要知道所有的策略,并选择适合的策略,这可能会增加客户端的复杂度。

传统策略模式

策略定义和实现

@Data
public class AuthDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 手机号
     */
    private String cellphone;
}
public interface AuthStrategy {

    /**
     * 用户认证
     *
     * @param dto 认证参数
     * @return {@link UsersVO }
     */
    UsersVO auth(AuthDTO dto);
}
public class PasswordAuthStrategy implements AuthStrategy {

    private static final Logger log = LoggerFactory.getLogger(PasswordAuthStrategy.class);

    /**
     * 用户名密码认证
     */
    @Override
    public UsersVO auth(AuthDTO dto) {
        log.info("执行了用户名密码认证策略...");
        return null;
    }
}
public class SmsAuthStrategy implements AuthStrategy {

    private static final Logger log = LoggerFactory.getLogger(SmsAuthStrategy.class);

    /**
     * 短信认证
     */
    @Override
    public UsersVO auth(AuthDTO dto) {
        log.info("执行了短信认证策略...");
        return null;
    }
}
public class WechatCodeAuthStrategy implements AuthStrategy {

    private static final Logger log = LoggerFactory.getLogger(WechatCodeAuthStrategy.class);

    /**
     * 微信扫码认证
     */
    @Override
    public UsersVO auth(AuthDTO dto) {
        log.info("执行了微信扫码认证策略...");
        return null;
    }
}

策略上下文

public class AuthStrategyContext {

    private AuthStrategy authStrategy;

    public void setAuthStrategy(AuthStrategy authStrategy) {
        this.authStrategy = authStrategy;
    }

    public UsersVO auth(AuthDTO dto) {
        if (null == authStrategy) {
            throw new BusinessException("认证策略不能为空");
        }
        return authStrategy.auth(dto);
    }
}

传统策略模式实现

@Component
public class UserServiceImpl implements UserDetailsService {

    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    /**
     * 自定义Spring Security认证逻辑
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        AuthDTO authDTO;

        // 解析传入的json参数为AuthParamsDTO
        try {
            authDTO = JSON.parseObject(username, AuthDTO.class);
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("请求认证参数格式不正确, 认证参数: {}, 异常信息: []", username, e);
            throw new RuntimeException("请求认证参数格式不正确!");
        }

        AuthStrategyContext authStrategyContext = new AuthStrategyContext();

        authStrategyContext.setAuthStrategy(new PasswordAuthStrategy());
        // authStrategyContext.setAuthStrategy(new SmsAuthStrategy());
        // authStrategyContext.setAuthStrategy(new WechatCodeAuthStrategy());
        UsersVO usersVO = authStrategyContext.auth(authDTO);
        // 构建用户身份信息
        return buildUserPrincipal(usersVO);
    }
}

策略模式加枚举

策略定义和实现

@Data
public class AuthDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 手机号
     */
    private String cellphone;

    /**
     * 认证的类型   password:用户名密码认证   sms:短信认证  wechat_code:微信扫码认证
     */
    private String authType;
}
public enum AuthTypeEnum {

    /**
     * 用户名密码认证
     */
    PASSWORD("password", "用户名密码认证"),

    /**
     * 短信认证
     */
    SMS("sms", "短信认证"),

    /**
     * 微信扫码认证
     */
    WECHAT_CODE("wechat_code", "微信扫码认证");

    /**
     * 认证类型
     */
    private final String type;

    /**
     * 认证类型值
     */
    private final String text;

    public String getType() {
        return type;
    }

    public String getText() {
        return text;
    }

    AuthTypeEnum(String type, String text) {
        this.type = type;
        this.text = text;
    }
}
public interface AuthStrategy {

    /**
     * 认证类型枚举
     */
    AuthTypeEnum getAuthTypeEnum();

    /**
     * 认证
     *
     * @param dto 认证参数
     * @return {@link UsersVO }
     */
    UsersVO auth(AuthDTO dto);
}
@Slf4j
@Component(value = "passwordAuthStrategy")
public class PasswordAuthStrategy implements AuthStrategy {

    /**
     * 用户名密码类型
     */
    @Override
    public AuthTypeEnum getAuthTypeEnum() {
        return AuthTypeEnum.PASSWORD;
    }

    /**
     * 用户名密码认证
     */
    @Override
    public UsersVO auth(AuthDTO dto) {
        log.info("执行了用户名密码认证策略...");
        return null;
    }
}
@Slf4j
@Component(value = "smsAuthStrategy")
public class SmsAuthStrategy implements AuthStrategy {

    /**
     * 短信类型
     */
    @Override
    public AuthTypeEnum getAuthTypeEnum() {
        return AuthTypeEnum.SMS;
    }

    /**
     * 短信认证
     */
    @Override
    public UsersVO auth(AuthDTO dto) {
        log.info("执行了短信认证策略...");
        return null;
    }
}
@Slf4j
@Component(value = "wechatCodeAuthStrategy")
public class WechatCodeAuthStrategy implements AuthStrategy {

    /**
     * 微信扫码认证类型
     */
    @Override
    public AuthTypeEnum getAuthTypeEnum() {
        return AuthTypeEnum.WECHAT_CODE;
    }

    /**
     * 微信扫码认证
     */
    @Override
    public UsersVO auth(AuthDTO dto) {
        log.info("执行了微信扫码认证策略...");
        return null;
    }
}

策略上下文

@Component
public class AuthStrategyContext {

    @Resource
    private ApplicationContext applicationContext;

    /**
     * 存储策略类型和对应的策略实例
     */
    public final Map<String, AuthStrategy> strategyMap = new ConcurrentHashMap<>(16);

    /**
     * 初始化方法,在Bean创建后调用
     * 该方法获取所有AuthStrategy实例,并将它们存储在strategyMap中
     * 以便根据类型快速查找对应的策略。
     */
    @PostConstruct
    public void init() {
        // 获取实现AuthStrategy接口的所有实例bean
        Map<String, AuthStrategy> map = applicationContext.getBeansOfType(AuthStrategy.class);

        map.forEach((k, v) -> strategyMap.put(v.getAuthTypeEnum().getType(), v));
    }

    /**
     * 根据枚举类型获取对应的策略
     */
    public AuthStrategy getAuthStrategy(String enumType) {
        return strategyMap.get(enumType);
    }
}

策略模式配合枚举实现

@Component
public class UserServiceImpl implements UserDetailsService {

    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    @Resource
    private AuthStrategyContext authStrategyContext;

    /**
     * 自定义Spring Security认证逻辑
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        AuthDTO authDTO;

        // 解析传入的json参数为AuthParamsDTO
        try {
            authDTO = JSON.parseObject(username, AuthDTO.class);
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("请求认证参数格式不正确, 认证参数: {}, 异常信息: []", username, e);
            throw new RuntimeException("请求认证参数格式不正确!");
        }

        // 获取认证类型,根据认证类型获取对应的策略
        String authType = authDTO.getAuthType();
        AuthStrategy authStrategy = authStrategyContext.getAuthStrategy(authType);
        UsersVO usersVO = authStrategy.auth(authDTO);
        // 构建用户身份信息
        return buildUserPrincipal(usersVO);
    }
}

总结

可以看到,引入策略枚举后,在认证策略上下文里面,Spring 启动时就将所有的认证策略注入到容器中,后续直接通过认证类型就能获取到对应的认证策略,相比传统的策略模式更加清晰明了。使用策略模式虽然增加了大量的代码,但是对于后期的可扩展性和可维护性却有着很大的提升。好了就说这么多吧,要继续搬砖了。