首页 > 其他分享 >Abp vNext单点登录

Abp vNext单点登录

时间:2023-08-09 15:55:06浏览次数:24  
标签:vNext 单点 string Abp token IsNullOrWhiteSpace var false httpContext

Abp vNext单点登录

使用Abp vNext 6.0

分析

Abp vNext说OpenIddict是支持单点登录的,不过我找不到相关内容

OpenIddict module provides an integration with the OpenIddict which provides advanced authentication features like single sign-on, single log-out, and API access control. This module persists applications, scopes, and other OpenIddict-related objects to the database.

而且以abp之前IdentityServer4的神奇操作来说

  • /connect/revocation这个接口只能把refresh_token过期
  • /connect/token刷新refresh_token,原先的access_token并没有失效
  • 未使用refresh_token就重新登录去获取access_tokenrefresh_token,那么原先的refresh_token仍然有效

而abp在OpenIddict里,删掉了之前的/connect/revocation,虽然有/connect/revocat这个路由,但是好像没实现
/connect/token也默认不给refresh_token,需要scope:offline_access这个参数才能获取到,毕竟这个刷新其实跟重新登录没啥区别,不过能减少密码输入次数,稍微安全点

所以还是自己实现比较靠谱
方法其实挺简单的,用中间件和Redis就够了

  • 登录时,生成完token,在中间件的响应处理中把token添加到redis
  • 请求时,判断redis里没有对应的token就返回
  • 刷新token时,替换掉redis里的token就可以了

说实话,我是看不懂abp这个ICurrentUser怎么来的,源码都翻不到,但是看起来是解析jwt的,header和payload是不需要密钥的,而且用abp的demo确实可以把access_token拿去解析出用户数据

实现

既然写了中间件,那就先加个配置SinglePointLoginMiddlewareOption

public class SinglePointLoginMiddlewareOption
{
    /// <summary>
    /// 获取Token路由
    /// </summary>
    public string TokenUrl { get; set; }

    /// <summary>
    /// 是否全局验证
    /// </summary>
    public bool IsGlobalAuthorize { get; set; }

    /// <summary>
    /// redis中key的前缀
    /// </summary>
    public string RedisKeyPrefix { get; set; }

    public SinglePointLoginMiddlewareOption()
    {
        this.TokenUrl = "/connect/token";
        this.IsGlobalAuthorize = false;
        this.RedisKeyPrefix = "Token_UserId_";
    }
}

加个扩展SinglePointLoginMiddlewareExtensions

public static class SinglePointLoginMiddlewareExtensions
{
    /// <summary>
    /// 使用单点登录
    /// </summary>
    /// <param name="builder"></param>
    /// <returns></returns>
    public static IApplicationBuilder UseSinglePointLogin(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SinglePointLoginMiddleware>();
    }
}

再然后是中间件

