神刀安全网

Authentication in Angular 2

Most of the applications we build require some kind of authentication. In this tutorial I’ll show you how to build a simple application that uses routing and authentication. We will build a service that handles HTTP calls and stores JWT authentication tokens on the client to restrict access to pages and attach the token to authenticated HTTP calls.

The tutorial will only cover the frontend concepts, we will assume a backend exists. If you are not familiar with JWT tokens I would suggest reading this introduction first .

The setup

Our application will consist of 3 components associated with a route. The first one is the public homepage with static content. We need another component for logging in with given credentials. And last a component that is only available to those who already logged in.

// app.component.ts
import
{ Component } from 'angular2/core';
import { RouteConfig, RouterOutlet } from 'angular2/router';
import { HomeComponent } from './home.component';
import { LoginComponent } from './login.component';
import { ProfileComponent } from './profile.component';

@Component({
selector: 'app',
directives: [RouterOutlet],
template: `
<div class="container body-container">
<router-outlet></router-outlet>
</div>
`
})
@RouteConfig([
{ path: '/', component: HomeComponent, name: 'Home', useAsDefault: true },
{ path: '/login', component: LoginComponent, name: 'Login' },
{ path: '/profile', component: ProfileComponent, name: 'Profile' }
])
export class AppComponent { }

Here is our AppComponent which will be passed to the bootstrap function. With @RouteConfig decorator we can tell the application that these routes exist with given urls and components. It is important to give them names, because later we will use them as references redirecting to another page.

To make routing work one more thing is needed, the RouterOutlet directive. This will be the place where Angular renders the current components output based on the url.

Authentication

Our application works fine, but everyone can access every page. We need to get an authentication token to restrict it. This logic can be put into a service and become available to every part of the application through dependency injection. If you don’t know how dependency injection works in Angular 2 there is a very nice article on the Thoughtram blog .

// user.service.ts
import
{ Injectable } from 'angular2/core';
import { Http, Headers } from 'angular2/http';
import localStorage from 'localStorage';

@Injectable()
export class UserService {
private loggedIn = false;

constructor(private http: Http) {
this.loggedIn = !!localStorage.getItem('auth_token');
}

login(email, password) {
let headers = new Headers();
headers.append('Content-Type', 'application/json');

return this.http
.post(
'/login',
JSON.stringify({ email, password }),
{ headers }
)
.map(res => res.json())
.map((res) => {
if (res.success) {
localStorage.setItem('auth_token', res.auth_token);
this.loggedIn = true;
}

return res.success;
});
}

logout() {
localStorage.removeItem('auth_token');
this.loggedIn = false;
}

isLoggedIn() {
return this.loggedIn;
}
}

Our UserService consists of 3 main methods. The first is the login to authenticate with an email address and a password. We will use it in the login component and based on it’s result redirect to the home page and store the received token from the server. The isLoggedIn method will be important when we restrict access to the profile page, showing the current authentication state.

The UserService needs the @Injectable decorator to access the Http service and with it send the login credentials (email, password) to the server ( /login ). By default the content type is plain/text and we need to set it with the help of Headers to application/json . Listening to the response of a HTTP call is a bit different from Angular 1. We get an RxJS observable object instead of a promise. Just as with promises we can listen to it’s result, the subscribe method will take the place of the promise’s then method.

We won’t simply pass the raw response to the components, we will transform it to a boolean value and while doing it, check it’s result. The backend service generates a unique token for us, what we can use for authentication of our requests. If the backend process is successful, we store the authentication token in LocalStorage and save the state in the service to the loggedIn property.

// login.component.ts
import
{ Component } from 'angular2/core';
import { Router } from 'angular2/router';
import { UserService } from './user.service';

@Component({
selector: 'login',
template: `...`
})
export class LoginComponent {
constructor(
private userService: UserService,
private router: Router
) { }

onSubmit(email, password) {
this.userService.login(email, password).subscribe((result) => {
if (result) {
this.router.navigate(['Home']);
}
});
}
}

In our LoginComponent we listen to the result of the login and after a successful login, we redirect the user to the home page. As you can see we reference the route by it’s @RouteConfig name declared before.

Restricting access

Now that we are able to log in, it is time to restrict access to the profile page only to logged in users. To accomplish this we will take a look at our RouterOutlet .

When Angular loads the component of the route the RouterOutlet ’s activate function is called with the actual Instruction . The Instruction describes information for the router how to transition to the next component. From it we can extract the current url and based on it redirect the user when trying to access a restricted page without logging in.

import { ElementRef, DynamicComponentLoader, AttributeMetadata, Directive, Attribute } from 'angular2/core';
import { Router, RouterOutlet, ComponentInstruction } from 'angular2/router';
import { UserService } from './user.service';

@Directive({
selector: 'router-outlet'
})
export class LoggedInRouterOutlet extends RouterOutlet {
publicRoutes: Array;
private parentRouter: Router;
private userService: UserService;

constructor(
_elementRef: ElementRef, _loader: DynamicComponentLoader,
_parentRouter: Router, @Attribute('name') nameAttr: string,
private userService: UserService
) {
super(_elementRef, _loader, _parentRouter, nameAttr);

this.router = _parentRouter;
this.publicRoutes = [
'', 'login', 'signup'
];
}

activate(instruction: ComponentInstruction) {
if (this._canActivate(instruction.urlPath)) {
return super.activate(instruction);
}

this.router.navigate(['Login']);
}

_canActivate(url) {
return this.publicRoutes.indexOf(url) !== -1
|| this.userService.isLoggedIn();
}
}

