策略者模式

很简单的一个定义:抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。

场景

在这之前,你需要看这个文章SPA+.NET Core3.1 GitHub第三方授权登录,了解如何实现第三方授权登录。

我们这里使用策略者模式应用实践,实现第三方授权登录,支持QQ,Gitee,GitHub登录,并且如何把switch case的逻辑判断去掉。

我们先按正常的思路写代码,引用如下类库

  • AspNet.Security.OAuth.Gitee
  • AspNet.Security.OAuth.GitHub
  • AspNet.Security.OAuth.QQ

我们会创建一个Service,这个Service包含了保存Github,QQ,Gitee信息的接口。由于三者之间,数据都是以Claims的情况存到ClaimsPrincipal中,键各不相同,只能独立处理

public  interface IUserIdentityService
{
Task<long> SaveGitHubAsync(ClaimsPrincipal principal, string openId);
Task<long> SaveQQAsync(ClaimsPrincipal principal, string openId);
Task<long> SaveGiteeAsync(ClaimsPrincipal principal, string openId);
}

实现,保存登录后的授权信息,生成账号,并返回生成的用户id,伪代码如下

  public class UserIdentityService :ApplicationService, IUserIdentityService
{ public async Task<long> SaveGitHubAsync(ClaimsPrincipal principal, string openId)
{
return userId;
} public async Task<long> SaveQQAsync(ClaimsPrincipal principal, string openId)
{
return userId;
}
public async Task<long> SaveGiteeAsync(ClaimsPrincipal principal, string openId)
{
return userId;
}
}

这时候我们怎么调用 呢,provider为GitHub,QQ,Gitee这种字符串,登录成功后,会回调到此地址,这时,根据provider选择不同的方法进行保存用户数据

Oauth2Controller


[HttpGet("signin-callback")]
public async Task<IActionResult> Home(string provider, string redirectUrl = "")
{
AuthenticateResult authenticateResult = await _contextAccessor.HttpContext.AuthenticateAsync(provider);
if (!authenticateResult.Succeeded) return Redirect(redirectUrl); var openIdClaim = authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier);
if (openIdClaim == null || string.IsNullOrWhiteSpace(openIdClaim.Value))
return Redirect(redirectUrl); long id = 0;
switch (provider)
{
case LinUserIdentity.GitHub:
id = await _userCommunityService.SaveGitHubAsync(authenticateResult.Principal, openIdClaim.Value);
break; case LinUserIdentity.QQ:
id = await _userCommunityService.SaveQQAsync(authenticateResult.Principal, openIdClaim.Value);
break; case LinUserIdentity.Gitee:
id = await _userCommunityService.SaveGiteeAsync(authenticateResult.Principal, openIdClaim.Value);
break;
default:
_logger.LogError($"未知的privoder:{provider},redirectUrl:{redirectUrl}");
throw new LinCmsException($"未知的privoder:{provider}!");
} //xxx更多参考 https://github.com/luoyunchong/lin-cms-dotnetcore/issues/9
string token =""; return Redirect($"{redirectUrl}#login-result?token={token}");
}

一看上面的代码,也没毛病,原本也没想要再优化,但后来,我想实现账号绑定。比如,我先用QQ登录,退出后,再用gitee登录,这时就是二个账号了。我们可以在QQ登录的情况下,点击绑定账号,实现二者之间的绑定。如下表结构也是支持此功能的。只要他们的create_userid是一个,就是同一个账号。

按上面的思路,绑定也是lin_user_identity表的数据操作,我们还放到IUserIdentityService服务中。这时就带来新的问题,这个接口在膨胀,他的实现类就更膨胀了。

public  interface IUserIdentityService
{
Task<long> SaveGitHubAsync(ClaimsPrincipal principal, string openId);
Task<long> SaveQQAsync(ClaimsPrincipal principal, string openId);
Task<long> SaveGiteeAsync(ClaimsPrincipal principal, string openId); Task<UnifyResponseDto> BindGitHubAsync(ClaimsPrincipal principal, string openId, long userId);
Task<UnifyResponseDto> BindQQAsync(ClaimsPrincipal principal, string openId, long userId);
Task<UnifyResponseDto> BindGiteeAsync(ClaimsPrincipal principal, string openId, long userId);
}