public class SinglePointLoginMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IDistributedCache _distributedCache;
    private readonly SinglePointLoginMiddlewareOption _option;

    public SinglePointLoginMiddleware(RequestDelegate next, IDistributedCache distributedCache, IOptions<SinglePointLoginMiddlewareOption> options)
    {
        this._next = next;
        this._distributedCache = distributedCache;
        this._option = options.Value;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        //需要读取响应,所以需要替换
        var originBodyStream = httpContext.Response.Body;
        using (var responseBodyStream = new MemoryStream())
        {
            try
            {
                httpContext.Response.Body = responseBodyStream;

                bool isAllowAnonymous = false;
                if (true == this._option.IsGlobalAuthorize)
                {
                    //全局验证,AllowAnonymous不需要验证
                    var allowAnonymousAttribute = HttpContextHelper.GetAttribute<AllowAnonymousAttribute>(httpContext);

                    if (null != allowAnonymousAttribute)
                    {
                        isAllowAnonymous = true;
                    }
                }
                else
                {
                    //非全局验证,默认不需要验证
                    var authorizeAttribute = HttpContextHelper.GetAttribute<AuthorizeAttribute>(httpContext);

                    if (null == authorizeAttribute)
                    {
                        isAllowAnonymous = true;
                    }
                }

                if (false == isAllowAnonymous)
                {
                    //目标控制器函数没有匿名属性
                    string userId = string.Empty;
                    string jwtToken = string.Empty;

                    //获取token
                    jwtToken = HttpContextHelper.GetJwtToken(httpContext);
                    if (true == string.IsNullOrWhiteSpace(jwtToken))
                    {
                        //没有传递token
                        var response_401 = httpContext.Response;
                        response_401.StatusCode = 401;

                        return;
                    }

                    //获取用户Id
                    JwtPayload payload = JwtHelper.GetPayload(jwtToken);
                    if (null != payload)
                    {
                        if (true == payload.TryGetValue("sub", out object userIdObj) && null != userIdObj)
                        {
                            string userIdStr = userIdObj.ToString();
                            if (false == string.IsNullOrWhiteSpace(userIdStr))
                            {
                                userId = userIdStr;
                            }
                        }
                    }
                    if (true == string.IsNullOrWhiteSpace(userId))
                    {
                        //payload中没有userId数据
                        var response_401 = httpContext.Response;
                        response_401.StatusCode = 401;

                        return;
                    }

                    //从redis获取token
                    string redisTokenKey = $"{this._option.RedisKeyPrefix}{userId}";
                    string redisToken = this._distributedCache.GetString(redisTokenKey);
                    if (true == string.IsNullOrWhiteSpace(redisToken) || redisToken != jwtToken)
                    {
                        //单点登录,本地token必须与redis里的相同
                        var response_401 = httpContext.Response;
                        response_401.StatusCode = 401;

                        return;
                    }
                }
                await this._next(httpContext);
                await this.ResponseHandler(httpContext);
            }
            finally
            {
                //重置响应
                responseBodyStream.Seek(0, SeekOrigin.Begin);
                await responseBodyStream.CopyToAsync(originBodyStream);
                httpContext.Response.Body = originBodyStream;//这一步要不要都可以
            }
        }
    }

    /// <summary>
    /// 响应处理
    /// 将token放到redis
    /// </summary>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    private async Task<bool> ResponseHandler(HttpContext httpContext)
    {
        var request = httpContext.Request;
        var response = httpContext.Response;
        string path = request.Path;
        int statusCode = response.StatusCode;
        string token = string.Empty;
        if (this._option.TokenUrl == path && 200 == statusCode)
        {
            string bodyStr = await HttpContextHelper.GetResponseBodyStr(httpContext);
            if (false == string.IsNullOrWhiteSpace(bodyStr))
            {
                var bodyDic = JsonConvert.DeserializeObject<Dictionary<string, object>>(bodyStr);
                if (bodyDic != null)
                {
                    if (true == bodyDic.TryGetValue("access_token", out object accessTokenObj) && null != accessTokenObj)
                    {
                        string accessTokenStr = accessTokenObj.ToString();
                        if (false == string.IsNullOrWhiteSpace(accessTokenStr))
                        {
                            token = accessTokenStr;
                        }
                    }
                }
            }
        }

        if (false == string.IsNullOrWhiteSpace(token))
        {
            //获取userId
            JwtPayload payload = JwtHelper.GetPayload(token);
            string userId = string.Empty;
            long exp = 0;
            if (null != payload)
            {
                if (true == payload.TryGetValue("sub", out object userIdObj) && null != userIdObj)
                {
                    string userIdStr = userIdObj.ToString();
                    if (false == string.IsNullOrWhiteSpace(userIdStr))
                    {
                        userId = userIdStr;
                    }
                }

                if (true == payload.TryGetValue("exp", out object expObj) && null != expObj)
                {
                    string expStr = expObj.ToString();
                    if (false == string.IsNullOrWhiteSpace(expStr))
                    {
                        exp = long.Parse(expStr);
                    }
                }

            }

            //保存到redis
            if (false == string.IsNullOrWhiteSpace(userId) && 0 != exp)
            {
                userId = $"{this._option.RedisKeyPrefix}{userId}";
                DistributedCacheEntryOptions options = new DistributedCacheEntryOptions();
                options.AbsoluteExpiration = DateTimeOffset.FromUnixTimeSeconds(exp);
                this._distributedCache.SetString(userId, token, options);
                return true;
            }
        }

        return false;
    }
}

