如何在 Angular 8 应用程序中添加移动访问令牌?

5
我已将我的应用程序部署到UAT,但无法在移动设备上运行我的应用程序,因为它直接进入了访问被拒绝(401)页面。我认为这是由于访问令牌问题引起的。
我主要有两个拦截器来处理我的应用程序。 1.错误拦截器 - 处理任何路由错误或未经授权的错误。 2.jwt - 分配令牌,内部调用身份验证服务以获取令牌。
以下是我的服务文件,在其中我尝试通过发送postMessage来从会话、本地或窗口事件中获取访问令牌,表明我已准备好接收令牌。
authentication-service.ts
public getToken() {
    let accessToken = null;
     const auth = JSON.parse(sessionStorage.getItem('auth'));
    if (auth) {
        accessToken = auth.access_token;
    } elseif (accessToken == null || accessToken === undefined) {
        accessToken = localStorage.getItem('access_token');
    }

 window.addEventListener('message', function(event){
      // this event should have all the necessary tokens
 }, false);

 // once my page is loaded to indicate that I am ready to receive the message from server side.
  parent.postMessage({ askForToken:"true"}, "*");

  return accessToken;
}

我正在发送 parent.postMessage,以使 window.addEventListener 检索数据,但是该事件未按预期发送令牌。
我在 authentication.service.ts 中实现了上述所有代码,但我不确定这是否是正确的方式。
请有人建议我正确实现此代码并适当接收令牌吗?
请纠正我,因为我是第一次尝试使用令牌进行部署。

你可以再详细描述一下你的应用程序流程吗?基本上它将会有一个登录表单。如果您成功登录系统,您将获得一个令牌并将其存储在localStorage中,就像我怀疑的那样。 在每个后续请求中,您都会检查该令牌并将其与每个HTTP请求的标头一起发送? 重定向用户到401页面的代码在哪里?这是在你的守卫内吗? - trungk18
@trungk18 是的,重定向到401是在错误拦截器守卫中。关于流程 - 当我登录时,我有一个主页面,从主页面,我有一个链接打开一个子应用程序。当子应用程序即将加载时,我调用auth服务向父窗口(即主页面)发送消息 - 从这个父窗口,我应该得到一个access_token。 - Onera
当您从主应用程序导航到子应用程序时,问题就出现了。您检查了令牌,它仍然有效吗?在我的应用程序中,我通过将令牌存储在localStore中并拥有AuthService来管理所有这些内容。但是,当您收到401错误时,有一件事是肯定的,您应该将用户重定向到登录页面,而不是401页面。有许多用例需要强制用户重新登录并使所有令牌失效。 - trungk18
许多子应用程序只有一个登录入口点。主页团队建议发送一个名为“askForToken”的postMessage,以便他们可以向我发送access_token,我需要使用window.addEventListener来获取event.data.access_token。 - Onera
请问您能否确认,所有上述的代码是否都应该写在认证服务中,还是应该在我的应用程序的其他位置进行提及? - Onera
显示剩余3条评论
1个回答

6

源代码和演示:

https://github.com/trungk18/angular-authentication-demo-with-lazy-loading

https://stackblitz.com/edit/angular-authentication-demo-with-lazy-loading

我将添加一个节以在我们第一次运行时实现所有其他模块的延迟加载。这意味着只有登录页面将在第一次加载时被加载。登录后,下一个模块将被加载。这将为我们节省大量带宽。


有一个用户,并且我会在成功登录后将整个对象存储到localStorage中。

流程可能是这样的:

  1. 打开应用程序

  2. AuthGuard 将被触发。如果在 localStorage 中存在带令牌的用户对象,则激活该路由。否则,将路由返回到登录页面。

  3. 当路由被激活并开始向服务器发出 API 请求时,JWTInterceptor 将被触发以在每个后续请求上发送令牌。

  4. ErrorInterceptor 检查是否为 401,如果是,则从 localStorage 中删除用户并重新加载页面。这个处理更多地关注于某人试图手动使用不同的令牌或对象更新 localStorage 的用例。如果令牌正确且没有来自服务器的修改器,则不应发生。


