【.NET Core项目实战-统一认证平台】开篇及目录索引

上篇文章我介绍了如何强制令牌过期的实现,相信大家对IdentityServer4的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方式集成老的业务系统验证,然后根据不同的客户端使用不同的认证方式来集成到统一认证平台。

.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。

一、自定授权源码剖析

当我们需要使用开源项目的某些功能时,最好了解实现的原理,才能正确和熟练使用功能,避免出现各种未知bug问题和出现问题无法解决的被动场面。

在使用此功能前,我们需要了解完整的实现流程,下面我将从源码开始讲解IdentityServer4是如何实现自定义的授权方式。

从我之前的文章中我们知道授权方式是通过Grant_Type的值来判断的,所以我们自定义的授权方式,也是通过此值来区分,所以需要了解自定义的值处理流程。TokenRequestValidator是请求验证的方法,除了常规验证外,还增加了自定义的验证方式。

  1. public async Task<TokenRequestValidationResult> ValidateRequestAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult)
  2. {
  3. _logger.LogDebug("Start token request validation");
  4. _validatedRequest = new ValidatedTokenRequest
  5. {
  6. Raw = parameters ?? throw new ArgumentNullException(nameof(parameters)),
  7. Options = _options
  8. };
  9. if (clientValidationResult == null) throw new ArgumentNullException(nameof(clientValidationResult));
  10. _validatedRequest.SetClient(clientValidationResult.Client, clientValidationResult.Secret, clientValidationResult.Confirmation);
  11. /////////////////////////////////////////////
  12. // check client protocol type
  13. /////////////////////////////////////////////
  14. if (_validatedRequest.Client.ProtocolType != IdentityServerConstants.ProtocolTypes.OpenIdConnect)
  15. {
  16. LogError("Client {clientId} has invalid protocol type for token endpoint: expected {expectedProtocolType} but found {protocolType}",
  17. _validatedRequest.Client.ClientId,
  18. IdentityServerConstants.ProtocolTypes.OpenIdConnect,
  19. _validatedRequest.Client.ProtocolType);
  20. return Invalid(OidcConstants.TokenErrors.InvalidClient);
  21. }
  22. /////////////////////////////////////////////
  23. // check grant type
  24. /////////////////////////////////////////////
  25. var grantType = parameters.Get(OidcConstants.TokenRequest.GrantType);
  26. if (grantType.IsMissing())
  27. {
  28. LogError("Grant type is missing");
  29. return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
  30. }
  31. if (grantType.Length > _options.InputLengthRestrictions.GrantType)
  32. {
  33. LogError("Grant type is too long");
  34. return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
  35. }
  36. _validatedRequest.GrantType = grantType;
  37. switch (grantType)
  38. {
  39. case OidcConstants.GrantTypes.AuthorizationCode:
  40. return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters);
  41. case OidcConstants.GrantTypes.ClientCredentials:
  42. return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters);
  43. case OidcConstants.GrantTypes.Password:
  44. return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters);
  45. case OidcConstants.GrantTypes.RefreshToken:
  46. return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters);
  47. default://统一的自定义的验证方式
  48. return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters);
  49. }
  50. }