When extending the built in RouterOutlet we can extend it’s constructor with one additional parameter, the UserService . It can provide whether the user is logged in and combining it with the list of public urls we can decide which navigations are allowed in the _canActivate method.

When we navigate in our application the next component gets instantiated and the activate method of the RouterOutlet gets called with the current ComponentInstruction . From the instruction we can easily get the current url and based on it decide if we should redirect the user to the login page. If everything is okay we just pass the instruction to the parent method and the application displays the component.

And with this step we secured every non public page in the application, no need to manually add it to every restricted component. Just need to pass our new class instead of the built-in one.

// app.component.ts
@Component({
selector: 'app',
directives: [LoggedInRouterOutlet],
template: `
<div class="container body-container">
<router-outlet></router-outlet>
</div>
`
})
@RouteConfig(...)
export class AppComponent { }

The only thing can be a bit weird of this approach is depending on an array of urls instead of lifecycle events or route configuration.

To see this solution in action check this Github repository .

Restricting again

Angular gives us another way of restricting access to page components and it can be accomplished with the @CanActivate router lifecycle decorator. Before activating the component the function passed to the @CanActivate decorator gets resolved by the router (can return promises also) and if the return value is false, the component won’t get activated.

// user.service.ts
export function
isLoggedIn() {
return !!storage.getAuthToken();
}
// profile.component.ts
import
{ Component } from 'angular2/core';
import { CanActivate } from 'angular2/router';
import { isLoggedIn } from './user.service';

@Component({
selector: 'profile',
template: `...`
})
@CanActivate(isLoggedIn)
export class ProfileComponent { }

This way we have to manually decorate the components we want to restrict, but it can be added not only to page level components but subcomponents also.

A serious drawback of this feature for now in version beta.8 is that dependency injection is not available inside this function. We can’t access the UserService or the Router to redirect the user. There is also an ongoing issue for this on Github .

To access the DI we need a bit of hack for this to work. When we bootstrap our application it returns a promise which will be resolved with the application’s DI injector and through this if we cache it, we can access the objects inside it.

// boot.ts
import { HTTP_PROVIDERS } from 'angular2/http';
import { bootstrap } from 'angular2/platform/browser';
import { provide, ComponentRef } from 'angular2/core';
import { ROUTER_PROVIDERS, LocationStrategy, HashLocationStrategy } from 'angular2/router';
import { AppComponent } from './app.component';
import { UserService } from './user.service';
import { appInjector } from './app-injector';

bootstrap(AppComponent, [
UserService,
HTTP_PROVIDERS,
ROUTER_PROVIDERS,
provide(LocationStrategy, { useClass: HashLocationStrategy })
]).then((appRef: ComponentRef) => {
// store a reference to the application injector
appInjector(appRef.injector);
});
// app-injector.ts
import
{ Injector } from 'angular2/core';

let appInjectorRef: Injector;
export const appInjector = (injector?: Injector):Injector => {
if (injector) {
appInjectorRef = injector;
}

return appInjectorRef;
};

After this bootstrap we can import the appInjector inside the isLoggedIn function and redirect, when the user is not logged in as we did when extending the RouterOutlet . A working application is available with this solution in this Plunkr .

Note: The @CanActivate lifecycle hook can also be used with extension, thanks toTamás Csaba for the tip.

import { appInjector } from './app-injector';
import { UserService } from './user.service';
import { Router } from 'angular2/router';
import { makeDecorator } from 'angular2/src/core/util/decorators';
import { CanActivate as CanActivateMetadata } from 'angular2/src/router/lifecycle_annotations_impl';

class PrivateMetadata extends CanActivateMetadata {
constructor() {
super((next, prev) => {
let injector = appInjector();
if (!injector.get(UserService).isLoggedIn()) {
injector.get(Router).navigate(['Login']);
return false;
}
return true;
});
}
}
export const CanActivatePrivate = makeDecorator(PrivateMetadata);

It is important to note that the solutions don’t play well with each other. The reason is when using @CanActive decorator and it resolves false, the activate method of the RouterOutlet won’t be called.

Final step

The non public pages are now restricted on the client with one of the solutions. The one thing that remains is to send authenticated requests to the server.

import { Injectable } from 'angular2/core';
import { Http, Headers } from 'angular2/http';
import localStorage from 'localStorage';

@Injectable()
export class ProfileService {
constructor(private http: Http) { }

getProfile() {
let headers = new Headers();
headers.append('Content-Type', 'application/json');
let authToken = localStorage.getItem('auth_token');
headers.append('Authorization', `Bearer ${authToken}`);

return this.http
.get('/profile', { headers: headers })
.map(res => res.json());
}
}

We are doing nearly the same we did with the UserService . Add the @Injectable decorator, pass in the HttpService and call the endpoint. The difference is that we add our authentication token we stored before in the UserService and send it in the Authorization header. With it the backend can check our identity, authenticate us and provide the content we asked for. Otherwise we would get a 401 Unauthorized error message.

Wrapping up

With some simple steps we authenticated our users, restricted access to pages and sent authenticated requests to the server back. Either way we go for restricting access to pages, both works, but till dependency injection is solved for the @CanActivate decorator I would go with extending the RouterOutlet .

Further reading

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Authentication in Angular 2

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址