模型

export const ConstValue = { 
    ReturnUrl: "returnUrl",
    CurrentUser: "currentUser",    
}

export const ConstRoutingValue = {
    Login: "login"
}

export interface AICreateUser {
    firstName: string;
    lastName: string;
    email: string;
    password: string;    
    roleIds: string[]
}

export interface PartnerUser extends AICreateUser {
    id: string;    
    createdAt: string;    
    token: string;    
    featureSet: string[]
}

export interface AuthDto {
    email: string;
    password: string;
}

auth.service

export class AuthService {
    private _currentUserSubject: BehaviorSubject<User>;
    public currentUser: Observable<User>;

    public get currentUserVal(): User {
        return this._currentUserSubject.value;
    }

    get currentUserToken() {
        return this.currentUserVal.token;
    }

    constructor(private http: HttpClient) {
        this._currentUserSubject = new BehaviorSubject<User>(this.getUserFromLocalStorage());
        this.currentUser = this._currentUserSubject.asObservable();
    }

    login(username, password) {
        return this.http.post<any>(`/users/authenticate`, { username, password })
            .pipe(map(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem(ConstValue.CurrentUser, JSON.stringify(user));
                this._currentUserSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage and set current user to null
        localStorage.removeItem(ConstValue.CurrentUser);
        this._currentUserSubject.next(null);
    }

    private getUserFromLocalStorage(): User {
        try {
          return JSON.parse(localStorage.getItem(ConstValue.CurrentUser)!);
        } catch (error) {
          return null!;
        }
      }
}

还有一个JwtInterceptor,可以在每个请求中将令牌附加到头文件中。

export class JwtInterceptor implements HttpInterceptor {
  constructor(private authenticationService: AuthService) {}

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    let currentUser = this.authenticationService.currentUserVal;
    if (currentUser && currentUser.token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${currentUser.token}`
        }
      });
    }

    return next.handle(request);
  }
}

export const JWTInterceptorProvider = {
  provide: HTTP_INTERCEPTORS,
  useClass: JwtInterceptor,
  multi: true
};

定义一个ErrorInterceptor拦截器,检查是否存在401错误码,如果是则从localStorage中删除用户信息并重新加载页面。

export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthService) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if (err.status === 401) {
                // auto logout if 401 response returned from api
                this.authenticationService.logout();
                location.reload(true);
            }
            
            const error = err.error.message || err.statusText;
            return throwError(error);
        }))
    }
}

export const ErrorInterceptorProvider = { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }

还有一个AuthGuard,用于确保在激活路由之前您拥有令牌。 在配置Angular路由器时必须包含它在所有路由中,除了登录页面。

export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private authenticationService: AuthService
    ) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const currentUserToken = this.authenticationService.currentUserToken;
        if (currentUserToken) {
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { [ConstValue.ReturnUrl]: state.url }});
        return false;
    }
}


如果我想使用用户对象,我将在AuthService中获取currentUser的公共可观察对象。例如,我想在列表上显示用户的名字。
export class AppComponent {
    currentUser: User;

    constructor(
        private router: Router,
        private authenticationService: AuthService
    ) {
        this.authenticationService.currentUser.subscribe(
            x => (this.currentUser = x)
        );
    }

    logout() {
        this.authenticationService.logout();
        this.router.navigate(["/login"]);
    }
}

我希望你从中得到了想法。


谢谢您的回复,我能够部分理解,但我需要看到一个正确的流程如何传播。由于我是初学者,我请求您给出一个演示,以便我完全理解。感谢您的热情支持。请在您方便的时候分享一个演示,我可以等待。 - Onera
我正在准备中,希望今天能够给你。 - trungk18
1
我添加了 Github 存储库,你可以下载并在你的机器上运行。 - trungk18
不客气。如果您有任何问题,请在此处告诉我,或者直接在存储库上创建一个问题。 - trungk18

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接