.net core基于Oauth2+jwt两种方式实现身份认证(附单点登录)

2023-05-16

引用地址:.net core基于Oauth2+jwt两种方式实现身份认证(附单点登录)_cslx5zx5的博客-CSDN博客_.net core oauth2

基于.net core 3.1微服务架构的SSO单点登录实战

本文所涵盖的几个比较重要的知识点如下:

资源请求管道模型
依赖注入
自定义中间件
静态扩展
AOP
先看两幅图大概了解一下

Oauth2

-------------------------------------分割线-----------------------------------------

JWT


由于篇幅所限,对与OAuth2和JWT的相关概念将不再赘述
核心代码分以下几个模块

数据仓储层,存放数据库实体以及数据库上下文

//当前登录用户实体
public class LoginUser
{
    public string UserId { get; set; }
    public string UserName { get; set; }
    public string Mobile { get; set; }
    /// <summary>
    /// 租户ID
    /// </summary>
    public string TenantId { get; set; }
}
//用户数据
public class AuthSysUser
{
    public string UserId { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public string Mobile { get; set; }
    public string TenantId { get; set; }
}
//用户token
public class AuthSysUserToken
{
    public string UserId { get; set; }
    public string Token { get; set; }
    public DateTime? ExpireTime { get; set; }
    public DateTime? UpdateTime { get; set; }
}
//数据库上下文,这里博主用的是sqlserver,当然你也可以用其他数据库实现
//右键依赖项管理NuGet程序包 搜索并添加 Microsoft.EntityFrameworkCore.SqlServer;
using Microsoft.EntityFrameworkCore;
public class AuthDbContext : DbContext
  {
      public AuthDbContext() { }
      public AuthDbContext(DbContextOptions<AuthDbContext> options)
        : base(options) { }

      public DbSet<AuthSysUserToken> SysUserTokens { get; set; }

      public DbSet<AuthSysUser> Users { get; set; }

  }

 
服务层,对于JWT和Oauth2提供接口约束和实现

 //定义接口
 public interface IAuthService
 {
     string ServiceName { get; }
     /// 判断权限
     Task<bool> PermissionAsync(string token, string path);
     /// 获取用户
     Task SetUserAsync(string token);
 }

//JWT实现
public class JwtAuthServiceImpl : IAuthService
{
    private readonly GlobalSettings _globalSettings;
    private readonly LoginUser _currentUser;

    public JwtAuthServiceImpl(IOptions<GlobalSettings> options, LoginUser currentUser)
    {
        _globalSettings = options.Value;
        
        _currentUser = currentUser;
    }

    public string ServiceName => nameof(JwtAuthServiceImpl);

    public Task<bool> PermissionAsync(string token, string path)
    {
        return Task.FromResult(JwtUtils.CheckToken(token, _globalSettings.Jwt.Secret));
    }

    public Task SetUserAsync(string token)
    {
        var payload = JwtUtils.GetPayload(token);
        _currentUser.UserId = payload.sub;
        return Task.CompletedTask;
    }
}

//Oauth2实现
public class OauthAuthServiceImpl : IAuthService
{
     private readonly AuthDbContext _passportDbContext;
     private readonly GlobalSettings _globalSettings;
     private readonly LoginUser _currentUser;

     public OauthAuthServiceImpl(AuthDbContext passportDbContext, IOptions<GlobalSettings> options, LoginUser currentUser)
     {
         _passportDbContext = passportDbContext;
         _globalSettings = options.Value;
         _currentUser = currentUser;
     }

     public string ServiceName => nameof(OauthAuthServiceImpl);

     /// <summary>
     /// 测试TOKEN
     /// </summary>
     public static string TEST_TOKEN = "73c8aa709d744848b3f4b697b48905ca";
     /// <summary>
     /// 测试用户
     /// </summary>
     public static string TEST_ADMIN = "a437a67c2de345a9a5c56ade745c0ecd";

