ASP.Web和移动的.NET Web API社会认证



我的问题有点复杂,所以请原谅我,因为我试图很好地列出我正在努力解决的问题。

拥有ASP。. NET网站,让用户注册&通过用户名/密码或社交(Facebook, Twitter, Google等)登录,也有一个API。这个API需要用[Authorize]锁定。API需要能够被移动客户端(Android, iOS等)访问,可以通过用户名/密码或社交(Facebook, Twitter, Google等)登录。

所以我做了一些网站,可以从我的目标做一两件事,但不是全部在一起。网上有很多很好的例子,VS项目中也有很多内置的例子,展示了如何让用户通过社交应用注册和登录,但它们只适用于网站,不适用于移动设备。我已经做了一个网站,Android应用程序使用用户名/密码与该API进行身份验证,但没有OAuth或社会凭证。

我开始使用这个页面作为参考,但我不知道如何采取,并使其工作为我的网站登录和我的移动应用程序登录。

这家伙让它听起来很简单,但没有显示任何代码。

是否有一个教程或GitHub的例子,可以让我达到我的目标?我基本上想要一个网站,人们可以注册用户名/密码或使用他们的社交帐户,也让用户做同样的(注册&通过移动设备登录。移动设备基本上只是使用API来推/拉数据,但我不确定如何将社交登录与我的API结合起来。我假设我需要使用OAuth并走那条路,但我找不到任何好的例子来展示如何为web和移动设备做到这一点。

或者也许是正确的解决方案是让网页是所有cookie认证和API是一个单独的"网站"和所有令牌认证,他们都绑定到相同的数据库?

我已经成功地在我自己的ASP中完成了这个任务。asp.net MVC应用程序。. NET身份,但随后遇到你提到的问题:我需要使用Web API工作,以便我的移动应用程序可以本地交互。

我不熟悉你链接的文章,但读完之后,我注意到他们的很多工作和代码是不必要的,复杂的功能已经存在于ASP中。净的身份。

这是我的建议,我假设你正在使用ASP。NET Identity V2,它相当于围绕MVC5的包(而不是新的MVC6 vNext)。这将允许您的网站和移动应用程序通过API通过本地登录(用户名/密码)和外部OAuth提供商进行身份验证,无论是从您网站上的MVC web视图还是通过您的移动应用程序的web API调用:

步骤1。在创建项目时,确保包含了MVC和Web API所需的包。在ASP中。. NET项目选择对话框,您将有选择复选框的选项,确保MVC和Web API都被选中。如果您在创建项目时还没有这样做,我建议您创建一个新项目并迁移现有代码,而不是搜索并手动添加依赖项和模板代码。

步骤2。在Startup.Auth.cs文件中,您需要代码来告诉OWIN使用cookie身份验证,允许外部在cookie中签名,并支持OAuth承载令牌(这就是Web API调用进行身份验证的方式)。以下是我工作项目代码库中的相关摘录:

Startup.Auth.cs

// Enable the application to use a cookie to store information for the signed in user
        // and to use a cookie to temporarily store information about a user logging in with a third party login provider
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/account/login"),
            Provider = new CookieAuthenticationProvider
            {
                // Enables the application to validate the security stamp when the user logs in.
                // This is a security feature which is used when you change a password or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Configure the application for OAuth based flow
        PublicClientId = "self";
        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/token"),
            Provider = new ApplicationOAuthProvider(PublicClientId),
            AuthorizeEndpointPath = new PathString("/api/account/externallogin"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            //AllowInsecureHttp = false
        };
        // Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(OAuthOptions);
 app.UseTwitterAuthentication(
            consumerKey: "Twitter API Key",
            consumerSecret: "Twitter API Secret");
        app.UseFacebookAuthentication(
            appId: "Facebook AppId",
            appSecret: "Facebook AppSecret");

在上述代码中,我目前支持Twitter和Facebook作为外部身份验证提供者;但是,您可以使用app.UserXYZProvider调用和其他库添加额外的外部提供程序,它们将与我在这里提供的代码一起使用。

步骤3。在你的WebApiConfig.cs文件中,你必须配置HttpConfiguration来抑制默认主机身份验证并支持OAuth承载令牌。解释一下,这告诉你的应用程序区分MVC和Web API之间的身份验证类型,这样你就可以为网站使用典型的cookie流,同时你的应用程序将接受来自Web API的OAuth形式的承载令牌,而不会抱怨或其他问题。

WebApiConfig.cs

// Web API configuration and services
        // Configure Web API to use only bearer token authentication.
        config.SuppressDefaultHostAuthentication();
        config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

步骤4。对于MVC和Web API,您都需要一个AccountController(或同等用途的控制器)。在我的项目中,我有两个AccountController文件,一个从基controller类继承的MVC控制器,另一个从控制器中的ApiController继承的AccountController。API命名空间以保持整洁。我使用的标准模板AccountController代码从Web API和MVC项目。以下是Account Controller的API版本:

AccountController.cs(控制器。API名称空间)

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Disco.Models.API;
using Disco.Providers;
using Disco.Results;
using Schloss.AspNet.Identity.Neo4j;
using Disco.Results.API;
namespace Disco.Controllers.API
{
    [Authorize]
    [RoutePrefix("api/account")]
    public class AccountController : ApiController
    {
        private const string LocalLoginProvider = "Local";
        private ApplicationUserManager _userManager;
        public AccountController()
        {            
        }
        public AccountController(ApplicationUserManager userManager,
            ISecureDataFormat<AuthenticationTicket> accessTokenFormat)
        {
            UserManager = userManager;
            AccessTokenFormat = accessTokenFormat;
        }
        public ApplicationUserManager UserManager
        {
            get
            {
                return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
            }
            private set
            {
                _userManager = value;
            }
        }
        public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; }
        // GET account/UserInfo
        [HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
        [Route("userinfo")]
        public UserInfoViewModel GetUserInfo()
        {
            ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
            return new UserInfoViewModel
            {
                Email = User.Identity.GetUserName(),
                HasRegistered = externalLogin == null,
                LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null
            };
        }
        // POST account/Logout
        [Route("logout")]
        public IHttpActionResult Logout()
        {
            Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
            return Ok();
        }
        // GET account/ManageInfo?returnUrl=%2F&generateState=true
        [Route("manageinfo")]
        public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false)
        {
            IdentityUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
            if (user == null)
            {
                return null;
            }
            List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
            foreach (UserLoginInfo linkedAccount in await UserManager.GetLoginsAsync(User.Identity.GetUserId()))
            {
                logins.Add(new UserLoginInfoViewModel
                {
                    LoginProvider = linkedAccount.LoginProvider,
                    ProviderKey = linkedAccount.ProviderKey
                });
            }
            if (user.PasswordHash != null)
            {
                logins.Add(new UserLoginInfoViewModel
                {
                    LoginProvider = LocalLoginProvider,
                    ProviderKey = user.UserName,
                });
            }
            return new ManageInfoViewModel
            {
                LocalLoginProvider = LocalLoginProvider,
                Email = user.UserName,
                Logins = logins,
                ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
            };
        }
        // POST account/ChangePassword
        [Route("changepassword")]
        public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword,
                model.NewPassword);
            if (!result.Succeeded)
            {
                return GetErrorResult(result);
            }
            return Ok();
        }
        // POST account/SetPassword
        [Route("setpassword")]
        public async Task<IHttpActionResult> SetPassword(SetPasswordBindingModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword);
            if (!result.Succeeded)
            {
                return GetErrorResult(result);
            }
            return Ok();
        }
        // POST account/AddExternalLogin
        [Route("addexternallogin")]
        public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
            AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken);
            if (ticket == null || ticket.Identity == null || (ticket.Properties != null
                && ticket.Properties.ExpiresUtc.HasValue
                && ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow))
            {
                return BadRequest("External login failure.");
            }
            ExternalLoginData externalData = ExternalLoginData.FromIdentity(ticket.Identity);
            if (externalData == null)
            {
                return BadRequest("The external login is already associated with an account.");
            }
            IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(),
                new UserLoginInfo(externalData.LoginProvider, externalData.ProviderKey));
            if (!result.Succeeded)
            {
                return GetErrorResult(result);
            }
            return Ok();
        }
        // POST account/RemoveLogin
        [Route("removelogin")]
        public async Task<IHttpActionResult> RemoveLogin(RemoveLoginBindingModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            IdentityResult result;
            if (model.LoginProvider == LocalLoginProvider)
            {
                result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId());
            }
            else
            {
                result = await UserManager.RemoveLoginAsync(User.Identity.GetUserId(),
                    new UserLoginInfo(model.LoginProvider, model.ProviderKey));
            }
            if (!result.Succeeded)
            {
                return GetErrorResult(result);
            }
            return Ok();
        }
        // GET account/ExternalLogin
        [OverrideAuthentication]
        [HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
        [AllowAnonymous]
        [Route("externallogin", Name = "ExternalLoginAPI")]
        public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null)
        {
            if (error != null)
            {
                return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error));
            }
            if (!User.Identity.IsAuthenticated)
            {
                return new ChallengeResult(provider, this);
            }
            ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
            if (externalLogin == null)
            {
                return InternalServerError();
            }
            if (externalLogin.LoginProvider != provider)
            {
                Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
                return new ChallengeResult(provider, this);
            }
            ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
                externalLogin.ProviderKey));
            bool hasRegistered = user != null;
            if (hasRegistered)
            {
                Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
                 ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
                    OAuthDefaults.AuthenticationType);
                ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
                    CookieAuthenticationDefaults.AuthenticationType);
                AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
                Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
            }
            else
            {
                IEnumerable<Claim> claims = externalLogin.GetClaims();
                ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
                Authentication.SignIn(identity);
            }
            return Ok();
        }
        // GET account/ExternalLogins?returnUrl=%2F&generateState=true
        [AllowAnonymous]
        [Route("externallogins")]
        public IEnumerable<ExternalLoginViewModel> GetExternalLogins(string returnUrl, bool generateState = false)
        {
            IEnumerable<AuthenticationDescription> descriptions = Authentication.GetExternalAuthenticationTypes();
            List<ExternalLoginViewModel> logins = new List<ExternalLoginViewModel>();
            string state;
            if (generateState)
            {
                const int strengthInBits = 256;
                state = RandomOAuthStateGenerator.Generate(strengthInBits);
            }
            else
            {
                state = null;
            }
            foreach (AuthenticationDescription description in descriptions)
            {
                ExternalLoginViewModel login = new ExternalLoginViewModel
                {
                    Name = description.Caption,
                    Url = Url.Route("ExternalLogin", new
                    {
                        provider = description.AuthenticationType,
                        response_type = "token",
                        client_id = Startup.PublicClientId,
                        redirect_uri = new Uri(Request.RequestUri, returnUrl).AbsoluteUri,
                        state = state
                    }),
                    State = state
                };
                logins.Add(login);
            }
            return logins;
        }
        // POST account/Register
        [AllowAnonymous]
        [Route("register")]
        public async Task<IHttpActionResult> Register(RegisterBindingModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
            IdentityResult result = await UserManager.CreateAsync(user, model.Password);
            if (!result.Succeeded)
            {
                return GetErrorResult(result);
            }
            return Ok();
        }
        // POST account/RegisterExternal
        [OverrideAuthentication]
        [HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
        [Route("registerexternal")]
        public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            var info = await Authentication.GetExternalLoginInfoAsync();
            if (info == null)
            {
                return InternalServerError();
            }
            var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
            IdentityResult result = await UserManager.CreateAsync(user);
            if (!result.Succeeded)
            {
                return GetErrorResult(result);
            }
            result = await UserManager.AddLoginAsync(user.Id, info.Login);
            if (!result.Succeeded)
            {
                return GetErrorResult(result); 
            }
            return Ok();
        }
        protected override void Dispose(bool disposing)
        {
            if (disposing && _userManager != null)
            {
                _userManager.Dispose();
                _userManager = null;
            }
            base.Dispose(disposing);
        }
        #region Helpers
        private IAuthenticationManager Authentication
        {
            get { return Request.GetOwinContext().Authentication; }
        }
        private IHttpActionResult GetErrorResult(IdentityResult result)
        {
            if (result == null)
            {
                return InternalServerError();
            }
            if (!result.Succeeded)
            {
                if (result.Errors != null)
                {
                    foreach (string error in result.Errors)
                    {
                        ModelState.AddModelError("", error);
                    }
                }
                if (ModelState.IsValid)
                {
                    // No ModelState errors are available to send, so just return an empty BadRequest.
                    return BadRequest();
                }
                return BadRequest(ModelState);
            }
            return null;
        }
        private class ExternalLoginData
        {
            public string LoginProvider { get; set; }
            public string ProviderKey { get; set; }
            public string UserName { get; set; }
            public IList<Claim> GetClaims()
            {
                IList<Claim> claims = new List<Claim>();
                claims.Add(new Claim(ClaimTypes.NameIdentifier, ProviderKey, null, LoginProvider));
                if (UserName != null)
                {
                    claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider));
                }
                return claims;
            }
            public static ExternalLoginData FromIdentity(ClaimsIdentity identity)
            {
                if (identity == null)
                {
                    return null;
                }
                Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
                if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer)
                    || String.IsNullOrEmpty(providerKeyClaim.Value))
                {
                    return null;
                }
                if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
                {
                    return null;
                }
                return new ExternalLoginData
                {
                    LoginProvider = providerKeyClaim.Issuer,
                    ProviderKey = providerKeyClaim.Value,
                    UserName = identity.FindFirstValue(ClaimTypes.Name)
                };
            }
        }
        private static class RandomOAuthStateGenerator
        {
            private static RandomNumberGenerator _random = new RNGCryptoServiceProvider();
            public static string Generate(int strengthInBits)
            {
                const int bitsPerByte = 8;
                if (strengthInBits % bitsPerByte != 0)
                {
                    throw new ArgumentException("strengthInBits must be evenly divisible by 8.", "strengthInBits");
                }
                int strengthInBytes = strengthInBits / bitsPerByte;
                byte[] data = new byte[strengthInBytes];
                _random.GetBytes(data);
                return HttpServerUtility.UrlTokenEncode(data);
            }
        }
        #endregion
    }
}

