本篇详细描述怎么为Angular SPA程序添加Authorization的全记录。相对应的,本篇中使用了Identity Server (.Net Core开源项目)作为Identity Provider。
权限控制无所不在,基于OAuth, OpenID这些解决方案在今时今日的开发中几乎是必不可少的。
这里只强调下Access Token和Refresh Token的关联与区别:
那么Access Token怎么跟Refresh Token协同工作呢?一般来说,整个
由于Refresh Token这个特效,在开发库中,其也被称为Offline Access。
如果是Angular CLI创建的应用程序,添加:
ng add angular-auth-oidc-client
当然也可以使用NPM/YARN来安装。
当开始执行时,首先要求确认:
ℹ Using package manager: npm
✔ Found compatible package version: angular-auth-oidc-client@14.1.5.
✔ Package information loaded.
The package angular-auth-oidc-client@14.1.5 will be installed and executed.
Would you like to proceed? (Y/n)
当选择Y
之后,会进行安装,并要求输入一些必要信息。下列中的(XXXX)
是项目特定信息,需要按照项目的实际填写。
✔ Package successfully installed.
? What flow to use? OIDC Code Flow PKCE using refresh tokens
? Please enter your authority URL or Azure tenant id or Http config URL (XXXX)
Running checks...
✅️ Project found, working with 'myproject'
✅️ Added "angular-auth-oidc-client" 14.1.5
Installing packages...
✅️ Installed
✅️ 'src/app/auth/auth-config.module.ts' will be created
✅️ 'AuthConfigModule' is imported in 'src/app/app.module.ts'
✅️ All imports done, please add the 'RouterModule' as well if you don't have it imported yet.
✅️ No silent-renew entry in assets array needed
✅️ No 'silent-renew.html' needed
CREATE src/app/auth/auth-config.module.ts (703 bytes)
UPDATE package.json (2281 bytes)
UPDATE src/app/app.module.ts (3951 bytes)
这时,项目中多了一个src\auth
的文件夹,其中只有一个Module。
@NgModule({
imports: [AuthModule.forRoot({
config: {
authority: 'XXXX.com',
redirectUrl: window.location.origin,
postLogoutRedirectUri: window.location.origin,
clientId: 'please-enter-clientId',
scope: 'please-enter-scopes', // 'openid profile offline_access ' + your scopes
responseType: 'code',
silentRenew: true,
useRefreshToken: true,
renewTimeBeforeTokenExpiresInSeconds: 30,
}
})],
exports: [AuthModule],
})
export class AuthConfigModule {}
其中有些信息需要更新:scope
,clientId
等。
如果需要silent renew(自动更新Access Token),需要在scope
中加上offline_access
,并且在Identity Provider也设置为Allow Offlien Access。
以Identity Server 6为例:
new Client
{
ClientName = "My App",
ClientId = "myangularapp",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowAccessTokensViaBrowser = true,
AllowOfflineAccess = true, // For refresh token
}
Unauthorized的Module和Component用来向客户显示错误信息。
首先创建Module:
ng g m pages\Unauthorized --routing
然后是Component:
ng g c pages\Unauthorized -m pages\unauthorized
可以在pages\unauthorized\unauthorized.html
中填充显示给终端客户的权限检查失败的信息。
譬如:
<h1>You are not unauthorized to access</h1>
更新Unauthorized Module中的路由(即文件unauthorized-routing.module.ts
)来添加标准跳转:
const routes: Routes = [{
path: '', component: UnauthorizedComponent
}];
在Angular程序中添加路由,用来支持跳转到上述刚刚创建的unauthorized的页面。
通常,在app-routing.module.ts
中添加路由项:
{ path: 'unauthorized', loadChildren: () => import('./pages/unauthorized/unauthorized.module').then(m => m.UnauthorizedModule) },
这时,在Angular SPA程序中的路由unauthorized
已经添加完成。
对需要Authorization的路由添加保护:
@Injectable({
providedIn: 'root'
})
export class AuthGuardService implements CanActivate {
constructor(private authService: OidcSecurityService, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const url: string = state.url;
return firstValueFrom(this.checkLogin(url));
}
checkLogin(url: string): Observable<boolean> {
return this.authService.isAuthenticated().pipe(map((rst: boolean) => {
if (!rst) {
this.authService.authorize();
}
return true;
}));
}
}
更新路由项:
{
path: 'protected-path',
canActivate: [AuthGuardService],
loadChildren: () => import('./pages/protected-path/protected-path.module').then(m => m.ProtectedPathModule),
},
登录(Login)和登出(Logout)操作一般放在主Component中进行,即,通常都是app.component.ts
中:
在构造函数中添加:
constructor(public oidcSecurityService: OidcSecurityService,) {
// Other codes...
}
添加登录函数:
public onLogon(): void {
this.oidcSecurityService.authorize();
}
登出函数:
public onLogon(): void {
this.oidcSecurityService.logoffAndRevokeTokens().subscribe();
}
通常在ngOnInit中添加相应Subscription来接受Logon的回调:
ngOnInit(): void {
this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData, accessToken, idToken }) => {
if (isAuthenticated) {
this.oidcSecurityService.getUserData().subscribe(val => {
this.currentUser = `${val.name}(${val.sub})`;
});
}
});
}
如果申请到的Access Token是用来访问被保护的API,那么Access Token就需要传给对应的API(authService
也是注入在Constructor
中的OidcSecurityService
的实例):
return this.authService.isAuthenticated().pipe(mergeMap(islogin => {
if (!islogin) {
return of({totalCount: 0, items: []});
}
let headers: HttpHeaders = new HttpHeaders();
headers = headers.append(this.contentType, this.appJson)
.append(this.strAccept, this.appJson);
let params: HttpParams = new HttpParams();
params = params.append('$top', top.toString());
params = params.append('$skip', skip.toString());
return this.authService.getAccessToken().pipe(mergeMap(token => {
headers = headers.append('Authorization', 'Bearer ' + token);
return this.http.get(apiurl, {
headers,
params,
})
.pipe(map(response => {
// Success received the response
return {
items
};
}),
catchError((error: HttpErrorResponse) => throwError(() => new Error(error.statusText + '; ' + error.error + '; ' + error.message))));
}));
}));