因为abp登录和刷新token都是同一个路由,所以步骤还更简单点,就是判断目标函数是不是匿名的,abp的源码默认是全部匿名,Authorize属性才验证token

  • 非匿名函数,我们从token中获取userId,再去redis中取出来
  • 匿名函数,我们就继续执行,到响应的时候再判断目标函数的路由属性,路由正确再把token存到redis中

因为刷新token的路由和获取token的路由是同一个,所以刷新token也会重置redis里的token
思路不算复杂,有个步骤是比较麻烦的,在中间件里的响应数据是不能读取的,需要走点弯路

最后在UseConfiguredEndpoints()前调用UseSinglePointLogin()就可以了
还有这个配置的依赖注入

//配置单点登录
Configure<SinglePointLoginMiddlewareOption>(options =>
{
    options.TokenUrl = "/connect/token";
    options.IsGlobalAuthorize = false;
    options.RedisKeyPrefix = "Token_UserId_";
});

还有两个工具类

JwtHelper操作token

public class JwtHelper
{
    /// <summary>
    /// 从jwtToken中获取Header
    /// </summary>
    /// <param name="jwtToken"></param>
    /// <returns></returns>
    public static JwtHeader GetHeader(string jwtToken)
    {
        var handler = new JwtSecurityTokenHandler();
        var jwt = handler.ReadJwtToken(jwtToken);
        var header = jwt.Header;

        return header;
    }

    /// <summary>
    /// 从jwtToken中获取Payload
    /// </summary>
    /// <param name="jwtToken"></param>
    /// <returns></returns>
    public static JwtPayload GetPayload(string jwtToken)
    {
        var handler = new JwtSecurityTokenHandler();
        var jwt = handler.ReadJwtToken(jwtToken);
        var payload = jwt.Payload;

        return payload;
    }
}

HttpContextHelper操作中间件的HttpContext

public class HttpContextHelper
{
    /// <summary>
    /// 从HttpContext中获取jwtToken
    /// </summary>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    public static string GetJwtToken(HttpContext httpContext)
    {
        string requestToken = string.Empty;
        string jwtToken = string.Empty;

        var request = httpContext.Request;
        var header = request.Headers;
        if (null != header)
        {
            if (true == header.TryGetValue("Authorization", out StringValues authorizationStr))
            {
                requestToken = authorizationStr;
            }
        }

        if (false == string.IsNullOrWhiteSpace(requestToken))
        {
            jwtToken = requestToken.Replace("Bearer ", string.Empty);
        }

        return jwtToken;
    }

    /// <summary>
    /// 从请求的Controller中获取Attribute
    /// </summary>
    /// <typeparam name="TAttribute"></typeparam>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    public static TAttribute GetAttribute<TAttribute>(HttpContext httpContext) where TAttribute : class
    {
        var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
        var attribute = endpoint?.Metadata.GetMetadata<TAttribute>();

        return attribute;
    }

    /// <summary>
    /// 获取响应数据,需要先将响应数据转换成MemoryStream
    /// </summary>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    public static async Task<string> GetResponseBodyStr(HttpContext httpContext)
    {
        var response = httpContext.Response;
        var body = response.Body;

        body.Seek(0, SeekOrigin.Begin);
        var streamReader = new StreamReader(body);
        var bodyStr = await streamReader.ReadToEndAsync();
        body.Seek(0, SeekOrigin.Begin);

        return bodyStr;
    }
}

刷新token

上面的代码虽然也适用于刷新token,但是刷新token本身就有缺陷
因为刷新的一段时间内可能还有多个同样token的请求,如果此时替换掉redis的token会出大问题,需要给旧的token一点时间和新的token同时存在
虽然思路是这样,但是操作起来问题不少,可以明确的是,肯定是替换token前的请求有问题,即旧token

  • 如果用新旧两个key保存token的方案,新token覆盖时,此时单点登录验证没取到旧token而取到新token,那就401了;如果不覆盖,只设置过期时间,似乎就没有这个问题,但是这个操作逻辑在两个token同时存在时执行有问题,即短时间内重复调用登录和刷新token,因为这样就执行了覆盖操作,大概率不是一般用户,可以不管;如果要处理这个问题,可以动态生成key,因为我们是验证本地token,所以这个key可以加个时间戳之类的,这样就没问题了
  • 如果用List来保存token,并不能给List的元素单独设置过期时间,这就需要在代码中设置延时操作来删除旧token了,这样其实也有隐患,操作起来反而比两个key麻烦