     public async Task<bool> PermissionAsync(string token, string path)
     {
         if (token == null) throw new ArgumentNullException(nameof(token));
         if (path == null) throw new ArgumentNullException(nameof(path));

         if (token == TEST_TOKEN)
         {
             await SetUserAsync(token);
             return true;
         }
         //Token过期
         var tokenEntity = await _passportDbContext.SysUserTokens.Where(s => s.Token == token).FirstOrDefaultAsync();
         if (tokenEntity == null) return false;
         if (tokenEntity.ExpireTime != null && tokenEntity.ExpireTime <= DateTime.Now) return false;

         return true;
     }

     public async Task SetUserAsync(string token)
     {
         if (string.IsNullOrEmpty(_currentUser.UserId))
         {
             //TODO: 固定一个测试用户Token
             if (token == TEST_TOKEN)
             {
                 var user = new LoginUser
                 {
                     UserId = Guid.NewGuid().ToString("N"),
                     Mobile = "13012345678",
                     UserName = "Roy",
                     TenantId = DateTime.Now.ToString()
                 };
                 user.CopyTo(_currentUser);
             }
             else
             {
                 var user = await (from t in _passportDbContext.SysUserTokens.AsNoTracking()
                                   join u in _passportDbContext.Users.AsNoTracking()
                                   on t.UserId equals u.UserId
                                   where t.Token == token
                                   select new LoginUser
                                   {
                                       UserId = u.UserId,
                                       Mobile = u.Mobile,
                                       UserName = u.Username,
                                       TenantId = u.TenantId
                                   }).FirstOrDefaultAsync();
                 user?.CopyTo(_currentUser);
             }
         }
     }
 }
 
中间件层,用于服务注册在管道模型中

public sealed class LoginMiddlerware
{
     private readonly RequestDelegate _next;

     public LoginMiddlerware(RequestDelegate next)
     {
         _next = next;
     }

     /// <summary>
     /// 设置登录用户
     /// </summary>
     /// <param name="context"></param>
     /// <param name="_authService"></param>
     /// <returns></returns>
     public async Task InvokeAsync(HttpContext context, IEnumerable<IAuthService> _authService)
     {
         string token = GetToken();
         if (!string.IsNullOrEmpty(token))
         {
             if (token.StartsWith("Bearer") || token.Contains('.'))
             {
                 await _authService.First(a => a.ServiceName == nameof(JwtAuthServiceImpl)).SetUserAsync(token);
             }
             else
             {
                 await _authService.First(a => a.ServiceName == nameof(OauthAuthServiceImpl)).SetUserAsync(token);
             }
         }
         await _next.Invoke(context);
         string GetToken()
         {
             string token = context.Request.Headers["token"];
             if (string.IsNullOrEmpty(token))
             {
                 token = context.Request.Query["token"];
             }
             return token;
         }
     }
 }

 //过滤器依赖注入(AOP)
  public class ClaimRequirementAttribute
  {
       /// <summary>
       /// 自定义授权验证特性
       /// </summary>
       public class RequiresPermissionsAttribute : TypeFilterAttribute
       {
           public RequiresPermissionsAttribute(ClaimType claimType, string claimValue = "") : base(typeof(ClaimRequirementFilter))
           {
               Arguments = new object[] { new Claim(claimType.ToString(), claimValue) };
           }
       }

       /// <summary>
       /// 自定义授权验证过滤器
       /// </summary>
       public class ClaimRequirementFilter : IAuthorizationFilter
       {
           //授权声明
           readonly Claim _claim;
           //授权接口
           readonly IEnumerable<IAuthService> _authService;
           //全局配置类
           private readonly GlobalSettings _globalSettings;

           //构造函数注入
           public ClaimRequirementFilter(Claim claim, IEnumerable<IAuthService> authService, IOptions<GlobalSettings> _options)
           {
               _claim = claim;
               _authService = authService;
               _globalSettings = _options.Value;
           }