从上面代码可以看出,除了内置的授权方式,其他的都是用ValidateExtensionGrantRequestAsync来进行验证,详细的验证规则继续分析实现过程。

  1. private async Task<TokenRequestValidationResult> ValidateExtensionGrantRequestAsync(NameValueCollection parameters)
  2. {
  3. _logger.LogDebug("Start validation of custom grant token request");
  4. /////////////////////////////////////////////
  5. // 校验客户端是否开启了此授权方式
  6. /////////////////////////////////////////////
  7. if (!_validatedRequest.Client.AllowedGrantTypes.Contains(_validatedRequest.GrantType))
  8. {
  9. LogError("{clientId} does not have the custom grant type in the allowed list, therefore requested grant is not allowed", _validatedRequest.Client.ClientId);
  10. return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
  11. }
  12. /////////////////////////////////////////////
  13. // 判断是否注入了此自定义的授权实现
  14. /////////////////////////////////////////////
  15. if (!_extensionGrantValidator.GetAvailableGrantTypes().Contains(_validatedRequest.GrantType, StringComparer.Ordinal))
  16. {
  17. LogError("No validator is registered for the grant type: {grantType}", _validatedRequest.GrantType);
  18. return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType);
  19. }
  20. /////////////////////////////////////////////
  21. // 校验是否支持scope
  22. /////////////////////////////////////////////
  23. if (!await ValidateRequestedScopesAsync(parameters))
  24. {
  25. return Invalid(OidcConstants.TokenErrors.InvalidScope);
  26. }
  27. /////////////////////////////////////////////
  28. // 调用自定义的验证实现方法
  29. /////////////////////////////////////////////
  30. var result = await _extensionGrantValidator.ValidateAsync(_validatedRequest);
  31. if (result == null)
  32. {
  33. LogError("Invalid extension grant");
  34. return Invalid(OidcConstants.TokenErrors.InvalidGrant);
  35. }
  36. if (result.IsError)
  37. {
  38. if (result.Error.IsPresent())
  39. {
  40. LogError("Invalid extension grant: {error}", result.Error);
  41. return Invalid(result.Error, result.ErrorDescription, result.CustomResponse);
  42. }
  43. else
  44. {
  45. LogError("Invalid extension grant");
  46. return Invalid(OidcConstants.TokenErrors.InvalidGrant, customResponse: result.CustomResponse);
  47. }
  48. }
  49. if (result.Subject != null)
  50. {
  51. /////////////////////////////////////////////
  52. // 判断当前的用户是否可用
  53. /////////////////////////////////////////////
  54. var isActiveCtx = new IsActiveContext(
  55. result.Subject,
  56. _validatedRequest.Client,
  57. IdentityServerConstants.ProfileIsActiveCallers.ExtensionGrantValidation);
  58. await _profile.IsActiveAsync(isActiveCtx);
  59. if (isActiveCtx.IsActive == false)
  60. {
  61. // todo: raise event?
  62. LogError("User has been disabled: {subjectId}", result.Subject.GetSubjectId());
  63. return Invalid(OidcConstants.TokenErrors.InvalidGrant);
  64. }
  65. _validatedRequest.Subject = result.Subject;
  66. }
  67. _logger.LogDebug("Validation of extension grant token request success");
  68. return Valid(result.CustomResponse);
  69. }

从代码中可以看出,实现流程如下:

  • 1、客户端是否配置了自定义的授权方式。
  • 2、是否注入了自定义的授权实现。
  • 3、授权的scope客户端是否有权限。
  • 4、使用自定义的授权验证方式校验请求数据是否合法。
  • 5、判断是否有有效数据信息,可自行实现接口。

从源码中,可以发现流程已经非常清晰了,核心类ExtensionGrantValidator实现了自定义授权的校验过程,进一步分析下此类的代码实现。

  1. using IdentityServer4.Models;
  2. using Microsoft.Extensions.Logging;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Threading.Tasks;
  7. namespace IdentityServer4.Validation
  8. {
  9. /// <summary>
  10. /// Validates an extension grant request using the registered validators
  11. /// </summary>
  12. public class ExtensionGrantValidator
  13. {
  14. private readonly ILogger _logger;
  15. private readonly IEnumerable<IExtensionGrantValidator> _validators;
  16. /// <summary>
  17. /// Initializes a new instance of the <see cref="ExtensionGrantValidator"/> class.
  18. /// </summary>
  19. /// <param name="validators">The validators.</param>
  20. /// <param name="logger">The logger.</param>
  21. public ExtensionGrantValidator(IEnumerable<IExtensionGrantValidator> validators, ILogger<ExtensionGrantValidator> logger)
  22. {
  23. if (validators == null)
  24. {
  25. _validators = Enumerable.Empty<IExtensionGrantValidator>();
  26. }
  27. else
  28. {
  29. _validators = validators;
  30. }
  31. _logger = logger;
  32. }
  33. /// <summary>
  34. /// Gets the available grant types.
  35. /// </summary>
  36. /// <returns></returns>
  37. public IEnumerable<string> GetAvailableGrantTypes()
  38. {
  39. return _validators.Select(v => v.GrantType);
  40. }
  41. /// <summary>
  42. /// Validates the request.
  43. /// </summary>
  44. /// <param name="request">The request.</param>
  45. /// <returns></returns>
  46. public async Task<GrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
  47. {
  48. var validator = _validators.FirstOrDefault(v => v.GrantType.Equals(request.GrantType, StringComparison.Ordinal));
  49. if (validator == null)
  50. {
  51. _logger.LogError("No validator found for grant type");
  52. return new GrantValidationResult(TokenRequestErrors.UnsupportedGrantType);
  53. }
  54. try
  55. {
  56. _logger.LogTrace("Calling into custom grant validator: {type}", validator.GetType().FullName);
  57. var context = new ExtensionGrantValidationContext
  58. {
  59. Request = request
  60. };
  61. await validator.ValidateAsync(context);
  62. return context.Result;
  63. }
  64. catch (Exception e)
  65. {
  66. _logger.LogError(1, e, "Grant validation error: {message}", e.Message);
  67. return new GrantValidationResult(TokenRequestErrors.InvalidGrant);
  68. }
  69. }
  70. }
  71. }