那么我们先确定使用两个token的方案,并且使用token生成的时间戳为标记,再来分析
因为校验的是本地token,而/connect/token并不需要token就能访问,所以如果前端不传token进来,那就没法操作旧token,单点登录也就失效了

那么我们操作旧token不从本地获取,再用一个key存储用户token,因为/connect/token的响应一定有access_token,所以可以在这里判断有没有旧token,这个key只用在响应处理时确认唯一token,而不是用于验证
不过这样会占掉双倍的内存,毕竟存了两个token,如果新旧token都只短时间保留,这内存就能省下来,不过代码就要多走几步,经典内存换性能

那么就开始实现吧

SinglePointLoginMiddlewareOption配置里加一个旧token缓冲时间

public class SinglePointLoginMiddlewareOption
{
    /// <summary>
    /// 获取Token路由
    /// </summary>
    public string TokenUrl { get; set; }

    /// <summary>
    /// 是否全局验证
    /// </summary>
    public bool IsGlobalAuthorize { get; set; }

    /// <summary>
    /// redis中key的前缀
    /// </summary>
    public string RedisKeyPrefix { get; set; }

    /// <summary>
    /// 旧Token保存时间,单位 秒
    /// </summary>
    public int OldTokenExpiresIn { get; set; }

    public SinglePointLoginMiddlewareOption()
    {
        this.TokenUrl = "/connect/token";
        this.IsGlobalAuthorize = false;
        this.RedisKeyPrefix = "Token_UserId_";
        this.OldTokenExpiresIn = 30;
    }
}

Module里再加配置

//配置单点登录
Configure<SinglePointLoginMiddlewareOption>(options =>
{
    options.TokenUrl = "/connect/token";
    options.IsGlobalAuthorize = false;
    options.RedisKeyPrefix = "Token_UserId_";
    options.OldTokenExpiresIn = 30;
});

最后就是中间件SinglePointLoginMiddleware

