策略模式结合模板方法实现不同登录方式

1.概述

1.1 策略模式

策略模式是一种行为模式,它将对象和行为分开了,行为变成了一个接口以及这个行为的多个实现。策略模式可以让这些行为之间进行切换。

策略模式有3种角色,分别为:选择器抽象策略策略实例

其中选择器selector又被称为上下文context,其作用为通过不同的标识来获取对应的策略实例。策略实例就是封装不同算法的实例对象,而抽象策略就是策略实例的顶层接口。

image-20230907162617775

1.2 模板方法

模板方法通过继承来实现,顶层是一个抽象类,用于封装通用函数,并提供一个或多个抽象方法,下层是多个实现类,用于实现不同的业务逻辑分支,类图如下:

image-20230907163412965

模板方法实现:

实际使用的时候,一般会通过子类的实例调用父类中的模板方法templateMethod,在模板方法中调用抽象方法,最终还是会调用到子类中覆写的实例方法,这是一种常见的钩子函数使用方式。

  • 抽象父类:

    /**
     * 抽象父类
     */
    public abstract class BaseClass {
        final public void templateMethod() {
            System.out.println("执行模板方法");
            method1();
            method2();
        }
    
        abstract protected void method1();
    
        abstract protected void method2();
    }
    
  • 子类实现

    /**
     * 子类1
     */
    public class ChildClass1 extends BaseClass {
        @Override
        protected void method1() {
            System.out.println("执行子类1的方法1");
        }
        @Override
        protected void method2() {
            System.out.println("执行子类1的方法2");
        }
    }
    
    /**
     * 子类2
     */
    public class ChildClass2 extends BaseClass {
        @Override
        protected void method1() {
            System.out.println("执行子类2的方法1");
        }
        @Override
        protected void method2() {
            System.out.println("执行子类2的方法2");
        }
    }
    

2 实例

假设我们在用户登录模块中需要实现从不同的第三方登录的需求,这里假设分别是QQ登录和微信登录,下面用策略模式结合模板方法来进行实现这个需求。

2.1 实现思路

  • 前置设计:通过定义常量来标识策略的类型,使用者调用时可以通过常量获取对应的策略实例。
  • QQ登录微信登录分别对应两个bean实例,在内部各自实现其登录逻辑,在两个bean实例的上层是抽象策略,有一个通用的接口(或抽象类)用于提供访问入口
  • 选择器设计:可以通过Map来存储数据,调用者调用时可以通过策略标识来获取对应的策略实例。