           public void OnAuthorization(AuthorizationFilterContext context)
           {
               //获取控制器描述符
               ControllerActionDescriptor controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
               if (controllerActionDescriptor != null)
               {
                   var skipAuthorization = controllerActionDescriptor.MethodInfo.GetCustomAttributes(inherit: true)
                       .Any(a => a.GetType().Equals(typeof(AllowAnonymousAttribute)));
                   //如果存在描述符则跳过验证 比如【AllowAnonymous】
                   if (skipAuthorization)
                   {
                       return;
                   }
               }

               //检查白名单
               if (_globalSettings.WhiteList != null && _globalSettings.WhiteList.Any(path => path == context.HttpContext.Request.Path.Value))
               {
                   return;
               }
               //声明类型
               ClaimType claimType = Enum.Parse<ClaimType>(_claim.Type);
               bool permission = false;
               //获取token
               string token = GetToken();
               if (string.IsNullOrEmpty(token))
               {
                   //返回401
                   context.Result = new UnauthorizedResult();
                   return;
               }
               if (claimType == ClaimType.Multiple)
               {
                   //根据Token类型选择认证方式
                   if (token.Any(t => t == '.'))
                   {
                       claimType = ClaimType.JWT;
                   }
                   else
                   {
                       claimType = ClaimType.Oauth2;
                   }
               }

               switch (claimType)
               {
                   case ClaimType.Oauth2:
                       permission = _authService.First(a => a.ServiceName == nameof(OauthAuthServiceImpl)).PermissionAsync(token, _claim.Value).Result;
                       break;
                   case ClaimType.JWT:
                       permission = _authService.First(a => a.ServiceName == nameof(JwtAuthServiceImpl)).PermissionAsync(token, _claim.Value).Result;
                       break;
                   default:
                       throw new Exception($"没有指定的授权方式:{claimType}");
               }

               if (!permission)
               {
                   //返回401
                   context.Result = new UnauthorizedResult();
                   return;
               }

               string GetToken()
               {
                   string token = context.HttpContext.Request.Headers["token"];
                   if (string.IsNullOrEmpty(token))
                   {
                       token = context.HttpContext.Request.Query["token"];
                   }
                   if (string.IsNullOrEmpty(token))
                   {
                       context.HttpContext.Request.Cookies.TryGetValue("token", out token);
                   }
                   return token;
               }
           }
       }
   }

    public enum ClaimType
    {
        Oauth2,
        JWT,
        Multiple
    }
 
附上JWT帮助类

using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace MyTest.Utils
{
    public static class JwtUtils
    {
        /// <summary>
        /// 生成token
        /// </summary>
        /// <param name="claims"></param>
        /// <returns></returns>
        public static string CreateToken(IEnumerable<Claim> claims, string securityKey)
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512);
            var securityToken = new JwtSecurityToken(
                issuer: null,
                audience: null,
                claims: claims,
                //expires: DateTime.Now.AddMinutes(settings.ExpMinutes),
                signingCredentials: creds);
            var token = new JwtSecurityTokenHandler().WriteToken(securityToken);
            return token;
        }