从上面代码可以发现,自定义授权方式,只需要实现IExtensionGrantValidator接口即可,然后支持多个自定义授权方式的共同使用。

到此整个验证过程解析完毕了,然后再查看下生成Token流程,实现方法为TokenResponseGenerator,这个方法并不陌生,前几篇介绍不同的授权方式都介绍了,所以直接看实现代码。

  1. public virtual async Task<TokenResponse> ProcessAsync(TokenRequestValidationResult request)
  2. {
  3. switch (request.ValidatedRequest.GrantType)
  4. {
  5. case OidcConstants.GrantTypes.ClientCredentials:
  6. return await ProcessClientCredentialsRequestAsync(request);
  7. case OidcConstants.GrantTypes.Password:
  8. return await ProcessPasswordRequestAsync(request);
  9. case OidcConstants.GrantTypes.AuthorizationCode:
  10. return await ProcessAuthorizationCodeRequestAsync(request);
  11. case OidcConstants.GrantTypes.RefreshToken:
  12. return await ProcessRefreshTokenRequestAsync(request);
  13. default://自定义授权生成Token的方式
  14. return await ProcessExtensionGrantRequestAsync(request);
  15. }
  16. }
  17. protected virtual Task<TokenResponse> ProcessExtensionGrantRequestAsync(TokenRequestValidationResult request)
  18. {
  19. Logger.LogTrace("Creating response for extension grant request");
  20. return ProcessTokenRequestAsync(request);
  21. }

实现的代码方式和客户端模式及密码模式一样,这里就不多介绍了。

最后我们查看下是如何注入IExtensionGrantValidator,是否对外提供接入方式,发现IdentityServer4提供了AddExtensionGrantValidator扩展方法,我们自己实现自定义授权后添加即可,详细实现代码如下。

  1. public static IIdentityServerBuilder AddExtensionGrantValidator<T>(this IIdentityServerBuilder builder)
  2. where T : class, IExtensionGrantValidator
  3. {
  4. builder.Services.AddTransient<IExtensionGrantValidator, T>();
  5. return builder;
  6. }

二、自定义授权实现

现在开始开发第一个自定义授权方式,GrantType定义为CzarCustomUser,然后实现IExtensionGrantValidator接口,为了演示方便,我新建一个测试用户表,用来模拟老系统的登录方式。

  1. Create Table CzarCustomUser
  2. (
  3. iid int identity,
  4. username varchar(50),
  5. usertruename varchar(50),
  6. userpwd varchar(100)
  7. )
  8. --插入测试用户密码信息,测试数据密码不加密
  9. insert into CzarCustomUser values('jinyancao','金焰的世界','777777')