第5步。您还需要创建ApplicationOAuthProvider,以便服务器可以生成并验证OAuth令牌。这在WebAPI样例项目中提供。这是我的文件版本:

ApplicationOAuthProvider.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Butler.Models;
using Schloss.AspNet.Identity.Neo4j;
namespace Butler.Providers
{
    public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
    {
        private readonly string _publicClientId;
        public ApplicationOAuthProvider(string publicClientId)
        {
            if (publicClientId == null)
            {
                throw new ArgumentNullException("publicClientId");
            }
            _publicClientId = publicClientId;
        }
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
            ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
            if (user == null)
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                return;
            }
            ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
               OAuthDefaults.AuthenticationType);
            ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
                CookieAuthenticationDefaults.AuthenticationType);
            AuthenticationProperties properties = CreateProperties(user.UserName);
            AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
            context.Validated(ticket);
            context.Request.Context.Authentication.SignIn(cookiesIdentity);
        }
        public override Task TokenEndpoint(OAuthTokenEndpointContext context)
        {
            foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
            {
                context.AdditionalResponseParameters.Add(property.Key, property.Value);
            }
            return Task.FromResult<object>(null);
        }
        public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            // Resource owner password credentials does not provide a client ID.
            if (context.ClientId == null)
            {
                context.Validated();
            }
            return Task.FromResult<object>(null);
        }
        public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
        {
            if (context.ClientId == _publicClientId)
            {
                //Uri expectedRootUri = new Uri(context.Request.Uri, "/");
                //if (expectedRootUri.AbsoluteUri == context.RedirectUri)
                //{
                    context.Validated();
                //}
            }
            return Task.FromResult<object>(null);
        }
        public static AuthenticationProperties CreateProperties(string userName)
        {
            IDictionary<string, string> data = new Dictionary<string, string>
            {
                { "userName", userName }
            };
            return new AuthenticationProperties(data);
        }
    }
}