        /// <summary>
        /// 生成Jwt
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="roleName"></param>
        /// <param name="userId"></param>
        /// <returns></returns>
        public static string GenerateToken(string userId, string securityKey)
        {
            //声明claim
            var claims = new Claim[] {
                new Claim(JwtRegisteredClaimNames.Typ,"JWT"),
                new Claim(JwtRegisteredClaimNames.Sub, userId),
                new Claim(JwtRegisteredClaimNames.Iat,DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),ClaimValueTypes.Integer64),
                new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddDays(2).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), //过期时间
                new Claim("username","roy")
            };
            return CreateToken(claims, securityKey);
        }

        /// <summary>
        /// 从token中获取用户身份
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public static IEnumerable<Claim> GetClaims(string token)
        {
            var handler = new JwtSecurityTokenHandler();
            var securityToken = handler.ReadJwtToken(token);
            return securityToken?.Claims;
        }


        /// <summary>
        /// 从Token中获取用户身份
        /// </summary>
        /// <param name="token"></param>
        /// <param name="securityKey">securityKey明文,Java加密使用的是Base64</param>
        /// <returns></returns>
        public static ClaimsPrincipal GetPrincipal(string token, string securityKey)
        {
            try
            {
                var handler = new JwtSecurityTokenHandler();
                TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
                {
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)),
                    ValidateLifetime = false
                };
                return handler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken);
            }
            catch (Exception ex)
            {
                return null;
            }
        }

        /// <summary>
        /// 校验Token
        /// </summary>
        /// <param name="token">token</param>
        /// <returns></returns>
        public static bool CheckToken(string token, string securityKey)
        {
            var principal = GetPrincipal(token, securityKey);
            if (principal is null)
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// 获取Token中的载荷数据
        /// </summary>
        /// <param name="token">token</param>
        /// <returns></returns>
        public static JwtPayload GetPayload(string token)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken securityToken = jwtHandler.ReadJwtToken(token);
            return new JwtPayload
            {
                sub = securityToken.Payload[JwtRegisteredClaimNames.Sub]?.ToString(),
                exp = DateTimeOffset.FromUnixTimeSeconds(long.Parse(securityToken.Payload[JwtRegisteredClaimNames.Exp].ToString())).ToLocalTime().DateTime,
                iat = securityToken.Payload[JwtRegisteredClaimNames.Iat]?.ToString()
            };
        }

        /// <summary>
        /// 获取Token中的载荷数据
        /// </summary>
        /// <typeparam name="T">泛型</typeparam>
        /// <param name="token">token</param>
        /// <returns></returns>
        public static T GetPayload<T>(string token)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(token);
            return JsonConvert.DeserializeObject<T>(jwtToken.Payload.SerializeToJson());
        }
    }

    /// <summary>
    /// Jwt载荷信息
    /// </summary>
    public class JwtPayload
    {
        public string sub { get; set; }

        public string iat { get; set; }

        public DateTime exp { get; set; }
    }
}

 
噢对了,还有静态扩展方法,通过泛型参数和反射用来实现cope两个对象的属性值

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;

namespace MyTest.Extension
{
    public static class ModelExtension
    {
        /// <summary>
        /// 反射实现两个类的对象之间相同属性的值的复制
        /// </summary>
        /// <typeparam name="Tag">返回的实体</typeparam>
        /// <typeparam name="Source">数据源实体</typeparam>
        /// <param name="tag">目标实体</param>
        /// <param name="sourct">数据源实体</param>
        /// <param name="ignoreId">忽略ID字段</param>
        /// <param name="ignoreEmpty">忽略空字段</param>
        /// <returns></returns>
        public static void CopyTo<Source, Tag>(this Source sourct, Tag tag, bool ignoreId = true, bool ignoreEmpty = false)
        {
            var Types = sourct.GetType();
            var TypeTag = typeof(Tag);
            foreach (PropertyInfo source in Types.GetProperties())
            {
                foreach (PropertyInfo tagProp in TypeTag.GetProperties())
                {
                    if (!(ignoreId && tagProp.Name.Equals("id", StringComparison.OrdinalIgnoreCase)) && tagProp.Name.Equals(source.Name, StringComparison.OrdinalIgnoreCase) && tagProp.PropertyType == source.PropertyType)
                    {
                        if (tagProp.CanWrite && source.GetValue(sourct) != null)
                        {
                            tagProp.SetValue(tag, source.GetValue(sourct, null), null);
                        }
                    }
                }
            }
        }
    }
}
 
OK核心代码上完,接下来就是重头戏了,我们都知道Startup启动类是整个项目的灵魂,里面包含了管道处理模型,中间件,以及各种服务的注入,于是乎对两个主要配置函数Configure和ConfigureServices进行扩展
Configure扩展