public class SinglePointLoginMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IDistributedCache _distributedCache;
    private readonly SinglePointLoginMiddlewareOption _option;

    public SinglePointLoginMiddleware(RequestDelegate next, IDistributedCache distributedCache, IOptions<SinglePointLoginMiddlewareOption> options)
    {
        this._next = next;
        this._distributedCache = distributedCache;
        this._option = options.Value;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        //需要读取响应,所以需要替换
        var originBodyStream = httpContext.Response.Body;
        using (var responseBodyStream = new MemoryStream())
        {
            try
            {
                string issuedAtTime = string.Empty;
                httpContext.Response.Body = responseBodyStream;

                bool isAllowAnonymous = false;
                if (true == this._option.IsGlobalAuthorize)
                {
                    //全局验证,AllowAnonymous不需要验证
                    var allowAnonymousAttribute = HttpContextHelper.GetAttribute<AllowAnonymousAttribute>(httpContext);

                    if (null != allowAnonymousAttribute)
                    {
                        isAllowAnonymous = true;
                    }
                }
                else
                {
                    //非全局验证,默认不需要验证
                    var authorizeAttribute = HttpContextHelper.GetAttribute<AuthorizeAttribute>(httpContext);

                    if (null == authorizeAttribute)
                    {
                        isAllowAnonymous = true;
                    }
                }

                if (false == isAllowAnonymous)
                {
                    //目标控制器函数没有匿名属性
                    string userId = string.Empty;
                    string jwtToken = string.Empty;

                    //获取token
                    jwtToken = HttpContextHelper.GetJwtToken(httpContext);
                    if (true == string.IsNullOrWhiteSpace(jwtToken))
                    {
                        //没有传递token
                        var response_401 = httpContext.Response;
                        response_401.StatusCode = 401;

                        return;
                    }

                    //获取用户Id
                    JwtPayload payload = JwtHelper.GetPayload(jwtToken);
                    if (null != payload)
                    {
                        if (true == payload.TryGetValue("sub", out object userIdObj) && null != userIdObj)
                        {
                            string userIdStr = userIdObj.ToString();
                            if (false == string.IsNullOrWhiteSpace(userIdStr))
                            {
                                userId = userIdStr;
                            }
                        }

                        if (true == payload.TryGetValue("iat", out object iatObj) && null != iatObj)
                        {
                            string iatStr = iatObj.ToString();
                            if (false == string.IsNullOrWhiteSpace(iatStr))
                            {
                                issuedAtTime = iatStr;
                            }
                        }
                    }
                    if (true == string.IsNullOrWhiteSpace(userId))
                    {
                        //payload中没有userId数据
                        var response_401 = httpContext.Response;
                        response_401.StatusCode = 401;

                        return;
                    }

                    //从redis获取token
                    //单点登录,本地token必须能在redis中找到
                    bool isTrueToken = false;

                    string redisTokenKey = $"{this._option.RedisKeyPrefix}{userId}";
                    string redisToken = await this._distributedCache.GetStringAsync(redisTokenKey);
                    if (false == string.IsNullOrWhiteSpace(redisToken) && redisToken == jwtToken)
                    {
                        //本地token与唯一token相等
                        isTrueToken = true;
                    }
                    else if (false == string.IsNullOrWhiteSpace(issuedAtTime))
                    {
                        //从redis中获取对应时间戳token
                        string tempRedisTokenKey = $"{this._option.RedisKeyPrefix}{userId}_{issuedAtTime}";
                        string tempRedisToken = await this._distributedCache.GetStringAsync(tempRedisTokenKey);

                        if (false == string.IsNullOrWhiteSpace(tempRedisToken) && tempRedisToken == jwtToken)
                        {
                            //本地token与对应时间戳token相等
                            isTrueToken = true;
                        }

                    }

                    if (false == isTrueToken)
                    {
                        var response_401 = httpContext.Response;
                        response_401.StatusCode = 401;

                        return;
                    }
                }
                await this._next(httpContext);
                await this.ResponseHandler(httpContext);
            }
            finally
            {
                //重置响应
                responseBodyStream.Seek(0, SeekOrigin.Begin);
                await responseBodyStream.CopyToAsync(originBodyStream);
                httpContext.Response.Body = originBodyStream;//这一步要不要都可以
            }
        }
    }

    /// <summary>
    /// 响应处理
    /// 将token放到redis
    /// </summary>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    private async Task<bool> ResponseHandler(HttpContext httpContext)
    {
        var request = httpContext.Request;
        var response = httpContext.Response;
        string path = request.Path;
        int statusCode = response.StatusCode;

        //从请求中获取access_token
        string newToken = string.Empty;
        if (this._option.TokenUrl == path && 200 == statusCode)
        {
            string bodyStr = await HttpContextHelper.GetResponseBodyStr(httpContext);
            if (false == string.IsNullOrWhiteSpace(bodyStr))
            {
                var bodyDic = JsonConvert.DeserializeObject<Dictionary<string, object>>(bodyStr);
                if (bodyDic != null)
                {
                    if (true == bodyDic.TryGetValue("access_token", out object accessTokenObj) && null != accessTokenObj)
                    {
                        string accessTokenStr = accessTokenObj.ToString();
                        if (false == string.IsNullOrWhiteSpace(accessTokenStr))
                        {
                            newToken = accessTokenStr;
                        }
                    }
                }
            }
        }

        if (false == string.IsNullOrWhiteSpace(newToken))
        {
            //获取userId
            JwtPayload payload = JwtHelper.GetPayload(newToken);
            string userId = string.Empty;
            long exp = 0;
            string newTokenIssuedAtTime = string.Empty;
            if (null != payload)
            {
                if (true == payload.TryGetValue("sub", out object userIdObj) && null != userIdObj)
                {
                    string userIdStr = userIdObj.ToString();
                    if (false == string.IsNullOrWhiteSpace(userIdStr))
                    {
                        userId = userIdStr;
                    }
                }
                if (true == payload.TryGetValue("exp", out object expObj) && null != expObj)
                {
                    string expStr = expObj.ToString();
                    if (false == string.IsNullOrWhiteSpace(expStr))
                    {
                        exp = long.Parse(expStr);
                    }
                }
                if (true == payload.TryGetValue("iat", out object iatObj) && null != iatObj)
                {
                    string iatStr = iatObj.ToString();
                    if (false == string.IsNullOrWhiteSpace(iatStr))
                    {
                        newTokenIssuedAtTime = iatStr;
                    }
                }
            }

            //保存到redis
            if (false == string.IsNullOrWhiteSpace(userId) && false == string.IsNullOrWhiteSpace(newTokenIssuedAtTime) && 0 != exp)
            {
                //确认唯一token
                string identityTokenKey = $"{this._option.RedisKeyPrefix}{userId}";
                string identityToken = await this._distributedCache.GetStringAsync(identityTokenKey);
                if (false == string.IsNullOrWhiteSpace(identityToken))
                {
                    var identityTokenPayload = JwtHelper.GetPayload(identityToken);

                    string oldToken = identityToken;
                    string oldTokenIssuedAtTime = string.Empty;
                    string oldTokenExpirationTime = string.Empty;
                    if (null != identityTokenPayload)
                    {
                        if (true == identityTokenPayload.TryGetValue("exp", out object identityExpObj) && null != identityExpObj)
                        {
                            string identityExpStr = identityExpObj.ToString();
                            if (false == string.IsNullOrWhiteSpace(identityExpStr))
                            {
                                oldTokenExpirationTime = identityExpStr;
                            }
                        }
                        if (true == identityTokenPayload.TryGetValue("iat", out object identityIatObj) && null != identityIatObj)
                        {
                            string identityIatStr = identityIatObj.ToString();
                            if (false == string.IsNullOrWhiteSpace(identityIatStr))
                            {
                                oldTokenIssuedAtTime = identityIatStr;
                            }
                        }
                    }

                    //旧token设置到期时间
                    if (false == string.IsNullOrWhiteSpace(oldTokenIssuedAtTime) && false == string.IsNullOrWhiteSpace(oldTokenExpirationTime))
                    {
                        //判断旧Token剩余时间是否大于将要设置的缓冲时间
                        //其实这步判断可以不要,可以直接设置缓冲时间,因为之前的中间件会验证token是否有效
                        var tempOldTokenExpirationTime = DateTimeOffset.UtcNow.AddSeconds(this._option.OldTokenExpiresIn).ToUnixTimeSeconds();
                        long oldTokenExpirationTimeLong = long.Parse(oldTokenExpirationTime);
                        if (tempOldTokenExpirationTime < oldTokenExpirationTimeLong)
                        {
                            string oldRedisTokenKey = $"{this._option.RedisKeyPrefix}{userId}_{oldTokenIssuedAtTime}";
                            DistributedCacheEntryOptions oldTokenOptions = new DistributedCacheEntryOptions();
                            oldTokenOptions.AbsoluteExpiration = DateTimeOffset.FromUnixTimeSeconds(tempOldTokenExpirationTime);
                            await this._distributedCache.SetStringAsync(oldRedisTokenKey, oldToken, oldTokenOptions);
                        }
                    }
                }

                //添加新token
                var newTokenExpirationTime = DateTimeOffset.UtcNow.AddSeconds(this._option.OldTokenExpiresIn).ToUnixTimeSeconds();
                DistributedCacheEntryOptions newTokenOptions = new DistributedCacheEntryOptions();
                newTokenOptions.AbsoluteExpiration = DateTimeOffset.FromUnixTimeSeconds(newTokenExpirationTime);
                string newRedisTokenKey = $"{this._option.RedisKeyPrefix}{userId}_{newTokenIssuedAtTime}";
                await this._distributedCache.SetStringAsync(newRedisTokenKey, newToken, newTokenOptions);

                //设置唯一token为新token
                DistributedCacheEntryOptions identityTokenOptions = new DistributedCacheEntryOptions();
                identityTokenOptions.AbsoluteExpiration = DateTimeOffset.FromUnixTimeSeconds(exp);
                await this._distributedCache.SetStringAsync(identityTokenKey, newToken, identityTokenOptions);

                return true;
            }
        }

        return false;
    }
}