实现类多了一些方法,也能通过私有方法减少一些重复方法,但总感觉这样的设计实在是太挫了。

这样代码中包含了不同的处理逻辑,一看就是违反了职责单一原则。

   public async Task<UnifyResponseDto> BindGitHubAsync(ClaimsPrincipal principal, string openId, long userId)
{
string name = principal.FindFirst(ClaimTypes.Name)?.Value;
return await this.BindAsync(LinUserIdentity.GitHub, name, openId, userId);
} public async Task<UnifyResponseDto> BindQQAsync(ClaimsPrincipal principal, string openId, long userId)
{
string nickname = principal.FindFirst(ClaimTypes.Name)?.Value;
return await this.BindAsync(LinUserIdentity.QQ, nickname, openId, userId);
} public async Task<UnifyResponseDto> BindGiteeAsync(ClaimsPrincipal principal, string openId, long userId)
{
string name = principal.FindFirst(ClaimTypes.Name)?.Value;
return await this.BindAsync(LinUserIdentity.Gitee, name, openId, userId);
} private async Task<UnifyResponseDto> BindAsync(string identityType, string name, string openId, long userId)
{
LinUserIdentity linUserIdentity = await _userIdentityRepository.Where(r => r.IdentityType == identityType && r.Credential == openId).FirstAsync();
if (linUserIdentity == null)
{
var userIdentity = new LinUserIdentity(identityType, name, openId, DateTime.Now);
userIdentity.CreateUserId = userId;
await _userIdentityRepository.InsertAsync(userIdentity);
return UnifyResponseDto.Success("绑定成功");
}
else
{
return UnifyResponseDto.Error("绑定失败,该用户已绑定其他账号");
}
}

第三方账号绑定回调,调用方法如下,非全部代码,

[HttpGet("signin-bind-callback")]
public async Task<IActionResult> SignInBindCallBack(string provider, string redirectUrl = "", string token = "")
{
//更多xxx代码
long userId = 11;
UnifyResponseDto unifyResponseDto;
switch (provider)
{
case LinUserIdentity.GitHub:
unifyResponseDto = await _userCommunityService.BindGitHubAsync(authenticateResult.Principal, openIdClaim.Value, userId);
break;
case LinUserIdentity.QQ:
unifyResponseDto = await _userCommunityService.BindQQAsync(authenticateResult.Principal, openIdClaim.Value, userId);
break;
case LinUserIdentity.Gitee:
unifyResponseDto = await _userCommunityService.BindGiteeAsync(authenticateResult.Principal, openIdClaim.Value, userId);
break;
default:
_logger.LogError($"未知的privoder:{provider},redirectUrl:{redirectUrl}");
unifyResponseDto = UnifyResponseDto.Error($"未知的privoder:{provider}!");
break;
} return Redirect($"{redirectUrl}#bind-result?code={unifyResponseDto.Code.ToString()}&message={HttpUtility.UrlEncode(unifyResponseDto.Message.ToString())}");
}

那么,我们如何优化呢。我们也看下表结构。

表结构

1. 用户表 lin_user

字段 备注 类型
id 主键Id bigint
username 用户名 varchar

2. 用户身份认证登录表 lin_user_identity

字段 备注 类型
id char 主键Id
identity_type varchar 认证类型Password,GitHub、QQ、WeiXin等
identifier varchar 认证者,例如 用户名,手机号,邮件等,
credential varchar 凭证,例如 密码,存OpenId、Id,同一IdentityType的OpenId的值是唯一的
create_user_id bigint 绑定的用户Id
create_time datetime

实体类

  • 用户信息 LinUser
    [Table(Name = "lin_user")]
public class LinUser : FullAduitEntity
{
public LinUser() { } /// <summary>
/// 用户名
/// </summary>
[Column(StringLength = 24)]
public string Username { get; set; } [Navigate("CreateUserId")]
public virtual ICollection<LinUserIdentity> LinUserIdentitys { get; set; } }
  • 用户身份认证登录表 LinUserIdentity
    [Table(Name = "lin_user_identity")]
