详解在ASP.NET Core中使用Angular2以及与Angular2的Token base身份认证
angular2是对angular1的一次彻底的,破坏性的更新。
相对于angular1.x,借用某果的广告语,唯一的不同,就是处处都不同。
•首先,推荐的语言已经不再是javascript,取而代之的typescript,(typescript = es6 + 类型系统 + 类型注解), typescriipt的类型系统对于开发复杂的单页web app大有帮助,同时编译成javascript后的执行效率也比大多数手写javascript要快。有兴趣的同学可以查阅官方文档: |。
•得益于彻底重构,性能相对于angular1.x有了大幅提升,也更适合再全平台部署。
•angular2是基于component的,component可以理解为是1.x时代的controller + $scope + view
•view的很多语法也做了更新,比如<li ng-repeat="movie in vm.movies"></li> 变成了 <li *ngfor="let movie of movies"></li>
关于angular2,强烈建议查阅官方文档:|
注意:本文章属于step by step + code sample教程,且篇幅较长,建议下载本sample并跟着本文进度自己重做一遍本例,下载完整代码并分析代码结构才有意义,下载地址:how to authorization angular 2 app with asp.net core web api
1.前期准备
•推荐使用vs2015 update3或更新的版本完成本示例,下载地址:
•你需要安装.net core开发环境,这里提供vs版: //www.jb51.net/softs/472362.html
•安装node.js 版本5.0.0或以上,(在本例中,这个主要是编译typescript用的)下载地址:node.js and npm
•npm 3.0.0或以上,默认npm会随着node.js一并安装完毕。(在本例中,这个主要是下载各种angular的各个包用的,参考vs中的nuget)
2.创建项目
在vs中新建项目,项目类型选择 asp.net core web application(.net core),输入项目名称为:csauthorangular2inaspnetcore,template选择为empty.
3.在项目中整合angular2
3.1.配置startup.cs
注:添加下面的代码时ide会报代码错误,这是因为还没有引用对用的包,进入报错的这一行,点击灯泡,加载对应的包就可以了。
(图文无关)
在configureservices中添加如下代码
services.addmvc();
这里是添加mvc服务
在configure中添加如下代码
app.usestaticfiles(); app.usemvc(routes => { routes.maproute( name: "default", template: "{controller=home}/{action=index}"); });
第一句是启用静态文件,第二句是应用mvc模式并添加路由配置。
完整的代码应该是这个样子
public class startup { // this method gets called by the runtime. use this method to add services to the container. // for more information on how to configure your application, visit http://go.microsoft.com/fwlink/?linkid=398940 public void configureservices(iservicecollection services) { services.addmvc(); } // 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) { app.usestaticfiles(); app.usemvc(routes => { routes.maproute( name: "default", template: "{controller=home}/{action=index}"); }); } }
3.2.添加控制器以及视图
3.2.1.在项目根目录下添加controllers目录,并在其中添加一个控制器homecontroller.cs,默认代码即可。
3.2.2.在项目跟目录下创建views目录,在views目录中新建目录home, 最后在home目录中新建视图index.cshtml,内容应该是这样:
<html> <head> <title>angular quickstart</title> <base href="/"> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 1. load libraries --> <!-- polyfill(s) for older browsers --> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.js"></script> <script src="node_modules/reflect-metadata/reflect.js"></script> <script src="node_modules/systemjs/dist/system.src.js"></script> <!-- 2. configure systemjs --> <script src="systemjs.config.js"></script> <script> system.import('app').catch(function(err){ console.error(err); }); </script> </head> <!-- 3. display the application --> <body> <my-app>loading...</my-app> </body> </html>
现在运行项目的话你仅仅能看到一个loading,再控制台中你还能看到错误,这是因为我们还没有配置angular。让我们前往wwwroot目录。
3.3.在项目的wwwroot目录中添加如下结构:
3.3.1搭建angular2基础环境
•package.json
{ "name": "angular-quickstart", "version": "1.0.0", "scripts": { "start": "tsc && concurrently \"tsc -w\" \"lite-server\" ", "lite": "lite-server", "postinstall": "typings install", "tsc": "tsc", "tsc:w": "tsc -w", "typings": "typings" }, "licenses": [ { "type": "mit", "url": "https://github.com/angular/angular.io/blob/master/license" } ], "dependencies": { "@angular/common": "2.0.2", "@angular/compiler": "2.0.2", "@angular/core": "2.0.2", "@angular/forms": "2.0.2", "@angular/http": "2.0.2", "@angular/platform-browser": "2.0.2", "@angular/platform-browser-dynamic": "2.0.2", "@angular/router": "3.0.2", "@angular/upgrade": "2.0.2", "angular-in-memory-web-api": "0.1.5", "bootstrap": "3.3.7", "core-js": "2.4.1", "reflect-metadata": "0.1.8", "rxjs": "5.0.0-beta.12", "systemjs": "0.19.39", "zone.js": "0.6.25" }, "devdependencies": { "concurrently": "3.0.0", "gulp": "^3.9.1", "lite-server": "2.2.2", "typescript": "2.0.3", "typings": "1.4.0" } }
•systemjs.config.js
(function (global) { system.config({ paths: { // paths serve as alias 'npm:': 'node_modules/' }, // map tells the system loader where to look for things map: { // our app is within the app folder app: 'app', // angular bundles '@angular/core': 'npm:@angular/core/bundles/core.umd.js', '@angular/common': 'npm:@angular/common/bundles/common.umd.js', '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', '@angular/http': 'npm:@angular/http/bundles/http.umd.js', '@angular/router': 'npm:@angular/router/bundles/router.umd.js', '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js', // other libraries 'rxjs': 'npm:rxjs', 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js' }, // packages tells the system loader how to load when no filename and/or no extension packages: { app: { main: './main.js', defaultextension: 'js' }, rxjs: { defaultextension: 'js' } } }); })(this);
•tsconfig.js
{ "compileonsave": true, "compileroptions": { "target": "es5", "module": "commonjs", "moduleresolution": "node", "sourcemap": true, "emitdecoratormetadata": true, "experimentaldecorators": true, "removecomments": false, "noimplicitany": false }, "exclude": [ "node_modules" ] }
•typings.json(注,在最新文档中typings已被npm的@types替代,参见官方文档:)
{ "globaldependencies": { "core-js": "registry:dt/core-js#0.0.0+20160725163759", "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", "node": "registry:dt/node#6.0.0+20160909174046" } }
右击wwwroot中的package.json,选择restore packages(或者在cmd下进入wwwroot目录,并执行命令 npm install),npm会去下载需要的包,并存储于node_modules目录中。
3.3.2.配置启动文件以启用angular2
在wwwroot下新建目录app,app拥有如下文件:
•app.component.ts
import { component } from '@angular/core'; @component({ moduleid: module.id, selector: 'my-app', template: "this is in angular2", }) export class appcomponent { }
可以发现被@component装饰属性装饰了appcomponent,selector指代你component的占位符,比如本例中你可以再home/index.cshtml中发现一段这样的标记
<my-app>loading...</my-app>
template既为该component的view,不要忘记moduleid,不添加它会出现很多奇怪的问题。
•app.module.ts
import { ngmodule } from "@angular/core"; import { browsermodule } from "@angular/platform-browser"; import { appcomponent } from "./app.component"; @ngmodule({ bootstrap: [appcomponent], imports: [ browsermodule ], declarations: [ appcomponent ] }) export class appmodule { }
•main.ts
import { platformbrowserdynamic } from '@angular/platform-browser-dynamic'; import { appmodule } from './app.module'; const platform = platformbrowserdynamic(); platform.bootstrapmodule(appmodule);
基础整合完毕。
按f5 debug一下,现在你能再浏览器中看到一句话:this is in angular 2
4.实现身份认证
废了半天劲,看着很傻,没有任何成就感。怎么办,让我们再深入一点,接下来我们来为angular2完成一个token base的身份验证,我会把angular2的routing,data bind,service,http,等等你工作中最常用到的挨个演示一遍。
4.1.server端
4.1.1.创建一些辅助类
4.1.1.1.在项目根目录下创建一个文件夹auth,并添加rsakeyhelper.cs以及tokenauthoption.cs两个文件
•在rsakeyhelper.cs中
using system.security.cryptography; namespace cstokenbaseauth.auth { public class rsakeyhelper { public static rsaparameters generatekey() { using (var key = new rsacryptoserviceprovider(2048)) { return key.exportparameters(true); } } } }
•在tokenauthoption.cs中
using system; using microsoft.identitymodel.tokens; namespace cstokenbaseauth.auth { public class tokenauthoption { public static string audience { get; } = "exampleaudience"; public static string issuer { get; } = "exampleissuer"; public static rsasecuritykey key { get; } = new rsasecuritykey(rsakeyhelper.generatekey()); public static signingcredentials signingcredentials { get; } = new signingcredentials(key, securityalgorithms.rsasha256signature); public static timespan expiresspan { get; } = timespan.fromminutes(20); } }
4.1.1.2.在项目根目录下创建目录model,并在其中添加requestresult.cs,代码应该是这样。
public class requestresult { public requeststate state { get; set; } public string msg { get; set; } public object data { get; set; } } public enum requeststate { failed = -1, notauth = 0, success = 1 }
4.1.2更新startup.cs
在configureservices中添加如下代码:
services.addauthorization(auth => { auth.addpolicy("bearer", new authorizationpolicybuilder() .addauthenticationschemes(jwtbearerdefaults.authenticationscheme) .requireauthenticateduser().build()); });
这里是添加身份认证服务
在configure方法中添加如下代码:
app.useexceptionhandler(appbuilder => { appbuilder.use(async (context, next) => { var error = context.features[typeof(iexceptionhandlerfeature)] as iexceptionhandlerfeature; //when authorization has failed, should retrun a json message to client if (error != null && error.error is securitytokenexpiredexception) { context.response.statuscode = 401; context.response.contenttype = "application/json"; await context.response.writeasync(jsonconvert.serializeobject(new requestresult { state = requeststate.notauth, msg = "token expired" })); } //when orther error, retrun a error message json to client else if (error != null && error.error != null) { context.response.statuscode = 500; context.response.contenttype = "application/json"; await context.response.writeasync(jsonconvert.serializeobject(new requestresult { state = requeststate.failed, msg = error.error.message })); } //when no error, do next. else await next(); }); });
本段是handle当身份认证失败时抛出的异常,并返回合适的json
在相同的方法中添加另外一段代码:
app.usejwtbearerauthentication(new jwtbeareroptions() { tokenvalidationparameters = new tokenvalidationparameters() { issuersigningkey = tokenauthoption.key, validaudience = tokenauthoption.audience, validissuer = tokenauthoption.issuer, // when receiving a token, check that we've signed it. validateissuersigningkey = true, // when receiving a token, check that it is still valid. validatelifetime = true, // this defines the maximum allowable clock skew - i.e. provides a tolerance on the token expiry time // when validating the lifetime. as we're creating the tokens locally and validating them on the same // machines which should have synchronised time, this can be set to zero. where external tokens are // used, some leeway here could be useful. clockskew = timespan.fromminutes(0) } });
本段代码是应用jwtbearerauthentication身份认证。
4.1.3.tokenauthcontroller.cs
在controllers中新建一个web api controller class,命名为tokenauthcontroller.cs。我们将在这里完成登录授权,
在同文件下添加两个类,分别用来模拟用户模型,以及用户存储,代码应该是这样:
public class user { public guid id { get; set; } public string username { get; set; } public string password { get; set; } } public static class userstorage { public static list<user> users { get; set; } = new list<user> { new user {id=guid.newguid(),username="user1",password = "user1psd" }, new user {id=guid.newguid(),username="user2",password = "user2psd" }, new user {id=guid.newguid(),username="user3",password = "user3psd" } }; }
接下来在tokenauthcontroller.cs中添加如下方法
private string generatetoken(user user, datetime expires) { var handler = new jwtsecuritytokenhandler(); claimsidentity identity = new claimsidentity( new genericidentity(user.username, "tokenauth"), new[] { new claim("id", user.id.tostring()) } ); var securitytoken = handler.createtoken(new securitytokendescriptor { issuer = tokenauthoption.issuer, audience = tokenauthoption.audience, signingcredentials = tokenauthoption.signingcredentials, subject = identity, expires = expires }); return handler.writetoken(securitytoken); }
该方法仅仅只是生成一个auth token,接下来我们来添加另外一个方法来调用它
在相同文件中添加如下代码
[httppost] public string getauthtoken(user user) { var existuser = userstorage.users.firstordefault(u => u.username == user.username && u.password == user.password); if (existuser != null) { var requestat = datetime.now; var expiresin = requestat + tokenauthoption.expiresspan; var token = generatetoken(existuser, expiresin); return jsonconvert.serializeobject(new { statecode = 1, requertat = requestat, expiresin = tokenauthoption.expiresspan.totalseconds, accesstoken = token }); } else { return jsonconvert.serializeobject(new { statecode = -1, errors = "username or password is invalid" }); } }
接下来我们来完成授权部分,在相同的文件中添加如下代码:
public string getuserinfo() { var claimsidentity = user.identity as claimsidentity; return jsonconvert.serializeobject(new requestresult { state = requeststate.success, data = new { username = claimsidentity.name } }); }
为方法添加装饰属性
[httpget]
[authorize("bearer")]
第二行代码说明这个action需要身份验证。
该文件完整的代码应该是这个样子:
using system; using system.collections.generic; using system.linq;using microsoft.aspnetcore.mvc; using csauthorangular2inaspnetcore.auth; using system.identitymodel.tokens.jwt; using newtonsoft.json; using system.security.claims; using system.security.principal; using microsoft.identitymodel.tokens; using csauthorangular2inaspnetcore.model; using microsoft.aspnetcore.authorization; namespace csauthorangular2inaspnetcore.controllers { [route("api/[controller]")] public class tokenauthcontroller : controller { [httppost] public string getauthtoken([frombody]user user) { var existuser = userstorage.users.firstordefault(u => u.username == user.username && u.password == user.password); if (existuser != null) { var requestat = datetime.now; var expiresin = requestat + tokenauthoption.expiresspan; var token = generatetoken(existuser, expiresin); return jsonconvert.serializeobject(new requestresult { state = requeststate.success, data = new { requertat = requestat, expiresin = tokenauthoption.expiresspan.totalseconds, tokeytype = tokenauthoption.tokentype, accesstoken = token } }); } else { return jsonconvert.serializeobject(new requestresult { state = requeststate.failed, msg = "username or password is invalid" }); } } private string generatetoken(user user, datetime expires) { var handler = new jwtsecuritytokenhandler(); claimsidentity identity = new claimsidentity( new genericidentity(user.username, "tokenauth"), new[] { new claim("id", user.id.tostring()) } ); var securitytoken = handler.createtoken(new securitytokendescriptor { issuer = tokenauthoption.issuer, audience = tokenauthoption.audience, signingcredentials = tokenauthoption.signingcredentials, subject = identity, expires = expires }); return handler.writetoken(securitytoken); } [httpget] [authorize("bearer")] public string getuserinfo() { var claimsidentity = user.identity as claimsidentity; return jsonconvert.serializeobject(new requestresult { state = requeststate.success, data = new { username = claimsidentity.name } }); } } public class user { public guid id { get; set; } public string username { get; set; } public string password { get; set; } } public static class userstorage { public static list<user> users { get; set; } = new list<user> { new user {id=guid.newguid(),username="user1",password = "user1psd" }, new user {id=guid.newguid(),username="user2",password = "user2psd" }, new user {id=guid.newguid(),username="user3",password = "user3psd" } }; } }
4.2angular2端
4.2.1创建view model
在wwwroot/app下创建一个目录:_model, 并添加一个typescript文件requestresult.ts,内容应该是这样。
export class requestresult { state: number; msg: string; data: object; }
4.2.2创建service
在wwwroot/app下创建一个目录:_services,并添加一个typescript文件auth.service.ts,内容应该是这样。
import { injectable } from "@angular/core"; import { headers, http } from "@angular/http"; import "rxjs/add/operator/topromise"; import { requestresult } from "../_model/requestresult"; @injectable() export class authservice { private tokeykey = "token"; private token: string; constructor( private http: http ) { } login(username: string, password: string): promise<requestresult> { return this.http.post("/api/tokenauth", { username: username, password: password }).topromise() .then(response => { let result = response.json() as requestresult; if (result.state == 1) { let json = result.data as any; sessionstorage.setitem("token", json.accesstoken); } return result; }) .catch(this.handleerror); } checklogin(): boolean { var token = sessionstorage.getitem(this.tokeykey); return token != null; } getuserinfo(): promise<requestresult> { return this.authget("/api/tokenauth"); } authpost(url: string, body: any): promise<requestresult> { let headers = this.initauthheaders(); return this.http.post(url, body, { headers: headers }).topromise() .then(response => response.json() as requestresult) .catch(this.handleerror); } authget(url): promise<requestresult> { let headers = this.initauthheaders(); return this.http.get(url, { headers: headers }).topromise() .then(response => response.json() as requestresult) .catch(this.handleerror); } private getlocaltoken(): string { if (!this.token) { this.token = sessionstorage.getitem(this.tokeykey); } return this.token; } private initauthheaders(): headers { let token = this.getlocaltoken(); if (token == null) throw "no token"; var headers = new headers(); headers.append("authorization", "bearer " + token); return headers; } private handleerror(error: any): promise<any> { console.error('an error occurred', error); return promise.reject(error.message || error); } }
本文件主要用来完成登录以及登录验证工作,之后该service将可以被注入到component中以便被component调用。
注:主要的逻辑都应该写到service中
4.2.3.创建component
4.2.3.1.在wwwroot/app下创建一个目录home,该目录用来存放homecomponent,home应拥有如下文件:
•home.component.ts
import { component, oninit } from "@angular/core"; import { authservice } from "../_services/auth.service"; @component({ moduleid: module.id, selector: "my-home", templateurl: "view.html", styleurls: ["style.css"] }) export class homecomponent implements oninit { islogin = false; username: string; constructor( private authservice: authservice ) { } ngoninit(): void { this.islogin = this.authservice.checklogin(); if (this.islogin) { this.authservice.getuserinfo().then(res => { this.username = (res.data as any).username; }); } } }
查阅代码,在@component中指定了view以及style。
authservice被在构造方法中被注入了本component,ngoninit是接口oninit的一个方法,他在component初始化时会被调用。
•style.css
/*styles of this view*/
本例中没有添加任何样式,如有需要可以写在这里。
•view.html
<div *ngif="islogin"> <h1>hi <span>{{username}}</span></h1> </div> <div *ngif="!islogin"> <h1>please login</h1> <a routerlink="/login">login</a> </div>
*ngif=""是angular2 的其中一种标记语法,作用是当返回真时渲染该节点,完整教程请参阅官方文档。
4.2.3.2.在wwwroot/app下创建目录login,该目录用来存放logincomponent,文件结构类似于上一节。
•login.component.ts
import { component } from "@angular/core"; import { router } from '@angular/router'; import { authservice } from "../_services/auth.service"; @component({ moduleid: module.id, selector: "my-login", templateurl: "view.html", styleurls: ["style.css"] }) export class logincomponent { private username: string; private password: string; constructor( private authservice: authservice, private router: router ) { } login() { this.authservice.login(this.username, this.password) .then(result => { if (result.state == 1) { this.router.navigate(["./home"]); } else { alert(result.msg); } }); } }
•style.css
/*styles of this view*/
•view.html
<table> <tr> <td>username:</td> <td><input [(ngmodel)]="username" placeholder="usename:try type user1" /></td> </tr> <tr> <td>username:</td> <td><input [(ngmodel)]="password" placeholder="password:try type user1psd" /></td> </tr> <tr> <td></td> <td><input type="button" (click)="login()" value="login" /></td> </tr> </table>
4.2.4.应用路由
路由是切换多页面用的。
在wwwroot/app下新建一个typescript文件,命名为app-routing.module.ts,内容应该是这个样子。
import { ngmodule } from "@angular/core"; import { routermodule, routes } from "@angular/router"; import { homecomponent } from "./home/home.component"; import { logincomponent } from "./login/login.component" const routes: routes = [ { path: "", redirectto: "/home", pathmatch: "full" }, { path: "home", component: homecomponent }, { path: "login", component: logincomponent } ]; @ngmodule({ imports: [routermodule.forroot(routes)], exports: [routermodule] }) export class approutingmodule { }
接下来我们来应用这个路由,
打开app.module.ts,更新代码如下:
import { ngmodule } from "@angular/core"; import { browsermodule } from "@angular/platform-browser"; import { httpmodule } from "@angular/http"; import { formsmodule } from "@angular/forms"; import { approutingmodule } from "./app-routing.module"; import { authservice } from "./_services/auth.service"; import { appcomponent } from "./app.component"; import { homecomponent } from "./home/home.component"; import { logincomponent } from "./login/login.component"; @ngmodule({ bootstrap: [appcomponent], imports: [ browsermodule, httpmodule, approutingmodule, formsmodule ], declarations: [ appcomponent, homecomponent, logincomponent ], providers: [authservice] }) export class appmodule { }
ngmodule和browsermodule你可以理解为基础模块,必加的。
httpmodule是做http请求用的。
formsmodule是做双向数据绑定用的,比如下面这样的,如果想把数据从view更新到component,就必须加这个。
<input [(ngmodel)]="username" placeholder="usename:try type user1" />
approutingmodule即为我们刚才添加的路由文件。
authservice是我们最早添加的service文件。
appcomponent是我们最初添加的那个app.component.ts里的那个component.
homecomponent,logincomponent同上。
最后我们再app.component.ts中添加路由锚点,
把template的值为 "<router-outlet></router-outlet>"
完整的代码应该是这样:
import { component } from '@angular/core'; @component({ moduleid: module.id, selector: 'my-app', template: "<router-outlet></router-outlet>", }) export class appcomponent { }
router-outlet是路由锚点的关键词。
至此,所有代码完成,f5调试吧。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 利用jq让你的div居中的好方法分享
下一篇: JS实现指定区域的全屏显示功能示例