public static class GoAppExtensions
{
    public static IApplicationBuilder UseGo(this IApplicationBuilder app, IConfiguration configuration)
    {
        //允许body重用
        app.Use(next => context =>
        {
            context.Request.EnableBuffering();
            return next(context);
        });

        //设置基础路由ContextPath
        var config = configuration.GetValue<string>("GlobalSettings:ContextPath");

        if (config != null)
        {
            app.UsePathBase(new Microsoft.AspNetCore.Http.PathString($"/{config}"));
        }
        //路由匹配中间件,找到匹配的终结者路由Endpoint
        app.UseRouting();
        //启用 跨域 中间件
        app.UseCors();
        //登录中间件
        app.UseMiddleware(typeof(LoginMiddlerware));
        //针对 UseRouting 中间件中匹配到的路由进行拦截 做授权验证操作
        app.UseAuthorization();
        //终结者路由,针对 UseRouting 中间件匹配到的路由进行 委托方法的执行等操作
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
        return app;
    }
}

 
ConfigureServices扩展

 public static class GoServicesExtensions
 {
     public static IServiceCollection AddGoServices(this IServiceCollection services, IConfiguration configuration)
     {
         services.Configure<KestrelServerOptions>(x => x.AllowSynchronousIO = true);
         //添加对控制器以及与 API 相关的功能
         services.AddControllers();

         //跨域
         services.AddCors(options =>
         {
             options.AddDefaultPolicy(builder =>
             {
                 builder
                 .SetIsOriginAllowed(t => true)
                 .AllowAnyMethod()
                 .AllowAnyHeader()
                 .AllowCredentials();
             });
         });

         services.AddDbContext<AuthDbContext>(optionsBuilder =>
         {
             //数据库连接字符串,取配置文件,可自行更改
             optionsBuilder.UseSqlServer(configuration.GetConnectionString("passport"));
         });
         //当前登录用户实体
         services.AddScoped<LoginUser>();

         services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
         //注入配置类
         services.Configure<GlobalSettings>(configuration.GetSection("GlobalSettings"));
         //鉴权服务
         services.AddScoped<IAuthService, OauthAuthServiceImpl>();
         services.AddScoped<IAuthService, JwtAuthServiceImpl>();
         //ServiceLocator.SetLocatorProvider(services.BuildServiceProvider());
         return services;
     }
 }
 
Startup.cs

 public class Startup
 {
     public Startup(IConfiguration configuration)
     {
         Configuration = configuration;
     }

     public IConfiguration Configuration { get; }

     // This method gets called by the runtime. Use this method to add services to the container.
     public void ConfigureServices(IServiceCollection services)
     {
         services.AddGoServices(Configuration);
     }

     // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
     {
         app.Use(next => context =>
         {
             context.Request.EnableBuffering();
             return next(context);
         });
         app.UseGo(Configuration);
     }
 }
 
大功告成,整个框架就搭建完毕了,要想做单点登录的话可以在登录成功后把生成的token存储在数据库,设置一个有效期,然后把token返回到客户端存储,每次请求接口在请求头部带上token即可。

服务端如果有接口需要登录才能允许访问的,可以直接在Controller或者Action上面加上特性 [RequiresPermissions(ClaimType.Oauth2)]
不需要身份效验的Action上加上 [AllowAnonymous] (注意命名空间:using Microsoft.AspNetCore.Authorization)就OK了

最后附上项目源码Git地址
————————————————
版权声明:本文为CSDN博主「cslx5zx5」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cslx5zx5/article/details/109401116

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

.net core基于Oauth2+jwt两种方式实现身份认证(附单点登录) 的相关文章