然后把实现验证的方法,由于代码太简单,我就直接贴代码如下。

  1. namespace Czar.AuthPlatform.Web.Application.IRepository
  2. {
  3. public interface ICzarCustomUserRepository
  4. {
  5. /// <summary>
  6. /// 根据账号密码获取用户实体
  7. /// </summary>
  8. /// <param name="uaccount">账号</param>
  9. /// <param name="upassword">密码</param>
  10. /// <returns></returns>
  11. CzarCustomUser FindUserByuAccount(string uaccount, string upassword);
  12. }
  13. }
  14. namespace Czar.AuthPlatform.Web.Application.Repository
  15. {
  16. public class CzarCustomUserRepository : ICzarCustomUserRepository
  17. {
  18. private readonly string DbConn = "";
  19. public CzarCustomUserRepository(IOptions<CzarConfig> czarConfig)
  20. {
  21. DbConn = czarConfig.Value.DbConnectionStrings;
  22. }
  23. /// <summary>
  24. /// 根据账号密码获取用户实体
  25. /// </summary>
  26. /// <param name="uaccount">账号</param>
  27. /// <param name="upassword">密码</param>
  28. /// <returns></returns>
  29. public CzarCustomUser FindUserByuAccount(string uaccount, string upassword)
  30. {
  31. using (var connection = new SqlConnection(DbConn))
  32. {
  33. string sql = @"SELECT * from CzarCustomUser where username=@uaccount and userpwd=upassword ";
  34. var result = connection.QueryFirstOrDefault<CzarCustomUser>(sql, new { uaccount, upassword });
  35. return result;
  36. }
  37. }
  38. }
  39. }
  40. namespace Czar.AuthPlatform.Web.Application.IServices
  41. {
  42. public interface ICzarCustomUserServices
  43. {
  44. /// <summary>
  45. /// 根据账号密码获取用户实体
  46. /// </summary>
  47. /// <param name="uaccount">账号</param>
  48. /// <param name="upassword">密码</param>
  49. /// <returns></returns>
  50. CzarCustomUser FindUserByuAccount(string uaccount, string upassword);
  51. }
  52. }
  53. namespace Czar.AuthPlatform.Web.Application.Services
  54. {
  55. public class CzarCustomUserServices: ICzarCustomUserServices
  56. {
  57. private readonly ICzarCustomUserRepository czarCustomUserRepository;
  58. public CzarCustomUserServices(ICzarCustomUserRepository czarCustomUserRepository)
  59. {
  60. this.czarCustomUserRepository = czarCustomUserRepository;
  61. }
  62. /// <summary>
  63. /// 根据账号密码获取用户实体
  64. /// </summary>
  65. /// <param name="uaccount">账号</param>
  66. /// <param name="upassword">密码</param>
  67. /// <returns></returns>
  68. public CzarCustomUser FindUserByuAccount(string uaccount, string upassword)
  69. {
  70. return czarCustomUserRepository.FindUserByuAccount(uaccount, upassword);
  71. }
  72. }
  73. }

现在可以定义自定义的授权类型了,我起名为CzarCustomUserGrantValidator,实现代码如下。

  1. using Czar.AuthPlatform.Web.Application.IServices;
  2. using IdentityServer4.Models;
  3. using IdentityServer4.Validation;
  4. using System.Threading.Tasks;
  5. namespace Czar.AuthPlatform.Web.Application.Ids4
  6. {
  7. /// <summary>
  8. /// 金焰的世界
  9. /// 2019-01-28
  10. /// 自定义用户授权
  11. /// </summary>
  12. public class CzarCustomUserGrantValidator : IExtensionGrantValidator
  13. {
  14. public string GrantType => "CzarCustomUser";
  15. private readonly ICzarCustomUserServices czarCustomUserServices;
  16. public CzarCustomUserGrantValidator(ICzarCustomUserServices czarCustomUserServices)
  17. {
  18. this.czarCustomUserServices = czarCustomUserServices;
  19. }
  20. public Task ValidateAsync(ExtensionGrantValidationContext context)
  21. {
  22. var userName = context.Request.Raw.Get("czar_name");
  23. var userPassword = context.Request.Raw.Get("czar_password");
  24. if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(userPassword))
  25. {
  26. context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
  27. }
  28. //校验登录
  29. var result = czarCustomUserServices.FindUserByuAccount(userName, userPassword);
  30. if (result==null)
  31. {
  32. context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
  33. }
  34. //添加指定的claims
  35. context.Result = new GrantValidationResult(
  36. subject: result.iid.ToString(),
  37. authenticationMethod: GrantType,
  38. claims: result.Claims);
  39. return Task.CompletedTask;
  40. }
  41. }
  42. }

