- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
我有几个旧的 ASP.NET Web 应用程序,它们共享一个 ASP.NET 成员资格数据库。我想迁移到使用 .NET Core 和 IdentityServer4 的微服务架构,并让新微服务生态系统中的身份服务器使用现有的 ASP.NET 成员资格用户存储,但 .NET Core 似乎不支持 ASP.NET 成员资格全部。
我目前有一个概念证明,涉及一个网络 API、身份服务器和一个 MVC 网络应用程序作为我的客户端。身份服务器实现了 IdentityUser 的子类并实现了 IUserStore/IUserPasswordStore/IUserEmailStore 以使其适应我现有数据库中的 ASP.NET 成员表。我可以注册新用户并通过我的 POC MVC 客户端应用程序登录,但这些用户无法登录我的旧应用程序。相反,在遗留应用程序中注册的用户无法登录到我的 POC MVC 客户端。我认为这是因为我的 IPasswordHasher 实现没有像我的遗留应用程序中的 ASP.NET 成员一样对密码进行哈希处理。
下面是我的代码。任何对我可能做错了什么的洞察力将不胜感激。安全和密码学不是我的强项。
Startup.cs
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsDevelopment())
{
// For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709
builder.AddUserSecrets<Startup>();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
/* Add CORS policy */
services.AddCors(options =>
{
// this defines a CORS policy called "default"
options.AddPolicy("default", policy =>
{
policy.WithOrigins("http://localhost:5003")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
/* Add MVC componenets. */
services.AddMvc();
/* Configure IdentityServer. */
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
// Cookie settings
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150);
options.Cookies.ApplicationCookie.LoginPath = "/Account/Login";
options.Cookies.ApplicationCookie.LogoutPath = "/Account/Logout";
// User settings
options.User.RequireUniqueEmail = true;
});
/* Add the DbContext */
services.AddDbContext<StoreContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MyConnectionString")));
/* Add ASP.NET Identity to use for registration and authentication. */
services.AddIdentity<AspNetMembershipUser, IdentityRole>()
.AddEntityFrameworkStores<StoreContext>()
.AddUserStore<AspNetMembershipUserStore>()
.AddDefaultTokenProviders();
services.AddTransient<IPasswordHasher<AspNetMembershipUser>, AspNetMembershipPasswordHasher>();
/* Add IdentityServer and its components. */
services.AddIdentityServer()
.AddInMemoryCaching()
.AddTemporarySigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
/* Configure logging. */
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
if (env.IsDevelopment())
{
loggerFactory.AddDebug();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
/* Configure wwwroot */
app.UseStaticFiles();
/* Configure CORS */
app.UseCors("default");
/* Configure AspNet Identity */
app.UseIdentity();
/* Configure IdentityServer */
app.UseIdentityServer();
/* Configure MVC */
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
AspNetMembershipUser.cs
public class AspNetMembershipUser : IdentityUser
{
public string PasswordSalt { get; set; }
public int PasswordFormat { get; set; }
}
AspNetMembershipUserStore.cs
public class AspNetMembershipUserStore : IUserStore<AspNetMembershipUser>, IUserPasswordStore<AspNetMembershipUser>, IUserEmailStore<AspNetMembershipUser>
{
private readonly StoreContext _dbcontext;
public AspNetMembershipUserStore(StoreContext dbContext)
{
_dbcontext = dbContext;
}
public Task<IdentityResult> CreateAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = new User();
this.Convert(user, dbUser);
_dbcontext.Users.Add(dbUser);
_dbcontext.SaveChanges();
return IdentityResult.Success;
}
catch (Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
public Task<IdentityResult> DeleteAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderUserName == user.NormalizedUserName);
if (dbUser != null)
{
_dbcontext.AspNetUsers.Remove(dbUser.AspNetUser);
_dbcontext.Users.Remove(dbUser);
_dbcontext.SaveChanges();
}
return IdentityResult.Success;
}
catch (Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
public void Dispose()
{
_dbcontext.Dispose();
}
public Task<AspNetMembershipUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderEmailAddress == normalizedEmail);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<AspNetMembershipUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
long lUserId = long.Parse(userId);
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u=> u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.UserId == lUserId);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<AspNetMembershipUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderUserName == normalizedUserName);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<string> GetEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Email);
}
public Task<bool> GetEmailConfirmedAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.EmailConfirmed);
}
public Task<string> GetNormalizedEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedEmail);
}
public Task<string> GetNormalizedUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedUserName);
}
public Task<string> GetPasswordHashAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.PasswordHash);
}
public Task<string> GetUserIdAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Id.ToString());
}
public Task<string> GetUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.UserName);
}
public Task<bool> HasPasswordAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => !string.IsNullOrEmpty(user.PasswordHash));
}
public Task SetEmailAsync(AspNetMembershipUser user, string email, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Email = email);
}
public Task SetEmailConfirmedAsync(AspNetMembershipUser user, bool confirmed, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.EmailConfirmed = confirmed);
}
public Task SetNormalizedEmailAsync(AspNetMembershipUser user, string normalizedEmail, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedEmail = normalizedEmail);
}
public Task SetNormalizedUserNameAsync(AspNetMembershipUser user, string normalizedName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedUserName = normalizedName);
}
public Task SetPasswordHashAsync(AspNetMembershipUser user, string passwordHash, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.PasswordHash = passwordHash);
}
public Task SetUserNameAsync(AspNetMembershipUser user, string userName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.UserName = userName);
}
public Task<IdentityResult> UpdateAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.UserId.ToString() == user.Id);
if (dbUser != null)
{
this.Convert(user, dbUser);
_dbcontext.Users.Update(dbUser);
_dbcontext.SaveChanges();
}
return IdentityResult.Success;
}
catch(Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
private void Convert(User from, AspNetMembershipUser to)
{
to.Id = from.ProviderUserKey.ToString();
to.UserName = from.ProviderUserName;
to.NormalizedUserName = from.ProviderUserName.ToLower();
to.Email = from.ProviderEmailAddress;
to.NormalizedEmail = from.ProviderEmailAddress.ToLower();
to.EmailConfirmed = true;
to.PasswordHash = from.AspNetUser.AspNetMembership.Password;
to.PasswordSalt = from.AspNetUser.AspNetMembership.PasswordSalt;
to.PasswordFormat = from.AspNetUser.AspNetMembership.PasswordFormat;
to.AccessFailedCount = from.AspNetUser.AspNetMembership.FailedPasswordAttemptCount;
to.EmailConfirmed = true;
to.Roles.Clear();
from.UserGroups.ToList().ForEach(ug =>
{
to.Roles.Add(new IdentityUserRole<string>
{
RoleId = ug.GroupId.ToString(),
UserId = ug.UserId.ToString()
});
});
to.PhoneNumber = from.Phone ?? from.ShippingPhone;
to.PhoneNumberConfirmed = !string.IsNullOrEmpty(to.PhoneNumber);
to.SecurityStamp = from.AspNetUser.AspNetMembership.PasswordSalt;
}
private void Convert(AspNetMembershipUser from , User to)
{
AspNetApplication application = _dbcontext.AspNetApplications.First();
to.ProviderUserKey = Guid.Parse(from.Id);
to.ProviderUserName = from.UserName;
to.ProviderEmailAddress = from.Email;
to.InternalEmail = $"c_{Guid.NewGuid().ToString()}@mycompany.com";
to.AccountOwner = "MYCOMPANY";
to.UserStatusId = (int)UserStatus.Normal;
AspNetUser aspNetUser = to.AspNetUser;
if (to.AspNetUser == null)
{
to.AspNetUser = new AspNetUser
{
ApplicationId = application.ApplicationId,
AspNetApplication= application,
AspNetMembership = new AspNetMembership
{
ApplicationId = application.ApplicationId,
AspNetApplication = application
}
};
}
to.AspNetUser.UserId = Guid.Parse(from.Id);
to.AspNetUser.UserName = from.UserName;
to.AspNetUser.LoweredUserName = from.UserName.ToLower();
to.AspNetUser.LastActivityDate = DateTime.UtcNow;
to.AspNetUser.IsAnonymous = false;
to.AspNetUser.ApplicationId = application.ApplicationId;
to.AspNetUser.AspNetMembership.CreateDate = DateTime.UtcNow;
to.AspNetUser.AspNetMembership.Email = from.Email;
to.AspNetUser.AspNetMembership.IsApproved = true;
to.AspNetUser.AspNetMembership.LastLoginDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LastLockoutDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LastPasswordChangedDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LoweredEmail = from.NormalizedEmail.ToLower();
to.AspNetUser.AspNetMembership.Password = from.PasswordHash;
to.AspNetUser.AspNetMembership.PasswordSalt = from.PasswordSalt;
to.AspNetUser.AspNetMembership.PasswordFormat = from.PasswordFormat;
to.AspNetUser.AspNetMembership.IsLockedOut = false;
to.AspNetUser.AspNetMembership.FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.FailedPasswordAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000");
// Merge Groups/Roles
to.UserGroups
.Where(ug => !from.Roles.Any(r => ug.GroupId.ToString() == r.RoleId))
.ToList()
.ForEach(ug => to.UserGroups.Remove(ug));
to.UserGroups
.Join(from.Roles, ug => ug.GroupId.ToString(), r => r.RoleId, (ug, r) => new { To = ug, From = r })
.ToList()
.ForEach(j =>
{
j.To.UserId = long.Parse(j.From.UserId);
j.To.GroupId = int.Parse(j.From.RoleId);
});
from.Roles
.Where(r => !to.UserGroups.Any(ug => ug.GroupId.ToString() == r.RoleId))
.ToList()
.ForEach(r =>
{
to.UserGroups.Add(new UserGroup
{
UserId = long.Parse(from.Id),
GroupId = int.Parse(r.RoleId)
});
});
}
}
AspNetMembershipPasswordHasher.cs
public class AspNetMembershipPasswordHasher : IPasswordHasher<AspNetMembershipUser>
{
private readonly int _saltSize;
private readonly int _bytesRequired;
private readonly int _iterations;
public AspNetMembershipPasswordHasher()
{
this._saltSize = 128 / 8;
this._bytesRequired = 32;
this._iterations = 1000;
}
public string HashPassword(AspNetMembershipUser user, string password)
{
string passwordHash = null;
string passwordSalt = null;
this.HashPassword(password, out passwordHash, ref passwordSalt);
user.PasswordSalt = passwordSalt;
return passwordHash;
}
public PasswordVerificationResult VerifyHashedPassword(AspNetMembershipUser user, string hashedPassword, string providedPassword)
{
// Throw an error if any of our passwords are null
if (hashedPassword == null)
{
throw new ArgumentNullException("hashedPassword");
}
if (providedPassword == null)
{
throw new ArgumentNullException("providedPassword");
}
string providedPasswordHash = null;
if (user.PasswordFormat == 0)
{
providedPasswordHash = providedPassword;
}
else if (user.PasswordFormat == 1)
{
string providedPasswordSalt = user.PasswordSalt;
this.HashPassword(providedPassword, out providedPasswordHash, ref providedPasswordSalt);
}
else
{
throw new NotSupportedException("Encrypted passwords are not supported.");
}
if (providedPasswordHash == hashedPassword)
{
return PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
}
private void HashPassword(string password, out string passwordHash, ref string passwordSalt)
{
byte[] hashBytes = null;
byte[] saltBytes = null;
byte[] totalBytes = new byte[this._saltSize + this._bytesRequired];
if (!string.IsNullOrEmpty(passwordSalt))
{
// Using existing salt.
using (var pbkdf2 = new Rfc2898DeriveBytes(password, Convert.FromBase64String(passwordSalt), this._iterations))
{
saltBytes = pbkdf2.Salt;
hashBytes = pbkdf2.GetBytes(this._bytesRequired);
}
}
else
{
// Generate a new salt.
using (var pbkdf2 = new Rfc2898DeriveBytes(password, this._saltSize, this._iterations))
{
saltBytes = pbkdf2.Salt;
hashBytes = pbkdf2.GetBytes(this._bytesRequired);
}
}
Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, this._saltSize);
Buffer.BlockCopy(hashBytes, 0, totalBytes, this._saltSize, this._bytesRequired);
using (SHA256 hashAlgorithm = SHA256.Create())
{
passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes));
passwordSalt = Convert.ToBase64String(saltBytes);
}
}
}
最佳答案
我的一位同事能够帮助我。下面是散列函数的样子。通过此更改,ASP.NET Identity 能够利用现有的 ASP.NET 成员资格数据库。
private void HashPassword(string password, out string passwordHash, ref string passwordSalt)
{
byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
byte[] saltBytes = null;
if (!string.IsNullOrEmpty(passwordSalt))
{
saltBytes = Convert.FromBase64String(passwordSalt);
}
else
{
saltBytes = new byte[128 / 8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(saltBytes);
}
}
byte[] totalBytes = new byte[saltBytes.Length + passwordBytes.Length];
Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, saltBytes.Length);
Buffer.BlockCopy(passwordBytes, 0, totalBytes, saltBytes.Length, passwordBytes.Length);
using (SHA1 hashAlgorithm = SHA1.Create())
{
passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes));
}
passwordSalt = Convert.ToBase64String(saltBytes);
}
您可以找到所有 source code on GitHib .
关于c# - 如何在 ASP.NET Identity 中使用和使用 ASP.NET Membership 数据库?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45873006/
我的问题是如何在 python 中创建一个简单的数据库。我的例子是: User = { 'Name' : {'Firstname', 'Lastname'}, 'Address' : {'Street
我需要创建一个与远程数据库链接的应用程序! mysql 是最好的解决方案吗? Sqlite 是唯一的本地解决方案吗? 我使用下面的方法,我想知道它是否是最好的方法! NSString *evento
给定两台 MySQL 服务器,一台本地,一台远程。两者都有一个包含表 bohica 的数据库 foobar。本地服务器定义了用户 'myadmin'@'%' 和 'myadmin'@'localhos
我有以下灵活的搜索查询 Select {vt:code},{vt:productcode},{vw:code},{vw:productcode} from {abcd AS vt JOIN wxyz
好吧,我的电脑开始运行有点缓慢,所以我重置了 Windows,保留了我的文件。因为我的大脑还没有打开,所以我忘记事先备份我的 MySQL 数据库。我仍然拥有所有原始文件,因此我实际上仍然拥有数据库,但
如何将我的 Access 数据库 (.accdb) 转换为 SQLite 数据库 (.sqlite)? 请,任何帮助将不胜感激。 最佳答案 1)如果要转换 db 的结构,则应使用任何 DB 建模工具:
系统检查发现了一些问题: 警告:?:(mysql.W002)未为数据库连接“默认”设置 MySQL 严格模式 提示:MySQL 的严格模式通过将警告升级为错误来修复 MySQL 中的许多数据完整性问题
系统检查发现了一些问题: 警告:?:(mysql.W002)未为数据库连接“默认”设置 MySQL 严格模式 提示:MySQL 的严格模式通过将警告升级为错误来修复 MySQL 中的许多数据完整性问题
我想在相同的 phonegap 应用程序中使用 android 数据库。 更多说明: 我创建了 phonegap 应用程序,但 phonegap 应用程序不支持服务,所以我们已经在 java 中为 a
Time Tracker function clock() { var mytime = new Date(); var seconds
我需要在现有项目上实现一些事件的显示。我无法更改数据库结构。 在我的 Controller 中,我(从 ajax 请求)传递了一个时间戳,并且我需要显示之前的 8 个事件。因此,如果时间戳是(转换后)
我有一个可以收集和显示各种测量值的产品(不会详细介绍)。正如人们所期望的那样,显示部分是一个数据库+建立在其之上的网站(使用 Symfony)。 但是,我们可能还会创建一个 API 来向第三方公开数据
我们将 SQL Server 从 Azure VM 迁移到 Azure SQL 数据库。 Azure VM 为 DS2_V2、2 核、7GB RAM、最大 6400 IOPS Azure SQL 数据
我正在开发一个使用 MongoDB 数据库的程序,但我想问在通过 Java 执行 SQL 时是否可以使用内部数据库进行测试,例如 H2? 最佳答案 你可以尝试使用Testcontainers Test
已关闭。此问题不符合Stack Overflow guidelines 。目前不接受答案。 已关闭 9 年前。 此问题似乎与 a specific programming problem, a sof
我正在尝试使用 MSI 身份验证(无需用户名和密码)从 Azure 机器学习服务连接 Azure SQL 数据库。 我正在尝试在 Azure 机器学习服务上建立机器学习模型,目的是我需要数据,这就是我
我在我的 MySQL 数据库中使用这个查询来查找 my_column 不为空的所有行: SELECT * FROM my_table WHERE my_column != ""; 不幸的是,许多行在
我有那个基地:http://sqlfiddle.com/#!2/e5a24/2这是 WordPress 默认模式的简写。我已经删除了该示例不需要的字段。 如您所见,我的结果是“类别 1”的两倍。我喜欢
我有一张这样的 table : mysql> select * from users; +--------+----------+------------+-----------+ | userid
我有表: CREATE TABLE IF NOT EXISTS `category` ( `id` int(11) NOT NULL, `name` varchar(255) NOT NULL
我是一名优秀的程序员,十分优秀!