随机推荐

  • 简单理解混淆矩阵—Matlab详细代码注解

    本人计算机小白一枚 xff0c 将自己学到的知识点整理出来 xff0c 一方面是对自己学习的小总结 xff0c 另一方面是欢迎大家批评指正 如果觉得写得还可以 xff0c 大家可以转发关注此博客 xff0c 谢谢 xff01 后续会有新算法
  • ubuntu下/boot空间被占满的解决方法

    ubuntu下 boot空间被占满的解决方法 在安装Ubuntu的时候 xff0c 给 boot文件目录分配空间的时候 xff0c 是100M xff0c boot可以单独分成一个区 xff0c 也可以不单独分 xff0c 在 xff08
  • scikit-learn sklearn 0.18 官方文档中文版

    本人计算机小白一枚 xff0c 将自己学到的知识点整理出来 xff0c 一方面是对自己学习的小总结 xff0c 另一方面是欢迎大家批评指正 如果觉得写得还可以 xff0c 大家可以转发关注此博客 xff0c 谢谢 xff01 后续会有新算法
  • 机器学习面试问题汇总—史上最详细

    本人计算机小白一枚 xff0c 将自己学到的知识点整理出来 xff0c 一方面是对自己学习的小总结 xff0c 另一方面是欢迎大家批评指正 如果觉得写得还可以 xff0c 大家可以转发关注此博客 xff0c 谢谢 xff01 后续会有新算法
  • 从第一个人开始报数(从1到3报数),凡报到3的人退出圈子

    有n个人围成一圈 xff0c 顺序排号 从第一个人开始报数 xff08 从1到3报数 xff09 xff0c 凡报到3的人退出圈子 xff0c 问最后留下的是原来第几号的那位 xff08 1 lt 61 n lt 61 1000000 xf
  • Windows10 Xrdp远程桌面连接ubuntu 20.04,一步到位!

    https blog csdn net avinswang article details 93601312 utm medium 61 distribute pc relevant none task blog BlogCommendFr
  • 远程桌面无响应解决方案

    问题1 mstsc链接远程桌面后 发现只有桌面背景 xff08 桌面图标不见了 xff0c 任务栏不见了 xff0c 鼠标无处可点 xff09 问题2 远程桌面卡住了 问题分析 针对于问题1 xff0c 是因为关闭了explorer exe
  • 公用网络切换为专用网络的详细步骤

    win10连接的网络显示为公用网络 xff0c 一般来说公用网络的安全性不高 xff0c 专用网络安全性会比公用网络要高 具体方法如下 1 我的电脑安装的是win10系统 xff0c 网络状态为 xff1a 公用网络 2 把Windows1
  • mysql使用SSL连接配置学习(一)

    参考 xff1a https www jb51 net article 100432 htm https www cnblogs com mysql dba p 7061300 html 一 SSL介绍 SSL xff08 Secure S
  • 富文本回显

    今天遇了富文本的回显问题 xff0c 查了好多网页 xff0c 最后总结回显最简单方法 xff0c 留个纪念 lt c forEach items 61 34 bidNoticeList 34 varStatus 61 34 status
  • 弹出详细信息窗口

    lt DOCTYPE html PUBLIC 34 W3C DTD HTML 4 01 Transitional EN 34 gt lt html gt lt head gt lt meta charset 61 34 GB 2312 34
  • 字符串中特殊字符处理

    public String htmlReplace String str str 61 str replace 34 amp ldquo 34 34 34 str 61 str replace 34 amp rdquo 34 34 34 s
  • Android开发环境配置

    环境为jdk11 43 android studio2021 jdk11的安装 xff1a 参考https blog csdn net qq 22136439 article details 103295019 注意 xff1a 1 jdk
  • java中文字符乱码编码转换大全

    System out println new String myFileName getBytes 34 gbk 34 System out println new String myFileName getBytes 34 utf 8 3
  • java实现打印套打功能

    package test import java awt print import java awt 实现套打 xff0c 把套打的格式当成一张图片 xff0c 将要填入的数据按其在图片在坐标来定位 public class PrintTe
  • oracle中not in的优化

    ORACLE中NOT IN 的替代 http myjcwy iteye com blog 553563 典型的查询方式为 xff1a SELECT TITLE FROM BOOKSHELF WHERE TITLE NOT IN SELECT
  • Visual Stdio 中的error C2001: 常量中有换行符

    error C2001 常量中有换行符 问题 xff1a 使用Visual Stdio xff0c cout中文的时候 xff0c 有的时候可以正常编译并运行 xff0c 但是有的时候会出现error C2001 常量中有换行符 出现err
  • oracle实现每天定时执行一个计划任务

    http www shangxueba com jingyan 1614101 html oracle实现每天定时执行一个计划任务 创建测试表 SQL gt create table a a date 表已创建 创建一个自定义过程 SQL
  • Spring Security配置访问权限在登录页循环并报错302

    事情是这样的 xff0c 在学习Spring Security框架时 xff0c 使用框架默认的登录页面 xff0c 没有任何问题输入正确的账户和密码后都能登录 xff0c 但是将登录页面换为自定义的页面后就无法登录了 如下 xff1a 密
  • .net core基于Oauth2+jwt两种方式实现身份认证(附单点登录)

    引用地址 xff1a net core基于Oauth2 43 jwt两种方式实现身份认证 附单点登录 cslx5zx5的博客 CSDN博客 net core oauth2 基于 net core 3 1微服务架构的SSO单点登录实战 本文所