这就实现了自定义授权的功能,是不是很简单呢?然后添加此扩展方法。

  1. services.AddIdentityServer(option =>
  2. {
  3. option.PublicOrigin = Configuration["CzarConfig:PublicOrigin"];
  4. })
  5. .AddDeveloperSigningCredential()
  6. .AddDapperStore(option =>
  7. {
  8. option.DbConnectionStrings = Configuration["CzarConfig:DbConnectionStrings"];
  9. })
  10. .AddResourceOwnerValidator<CzarResourceOwnerPasswordValidator>()
  11. .AddProfileService<CzarProfileService>()
  12. .AddSecretValidator<JwtSecretValidator>()
  13. //添加自定义授权
  14. .AddExtensionGrantValidator<CzarCustomUserGrantValidator>();

现在是不是就可以使用自定义授权的方式了呢?打开PostMan测试,按照源码解析和设计参数,测试信息如下,发现报错,原来是还未配置好客户端访问权限,开启权限测试如下。

三、客户端权限配置

在使用IdentityServer4时我们一定要理解整个验证流程。根据这次配置,我再梳理下流程如下:

  • 1、校验客户端client_id和Client_Secret。
  • 2、校验客户端是否有当前的授权方式。
  • 3、校验是否有请求scope权限。
  • 4、如果非客户端验证,校验账号密码或自定义规则是否正确。
  • 5、非客户端验证,校验授权信息是否有效。

通过此流程会发现我们缺少授权方式配置,所以请求时提示上面的提示,既然知道原因了,那就很简单的来实现,添加客户端自定义授权模式。此信息是在ClientGrantTypes表中,字段为客户端ID和授权方式。我测试的客户端ID为21,授权方式为CzarCustomUser,那直接使用SQL语句插入关系,然后再测试。

  1. INSERT INTO ClientGrantTypes VALUES(21,'CzarCustomUser');

发现可以获取到预期结果,然后查看access_token是什么内容,显示如下。

显示的信息和我们定义的信息相同,而且可以通过amr来区分授权类型,不同的业务系统使用不同的认证方式,然后统一集成到认证平台即可。

四、总结与思考

本篇我介绍了自定义授权方式,从源码解析到最后的实现详细讲解了实现原理,并使用测试的用户来实现自定义的认证流程,本篇涉及的知识点不多,但是非常重要,因为我们在使用统一身份认证时经常会遇到多种认证方式的结合,和多套不同应用用户的使用,在掌握了授权原理后,就能在不同的授权方式中切换的游刃有余。

思考下,有了这些知识后,关于短信验证码登录和扫码登录是不是有心理有底了呢?如果自己实现这类登录应该都知道从哪里下手了吧。

下篇我将介绍常用登录的短信验证码授权方式,尽情期待吧。