2.2 实现代码

  1. 编写抽象策略接口和抽象策略实现类

    // 抽象策略接口
    public interface SocialLoginStrategy {
        /**
         * 登录
         *
         * @param data 数据
         * @return {@link UserInfoDTO} 用户信息
         */
        UserInfoDTO login(String data);
    
    }
    
    // 抽象策略实现类
    @Service
    public abstract class AbstractSocialLoginStrategyImpl implements SocialLoginStrategy {
    
        @Override
        public UserInfoDTO login(String data) {
            // 创建登录信息
            UserDetailDTO userDetailDTO;
            // 获取第三方token信息
            SocialTokenDTO socialToken = getSocialToken(data);
            // 获取用户ip信息
            String ipAddress = IpUtils.getIpAddress(request);
            String ipSource = IpUtils.getIpSource(ipAddress);
            // 判断是否已注册
            UserAuth user = getUserAuth(socialToken);
            if (Objects.nonNull(user)) {
                // 返回数据库用户信息
                userDetailDTO = getUserDetail(user, ipAddress, ipSource);
            } else {
                // 获取第三方用户信息,保存到数据库返回
                userDetailDTO = saveUserDetail(socialToken, ipAddress, ipSource);
            }
            // 判断账号是否禁用
            if (userDetailDTO.getIsDisable().equals(TRUE)) {
                throw new BizException("账号已被禁用");
            }
            // 将登录信息放入springSecurity管理
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetailDTO, null, userDetailDTO.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(auth);
    
            // 登录日志
            LoginLog loginLog = UserUtils.getLoginLog(userDetailDTO);
            loginLog.setStatus(1);
            loginLog.setMessage("登录成功");
            loginLogMapper.insert(loginLog);
    
            // 返回用户信息
            return BeanCopyUtils.copyObject(userDetailDTO, UserInfoDTO.class);
        }
    
        /**
         * 获取第三方token信息
         *
         * @param data 数据
         * @return {@link SocialTokenDTO} 第三方token信息
         */
        public abstract SocialTokenDTO getSocialToken(String data);
    
        /**
         * 获取第三方用户信息
         *
         * @param socialTokenDTO 第三方token信息
         * @return {@link SocialUserInfoDTO} 第三方用户信息
         */
        public abstract SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO);
    
        /**
         * 获取用户账号
         *
         * @return {@link UserAuth} 用户账号
         */
        private UserAuth getUserAuth(SocialTokenDTO socialTokenDTO) {
            return userAuthMapper.selectOne(new LambdaQueryWrapper<UserAuth>()
                    .eq(UserAuth::getUsername, socialTokenDTO.getUnionId())
                    .eq(UserAuth::getLoginType, socialTokenDTO.getLoginType()));
        }
    
        /**
         * 获取用户信息
         *
         * @param user      用户账号
         * @param ipAddress ip地址
         * @param ipSource  ip源
         * @return {@link UserDetailDTO} 用户信息
         */
        private UserDetailDTO getUserDetail(UserAuth user, String ipAddress, String ipSource) {
            // 更新登录信息
            userAuthMapper.update(new UserAuth(), new LambdaUpdateWrapper<UserAuth>()
                    .set(UserAuth::getLastLoginTime, LocalDateTime.now())
                    .set(UserAuth::getIpAddress, ipAddress)
                    .set(UserAuth::getIpSource, ipSource)
                    .eq(UserAuth::getId, user.getId()));
            // 封装信息
            return userDetailsService.convertUserDetail(user, request);
        }
    
        /**
         * 新增用户信息
         *
         * @param socialToken token信息
         * @param ipAddress   ip地址
         * @param ipSource    ip源
         * @return {@link UserDetailDTO} 用户信息
         */
        private UserDetailDTO saveUserDetail(SocialTokenDTO socialToken, String ipAddress, String ipSource) {
            // 获取第三方用户信息
            SocialUserInfoDTO socialUserInfo = getSocialUserInfo(socialToken);
            // 保存用户信息
            UserInfo userInfo = UserInfo.builder()
                    .nickname(socialUserInfo.getNickname())
                    .avatar(socialUserInfo.getAvatar())
                    .build();
            userInfoMapper.insert(userInfo);
            // 保存账号信息
            UserAuth userAuth = UserAuth.builder()
                    .userInfoId(userInfo.getId())
                    .username(socialToken.getUnionId())
                    .password(socialToken.getAccessToken())
                    .loginType(socialToken.getLoginType())
                    .lastLoginTime(LocalDateTime.now(ZoneId.of(ZoneEnum.SHANGHAI.getZone())))
                    .ipAddress(ipAddress)
                    .ipSource(ipSource)
                    .build();
            userAuthMapper.insert(userAuth);
            // 绑定角色
            UserRole userRole = UserRole.builder()
                    .userId(userInfo.getId())
                    .roleId(RoleEnum.USER.getRoleId())
                    .build();
            userRoleMapper.insert(userRole);
            return userDetailsService.convertUserDetail(userAuth, request);
        }
    }
    

    可以看到抽象策略实现类中实现了login方法,在login方法中调用了两个抽象方法(有没有很眼熟,这不就是模板方法嘛,再看一下模板方法的类图)

    image-20230907190239304

  2. 编写选择器

    public class SocialLoginStrategyContext {
    
        @Autowired
        private Map<String, SocialLoginStrategy> socialLoginStrategyMap;
    
        /**
         * 执行第三方登录策略
         *
         * @param data          数据
         * @param loginTypeEnum 登录枚举类型
         * @return {@link UserInfoDTO} 用户信息
         */
        public UserInfoDTO executeLoginStrategy(String data, LoginTypeEnum loginTypeEnum) {
            return socialLoginStrategyMap.get(loginTypeEnum.getStrategy()).login(data);
        }
    
    }
    

    这里选择器就是用Map存储不同的登录策略,在执行时根据传入的loginType来决定调用。

  3. 策略实现,这里以实现QQ登录为例

    @Service("qqLoginStrategyImpl")
    class QQLoginStrategyImpl extends AbstractSocialLoginStrategyImpl {
        @Autowired
        private QQConfigProperties qqConfigProperties;
        @Autowired
        private RestTemplate restTemplate;
    
        @Override
        public SocialTokenDTO getSocialToken(String data) {
            if (Objects.nonNull((JSON.parseObject(data).get("code")))) {
                QQLoginVO qqLoginVO = JSON.parseObject(data, QQLoginVO.class);
                // 定义请求参数
                Map<String, String> formData = new HashMap<>(1);
                formData.put(CODE, JSON.parseObject(data).getString("code"));
                String result = restTemplate.getForObject(qqConfigProperties.getQqUniqueInfoUrl(), String.class, formData);
                return SocialTokenDTO.builder()
                        .openId(JSON.parseObject(result).get("openid").toString())
                        .unionId(JSON.parseObject(result).get("unionid").toString())
                        .accessToken(JSON.parseObject(result).get("session_key").toString())
                        .platform(qqLoginVO.getPlatform())
                        .nickName(qqLoginVO.getNickname())
                        .avatar(qqLoginVO.getAvatar())
                        .loginType(LoginTypeEnum.QQ.getType())
                        .build();
            } else {
                QQLoginVO qqLoginVO = JSON.parseObject(data, QQLoginVO.class);
                // 校验QQ token信息
                checkQQToken(qqLoginVO);
                // 定义请求参数
                Map<String, String> formData = new HashMap<>(1);
                formData.put(ACCESS_TOKEN, JSON.parseObject(data).getString("accessToken"));
                String result = restTemplate.getForObject(qqConfigProperties.getUniqueInfoUrl(), String.class, formData);
                qqUnionInfoDTO qqUnionInfoDTO = JSON.parseObject(result.substring(9,result.length()-3), qqUnionInfoDTO.class);
                // 返回token信息
                return SocialTokenDTO.builder()
                        .openId(qqLoginVO.getOpenId())
                        .unionId(qqUnionInfoDTO.getUnionid())
                        .accessToken(qqLoginVO.getAccessToken())
                        .appId(qqUnionInfoDTO.getClient_id())
                        .platform(qqLoginVO.getPlatform())
                        .loginType(LoginTypeEnum.QQ.getType())
                        .build();
            }
        }
    
        @Override
        public SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO) {
            if (socialTokenDTO.getPlatform().equals("MP-QQ")) {
                // 返回用户信息
                return SocialUserInfoDTO.builder()
                        .nickname(socialTokenDTO.getNickName())
                        .avatar(socialTokenDTO.getAvatar())
                        .build();
            } else {
                // 定义请求参数
                Map<String, String> formData = new HashMap<>(3);
                formData.put(QQ_OPEN_ID, socialTokenDTO.getOpenId());
                formData.put(ACCESS_TOKEN, socialTokenDTO.getAccessToken());
                formData.put(OAUTH_CONSUMER_KEY, socialTokenDTO.getAppId());
                // 获取QQ返回的用户信息
                QQUserInfoDTO qqUserInfoDTO = JSON.parseObject(restTemplate.getForObject(qqConfigProperties.getUserInfoUrl(), String.class, formData), QQUserInfoDTO.class);
                // 返回用户信息
                return SocialUserInfoDTO.builder()
                        .nickname(Objects.requireNonNull(qqUserInfoDTO).getNickname())
                        .avatar(qqUserInfoDTO.getFigureurl_qq_1())
                        .build();
            }
        }
    
        /**
         * 校验qq token信息
         *
         * @param qqLoginVO qq登录信息
         */
        private void checkQQToken(QQLoginVO qqLoginVO) {
            // 根据token获取qq openId信息
            Map<String, String> qqData = new HashMap<>(1);
            qqData.put(SocialLoginConst.ACCESS_TOKEN, qqLoginVO.getAccessToken());
            try {
                String result = restTemplate.getForObject(qqConfigProperties.getCheckTokenUrl(), String.class, qqData);
                QQTokenDTO qqTokenDTO = JSON.parseObject(CommonUtils.getBracketsContent(Objects.requireNonNull(result)), QQTokenDTO.class);
                // 判断openId是否一致
                if (!qqLoginVO.getOpenId().equals(qqTokenDTO.getOpenid())) {
                    throw new BizException(StatusCodeEnum.QQ_LOGIN_ERROR);
                }
            } catch (Exception e) {
                e.printStackTrace();
                throw new BizException(StatusCodeEnum.QQ_LOGIN_ERROR);
            }
        }
    
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值