还包括ChallengeResult,您的应用程序的Web API部门将使用它来处理外部登录提供者提供的挑战,以验证您的用户:

ChallengeResult.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
namespace Butler.Results
{
    public class ChallengeResult : IHttpActionResult
    {
        public ChallengeResult(string loginProvider, ApiController controller)
        {
            LoginProvider = loginProvider;
            Request = controller.Request;
        }
        public string LoginProvider { get; set; }
        public HttpRequestMessage Request { get; set; }
        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            Request.GetOwinContext().Authentication.Challenge(LoginProvider);
            HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

使用这组代码,您将能够在API版本的AccountController上使用HTTP GET和HTTP POST路由来注册用户,使用用户名和密码登录以接收承载令牌,添加/删除外部登录,管理外部登录,最重要的是,通过传递外部登录令牌来为您的应用程序交换OAuth承载令牌进行身份验证。

您可能想看看这个系列的文章,看看它是否涵盖了您的目标:

基于令牌的身份验证NET Web API 2、Owin和Identity作者:Taiseer Joudeh

这两篇文章是关于使用OWIN创建基于令牌的身份验证服务,其中一个部分涉及使用外部登录(如Facebook和Google+)。这些示例主要围绕web应用程序作为web服务的消费者,但它也应该适用于移动应用程序。这些文章有一个GitHub项目和一个非常活跃的评论区,几乎没有任何问题是没有答案的。

希望这篇文章能帮你实现你的目标。

我将此添加为您问题的第二部分的单独答案,说YES您可以将两个单独的项目绑定到同一数据库,只需让MVC/Web Forms网站项目使用所有cookie认证,然后有一个单独的Web API项目,即所有令牌认证。

在我用源代码示例的较长回答中,我基本上所做的是将两个独立的项目合并为一个项目,以避免冗余的模型代码和控制器代码。就我而言,这对我来说更有意义;然而,我倾向于说,这取决于个人偏好和项目的需要来决定是否维护两个独立的项目,一个网站和一个web API端点,或者将它们组合在一起。

ASP。. NET被设计成非常灵活和即插即用的中间件,我可以证明我的项目已经存在,并且完全按照预期的代码运行,在两个独立的项目中,现在作为一个合并的项目。

最新更新