【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式的更多相关文章

  1. 【.NET Core项目实战-统一认证平台】第四章 网关篇-数据库存储配置(2)

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了如何扩展Ocelot网关,并实现数据库存储,然后测试了网关的路由功能,一切都是那么顺利,但是有一个问题未解决,就是如果网关 ...

  2. 【.NET Core项目实战-统一认证平台】第七章 网关篇-自定义客户端限流

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上增加自定义客户端授权功能,从设计到编码实现,一步一步详细讲解,相信大家也掌握了自定义中间件的开发技巧了,本篇我们 ...

  3. 【.NET Core项目实战-统一认证平台】第五章 网关篇-自定义缓存Redis

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了2种网关配置信息更新的方法和扩展Mysql存储,本篇我们将介绍如何使用Redis来实现网关的所有缓存功能,用到的文档及源码 ...

  4. 【.NET Core项目实战-统一认证平台】第六章 网关篇-自定义客户端授权

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了网关使用Redis进行缓存,并介绍了如何进行缓存实现,缓存信息清理接口的使用.本篇我们将介绍如何实现网关自定义客户端授权, ...

  5. 【.NET Core项目实战-统一认证平台】第三章 网关篇-数据库存储配置(1)

    [.NET Core项目实战-统一认证平台]开篇及目录索引 本篇将介绍如何扩展Ocelot中间件实现自定义网关,并使用2种不同数据库来演示Ocelot配置信息存储和动态更新功能,内容也是从实际设计出发 ...

  6. 【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上一篇我介绍了JWT的生成验证及流程内容,相信大家也对JWT非常熟悉了,今天将从一个小众的需求出发,介绍如何强制令牌过期的思路和实现过程. ...

  7. 【.NET Core项目实战-统一认证平台】第十一章 授权篇-密码授权模式

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了基于Ids4客户端授权的原理及如何实现自定义的客户端授权,并配合网关实现了统一的授权异常返回值和权限配置等相关功能,本篇将介绍 ...

  8. 【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了如何使用Dapper持久化IdentityServer4(以下简称ids4)的信息,并实现了sqlserver和mysql两种 ...

  9. 【.NET Core项目实战-统一认证平台】第十六章 网关篇-Ocelot集成RPC服务

    [.NET Core项目实战-统一认证平台]开篇及目录索引 一.什么是RPC RPC是"远程调用(Remote Procedure Call)"的一个名称的缩写,并不是任何规范化的 ...

  10. 【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能

    [.NET Core项目实战-统一认证平台]开篇及目录索引 一.背景 首先说声抱歉,可能是因为假期综合症(其实就是因为懒哈)的原因,已经很长时间没更新博客了,现在也调整的差不多了,准备还是以每周1-2 ...

随机推荐

  1. jenkins安装配置

    一.下载Jenkins 官网地址:https://jenkins.io/,图如下所示,点击下载可下载最新版本. 点击下载之后,我们可以看到下面的图,我这边选择的Jenkins.war 文件. 下面,使 ...

  2. Kubernetes中的RBAC

    Kubernetes中,授权有ABAC(基于属性的访问控制).RBAC(基于角色的访问控制).Webhook.Node.AlwaysDeny(一直拒绝)和AlwaysAllow(一直允许)这6种模式. ...

  3. oracle基础语句练习

    1. 创建相关表结构 Emp----员工信息表 Ename ), --姓名 Empno ), --编号 Deptno ), --所在部门 Job ), --工种(人员类别),如:manager 经理, ...

  4. [LeetCode] Buddy Strings 伙计字符串

    Given two strings A and B of lowercase letters, return true if and only if we can swap two letters i ...

  5. dedecms 后台 菜单点击后打开的慢

    原因之一: 加载后台信息的时候  耗费的时间太长. 如果不关注这边的数据, 可以将他们删除掉. 删除  “信息统计”   : 找到   www.yoursite.com/dede/js/indexbo ...

  6. vue 路由跳转,传参

    一.直接跳转 //js1.this.$router.push('/ad_new') //html 2.<router-link to="/ad_check"> < ...

  7. Shell 脚本处理用户输入

    传递参数 跟踪参数 移动变量 处理选项 将选项标准化 获得用户的输入 bash shell提供了一些不同的方法来从用户处获取数据,包括命令行参数(添加在命令后数据),命令行选项(可以修改命令行为的单个 ...

  8. 开启IIS Express可以调试X64项目

    //来源:https://blog.csdn.net/ma_jiang/article/details/52679431 现在项目开发时总有时需要在X64下开发,这样我们就需要IIS Express中 ...

  9. gc笔记(转)

    GC,即就是Java垃圾回收机制.目前主流的JVM(HotSpot)采用的是分代收集算法.与C++不同的是,Java采用的是类似于树形结构的可达性分析法来判断对象是否还存在引用.即:从gcroot开始 ...

  10. mui项目实时更新

    var wgtVer=null; function plusReady(){ // ...... // 获取本地应用资源版本号 plus.runtime.getProperty(plus.runtim ...