每次请求/connect/token时,缓冲时间内会存在两个或三个token,缓冲时间外则只有一个token

Abp vNext单点登录 结束

标签:vNext,单点,string,Abp,token,IsNullOrWhiteSpace,var,false,httpContext
From: https://www.cnblogs.com/zzy-tongzhi-cnblog/p/17606326.html

相关文章

  • Abp中使用Hangfire实现定时任务
    有时我们需要写一些定时任务来定期执行某些方法,比如数据统计、数据计算等。这时候,我们就需要用到定时任务。Hangfire是一个开源且商业免费使用的工具函数库。可以让你非常容易地在应用中执行多种类型的后台任务,而无需自行定制开发和管理基于WindowsService后台任务执行器。......
  • abp-vnext-pro 实战(四,给客户表增加多租户)
    XXXHttpApiHostModule里面默认启用多租户publicoverridevoidOnApplicationInitialization(ApplicationInitializationContextcontext){varapp=context.GetApplicationBuilder();。。。if(MultiTenancyConst......
  • 在Volo.Abp微服务中使用SignalR
    假设需要通过SignalR发送消息通知,并在前端接收消息通知的功能创建SignalR服务在项目中引用abpadd-packageVolo.Abp.AspNetCore.SignalR在Module文件中添加对模块依赖[DependsOn(...typeof(AbpAspNetCoreSignalRModule))]publicclassIdentityApplication......
  • abp使用动态api客户端注意事项
    步骤按照官方的来就行API/DynamicCSharpAPIClients|DocumentationCenter|ABP.IO但有一点要注意,这也是官方文档没提及的,比如你在application这一层调用另一个项目的api客户端则要在application层的module里加上依赖,这个容易忘记。[DependsOn(typeof(Bank......
  • 服务器管理工具WGCLOUD如何实现单点登录SSO
    WGCLOUD是支持单点登录SSO的WGCLOUD从v3.4.8版本开始支持SSO单点登录,也就免密登录,只需要修改server的配置文件中的如下配置项即可,改为yes,然后重启server就生效了#是否开启免密登录,yes开启,no关闭,此功能开启需升级到专业版openSSO:no我们怎么实现免登录呢,请看如下这个链接,就是......
  • 软件测试单点登录之—单点流程
    用户认证中心采用票据传递的方式进行用户信息共享,保证登录会话在不同的站点进行创建。用户访问目标站点时通过当前登录的站点创建票据,传递票据到目标站点,目标站点接收到票据之后调用用户中心认证系统接口进行票据认证,认证成功之后创建登录会话从而使得用户能够进行跨站登录,具体单点......
  • poj 2886 Who Gets the Most Candies? (线段树单点更新应用)
                           poj2886WhoGetstheMostCandies?DescriptionNchildrenaresittinginacircletoplayagame.Thechildrenarenumberedfrom1toNinclockwiseorder.Eachofthemhasacardwithanon-zerointegeronit......
  • uva 12299 RMQ with Shifts(线段树单点更新初步应用)
                                 uva12299RMQwithShiftsInthetraditionalRMQ(RangeMinimumQuery)problem,wehaveastaticarrayA.Thenforeachquery(L,R)(LR),wereporttheminimumvalueamongA[L],A[L+1],...,A[R].N......
  • cookie+session(这里使用redistemplate代替)实现单点登录流程
     user发起资源请求(带上回调的路径方便回调),通过判断是否浏览器的cookie中是否存在登录过的痕迹,比如有人登了,然后存了一个cookie到浏览器如果拿到了cookie是有东西的,则带上这个cookie的内容返回给client,如果没有东西,则继续登录,向session中存入userInfo,并给浏览器设置cookie......
  • ABP VNext添加全局认证(如何继承AuthorizeFilter)
    前言目前公司采用的开发框架是ABPVNext微服务框架最近突然发现一个问题,ABP中如果控制器或服务层没有加 Authorize特性的话,则不会走身份认证,且不会认证Token如图: 但是项目已开发大半,一个个去补Authorize特性,工作量比较大,也容易产生遗漏就想着以前做单体应用的时候......