public class LinUserIdentity : FullAduitEntity<Guid>
{
public const string GitHub = "GitHub";
public const string Password = "Password";
public const string QQ = "QQ";
public const string Gitee = "Gitee";
public const string WeiXin = "WeiXin"; /// <summary>
///认证类型, Password,GitHub、QQ、WeiXin等
/// </summary>
[Column(StringLength = 20)]
public string IdentityType { get; set; } /// <summary>
/// 认证者,例如 用户名,手机号,邮件等,
/// </summary>
[Column(StringLength = 24)]
public string Identifier { get; set; } /// <summary>
/// 凭证,例如 密码,存OpenId、Id,同一IdentityType的OpenId的值是唯一的
/// </summary>
[Column(StringLength = 50)]
public string Credential { get; set; } }

如何将六个方法,拆到不同的类中呢。

  1. 创建一个IOAuth2Service的接口,里面有二个方法,一个将授权登录后的信息保存,另一个是绑定和当前用户绑定。
   public interface IOAuth2Service
{
Task<long> SaveUserAsync(ClaimsPrincipal principal, string openId); Task<UnifyResponseDto> BindAsync(ClaimsPrincipal principal, string identityType, string openId, long userId);
}

然后,分别创建,GiteeOAuth2Service,GithubOAuth2Serivice,QQOAuth2Service

在这之前,因为整体逻辑相似,我们可以提取一个抽象类,在抽象类中写通用 的逻辑,子类只需要 实现SaveUserAsync,具体不同的逻辑了。

   public abstract class OAuthService : IOAuth2Service
{
private readonly IAuditBaseRepository<LinUserIdentity> _userIdentityRepository; public OAuthService(IAuditBaseRepository<LinUserIdentity> userIdentityRepository)
{
_userIdentityRepository = userIdentityRepository;
}
private async Task<UnifyResponseDto> BindAsync(string identityType, string name, string openId, long userId)
{
LinUserIdentity linUserIdentity = await _userIdentityRepository.Where(r => r.IdentityType == identityType && r.Credential == openId).FirstAsync();
if (linUserIdentity == null)
{
var userIdentity = new LinUserIdentity(identityType, name, openId, DateTime.Now);
userIdentity.CreateUserId = userId;
await _userIdentityRepository.InsertAsync(userIdentity);
return UnifyResponseDto.Success("绑定成功");
}
else
{
return UnifyResponseDto.Error("绑定失败,该用户已绑定其他账号");
}
} public abstract Task<long> SaveUserAsync(ClaimsPrincipal principal, string openId); public virtual async Task<UnifyResponseDto> BindAsync(ClaimsPrincipal principal, string identityType, string openId, long userId)
{
string nickname = principal.FindFirst(ClaimTypes.Name)?.Value;
return await this.BindAsync(identityType, nickname, openId, userId);
} }

我们拿Gitee登录为例,

public class GiteeOAuth2Service : OAuthService, IOAuth2Service
{
private readonly IUserRepository _userRepository;
private readonly IAuditBaseRepository<LinUserIdentity> _userIdentityRepository; public GiteeOAuth2Service(IAuditBaseRepository<LinUserIdentity> userIdentityRepository, IUserRepository userRepository) : base(userIdentityRepository)
{
_userIdentityRepository = userIdentityRepository;
_userRepository = userRepository;
}
public override async Task<long> SaveUserAsync(ClaimsPrincipal principal, string openId)
{ LinUserIdentity linUserIdentity = await _userIdentityRepository.Where(r => r.IdentityType == LinUserIdentity.Gitee && r.Credential == openId).FirstAsync(); long userId = 0;
if (linUserIdentity == null)
{
string email = principal.FindFirst(ClaimTypes.Email)?.Value;
string name = principal.FindFirst(ClaimTypes.Name)?.Value;
string nickname = principal.FindFirst(GiteeAuthenticationConstants.Claims.Name)?.Value;
string avatarUrl = principal.FindFirst("urn:gitee:avatar_url")?.Value;
string blogAddress = principal.FindFirst("urn:gitee:blog")?.Value;
string bio = principal.FindFirst("urn:gitee:bio")?.Value;
string htmlUrl = principal.FindFirst("urn:gitee:html_url")?.Value; LinUser user = new LinUser
{
Active = UserActive.Active,
Avatar = avatarUrl,
LastLoginTime = DateTime.Now,
Email = email,
Introduction = bio + htmlUrl,
LinUserGroups = new List<LinUserGroup>()
{
new LinUserGroup()
{
GroupId = LinConsts.Group.User
}
},
Nickname = nickname,
Username = "",
BlogAddress = blogAddress,
LinUserIdentitys = new List<LinUserIdentity>()
{
new LinUserIdentity(LinUserIdentity.Gitee,name,openId,DateTime.Now)
}
};
await _userRepository.InsertAsync(user);
userId = user.Id;
}
else
{
userId = linUserIdentity.CreateUserId;
} return userId;
} }

GitHub 登录,保存用户信息,伪代码。他们在获取用户信息中有些差别。

   public class GithubOAuth2Serivice : OAuthService, IOAuth2Service
{
private readonly IUserRepository _userRepository;
private readonly IAuditBaseRepository<LinUserIdentity> _userIdentityRepository; public GithubOAuth2Serivice(IAuditBaseRepository<LinUserIdentity> userIdentityRepository, IUserRepository userRepository) : base(userIdentityRepository)
{
_userIdentityRepository = userIdentityRepository;
_userRepository = userRepository;
} public override async Task<long> SaveUserAsync(ClaimsPrincipal principal, string openId)
{
return userId;
}
}

依赖注入我们使用Autofac。同一个接口,可以 注入多个实现,通过Named区分。

builder.RegisterType<GithubOAuth2Serivice>().Named<IOAuth2Service>(LinUserIdentity.GitHub).InstancePerLifetimeScope();
builder.RegisterType<GiteeOAuth2Service>().Named<IOAuth2Service>(LinUserIdentity.Gitee).InstancePerLifetimeScope();
builder.RegisterType<QQOAuth2Service>().Named<IOAuth2Service>(LinUserIdentity.QQ).InstancePerLifetimeScope();

注入成功后,如何使用呢。我们通过 IComponentContext得到我们想要的对象。

回调登录保存用户信息,相当于生成一个账号。伪代码。

    public Oauth2Controller(IComponentContext componentContext)
{
_componentContext = componentContext;
} [HttpGet("signin-callback")]
public async Task<IActionResult> Home(string provider, string redirectUrl = "")
{
AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync(provider); IOAuth2Service oAuth2Service = _componentContext.ResolveNamed<IOAuth2Service>(provider);
long id = await oAuth2Service.SaveUserAsync(authenticateResult.Principal, openIdClaim.Value); //...省略生成token的过程
string token = _jsonWebTokenService.Encode(claims); return Redirect($"{redirectUrl}#login-result?token={token}");
}

这里的Provider的值就是 LinUserIdentity.GitHub,一个字符串值。

    public class LinUserIdentity : FullAduitEntity<Guid>
{
public const string GitHub = "GitHub";
public const string QQ = "QQ";
public const string Gitee = "Gitee";
}

源码

接口

抽象类

实现

调用

接口注入

总结

总结来说,我们干掉了switch case,好处是

  • 实现了对扩展开放,对修改关闭,我们不需要修改现有的类,就能新增新的逻辑。
  • 在整体上逻辑更清晰,而不是有一个需求,加一个接口,加一个实现,这样无脑操作。

使用策略者模式减少switch case 语句的更多相关文章

  1. 使用策略模式重构switch case 代码

    目录 1.背景 2.案例 3.switch…case…方式实现 4.switch…case…带来的问题 5.使用策略模式重构switch…case…代码 6.总结 1.背景 之前在看<重构    ...

  2. if语句,if...else if语句和switch...case语句的区别和分析

    前段时间在工作中遇到了一个关于条件判断语句的问题,在if语句,if else if语句和switch case语句这三者之间分析,使用其中最有效率的一种方法. 所以就将这个问题作为自己第一篇博客的主要 ...

  3. 为什么说在使用多条件判断时switch case语句比if语句效率高?

    在学习JavaScript中的if控制语句和switch控制语句的时候,提到了使用多条件判断时switch case语句比if语句效率高,但是身为小白的我并没有在代码中看出有什么不同.去度娘找了半个小 ...

  4. java中的Switch case语句

    java中的Switch case 语句 在Switch语句中有4个关键字:switch,case break,default. 在switch(变量),变量只能是整型或者字符型,程序先读出这个变量的 ...

  5. switch… case 语句的用法

    switch… case 语句的用法   public class Test7 { public static void main(String[] args) { int i=5; switch(i ...

  6. Python | 基础系列 · Python为什么没有switch/case语句?

    与我之前使用的所有语言都不同,Python没有switch/case语句.为了达到这种分支语句的效果,一般方法是使用字典映射: def numbers_to_strings(argument): sw ...

  7. 为什么switch...case语句比if...else执行效率高

    在C语言中,教科书告诉我们switch...case...语句比if...else if...else执行效率要高,但这到底是为什么呢?本文尝试从汇编的角度予以分析并揭晓其中的奥秘. 第一步,写一个d ...

  8. JavaScript基础知识(if、if else、else if、while、switch...case语句)

    13.语句 概念:就是分号(:) 代表一条语句的结束 习惯:一行只编写一条语句:一行编写多条语句(代码可读性较差) 语句块:可以包含多条语句     "{ }"将多条语句包裹 u ...

  9. C语言中switch case语句可变参实现方法(case 参数 空格...空格 参数 :)

    正常情况下,switch case语句是这么写的: : : ... ;break ; default : ... ;break ; } 接下来说一种不常见的,但是对于多参数有很大的帮助的写法: 先给一 ...

随机推荐

  1. git stash回退

    目录 1.起因 2. 问题出现 3.修复 4. 注意 1.起因 这个问题要从今天刚遇到的事儿说起,昨晚代码出了个乌龙事件,本来正在dev分支进行新功能的开发,但是测试出现的问题是在release 分支 ...

  2. delphi xe 10.3 利用Git组群开发,Git服务器安装,Git 拉取,提交,推送相关设置操作

    1. Git服务器安装, 参考 https://blog.csdn.net/u012842630/article/details/97175397 Git服务器官方网站,要FQ. 2. 工具软件 gi ...

  3. http强制缓存、协商缓存、指纹ETag详解

    目录 实操目录及步骤 缓存分类 强制缓存 对比缓存 指纹 Etag 摘要及加密算法 缓存总结 每个浏览器都有一个自己的缓存区,使用缓存区的数据有诸多好处,减少冗余的数据传输,节省网络传输.减少服务器负 ...

  4. excel计数函数COUNTIF、COUNTIFS

    1.单条件计数:COUNTIF(条件区域,指定条件) =COUNTIF($H$2:$H$8,L2)求H2:H8中值等于L2的行记录数,这里求的是游泳项目总共有多少个人参加 2.多条件计数:COUNTI ...

  5. 易车网实战+【保姆级】:Feapder爬虫框架入门教程

    今天辰哥带大家来看看一个爬虫框架:Feapder,看完本文之后,别再说你不会Feapder了.本文辰哥将带你了解什么是Feapder?.如何去创建一个Feapder入门项目(实战:采集易车网轿车数据) ...

  6. 39、wget、curl

    39.1.wget介绍: wget命令用来从指定的URL下载文件.wget非常稳定,它在带宽很窄的情况下和不稳定网络中有很强的适应性,如果是由于网络的原因下载失败, wget会不断的尝试,直到整个文件 ...

  7. 26、linux文件系统

  8. LCA总结

    作为一名合格的 OIer ,一定要有自我总结的意识,一定要通过写博客的方式来验证自己的掌握程度 ----沃.茨基硕德 目录 作为一名合格的 OIer ,一定要有自我总结的意识,一定要通过写博客的方式来 ...

  9. xf浅谈_最短路

    最短路问题(short-path problem):最短路问题是图论研究中的一个经典算法问题,指在寻找图(由结点和路径组成的)中两结点之间的最短路径.算法具体的形式包括: 1.确定起点的最短路径问题 ...

  10. 如何跟领导解释为什么选择SpringCloud alibaba作为微服务开发框架

    什么是微服务 提到微服务不得不提Martin Fowler在2014年3月25日发表的文章 Microservices,里面给出了微服务的定义.后续国内所有关于微服务的介绍都是基于这篇文章的翻译,或加 ...