Authentication
In a real world application, it should provide login, registration and logout features for users, and also can identify if a user has the roles or permissions to access the protected resources.I have set adding post and editing post requires authentication in backend APIs. And it uses a JWT token based authentication to authorize users.
In this client, we
use window.localStorage
to store the JWT token, it is easy to read and restore authentication without new login.Create a
JWT
service to wrap read and write localStorage actions.class JWT {
constructor(AppConstants, $window) {
'ngInject';
this._AppConstants = AppConstants;
this._$window = $window;
}
save(token) {
this._$window.localStorage[this._AppConstants.jwtKey] = token;
}
get() {
return this._$window.localStorage[this._AppConstants.jwtKey];
}
destroy() {
this._$window.localStorage.removeItem(this._AppConstants.jwtKey);
}
}
export default JWT;
/auth/login
, /auth/signup
for login and registration.Logout is no need extra operation on server side. We are using stateless service, there is state need to clean.
Create an
Auth
service to wrap these operations.class Auth {
constructor(JWT, AppConstants, $http, $state, $q) {
'ngInject';
this._JWT = JWT;
this._AppConstants = AppConstants;
this._$http = $http;
this._$state = $state;
this._$q = $q;
this.current = null;
}
attempAuth(type, credentials) {
let path = (type == 'signin') ? '/login' : '/signup';
let request = {
url: this._AppConstants.api + '/auth' + path,
method: 'POST',
data: credentials
};
return this._$http(request)
.then((res) => {
this._JWT.save(res.data.id_token);
this.current = res.data.user;
return res;
});
}
ensureAuthIs(b) {
let deferred = this._$q.defer();
this.verifyAuth().then((authValid) => {
// if it's the opposite, redirect home
if (authValid !== b) {
this._$state.go('app.signin');
deferred.resolve(false);
} else {
deferred.resolve(true);
}
});
return deferred.promise;
}
verifyAuth() {
let deferred = this._$q.defer();
if (!this._JWT.get()) {
deferred.resolve(false);
return deferred.promise;
}
if (this.current) {
deferred.resolve(true);
} else {
this._$http({
url: this._AppConstants.api + '/me',
method: 'GET'
})
.then(
(res) => {
this.current = res.data;
deferred.resolve(true);
},
(err) => {
this._JWT.destroy();
deferred.resolve(false);
}
);
}
return deferred.promise;
}
logout() {
this.current = null;
this._JWT.destroy();
this._$state.go(this._$state.$current, null, { refresh: true });
}
}
export default Auth;
attempAuth
is responsive for signin and signup action, use a type to identify them. The
verifyAuth
and ensureAuthIs
are use for check user authentication status and make sure user is authenticated.We have generated signin and signup component skeleton codes for this application.
Let's implements signin firstly.
signin.controller.js:
class SigninController {
constructor(Auth, $state, toastr) {
'ngInject';
this._Auth = Auth;
this._$state = $state;
this._toastr = toastr;
this.name = 'signin';
this.data = { username: '', password: '' };
}
signin() {
console.log("signin with credentials:" + this.data);
this._Auth.attempAuth('signin', this.data)
.then((res) => {
this._toastr.success('Welcome back,' + this.data.username);
this._$state.go('app.posts');
});
}
}
export default SigninController;
signin
method, when Auth.attempAuth
is called successfully, then use angular-toastr to raise a notification and route to app.posts
state.signin.html:
<div class="row">
<div class="offset-md-3 col-md-6">
<div class="card">
<div class="card-header">
<h1>{{ $ctrl.name }}</h1>
</div>
<div class="card-block">
<form id="form" name="form" class="form" ng-submit="$ctrl.signin()" novalidate>
<div class="form-group" ng-class="{'has-danger':form.username.$invalid && !form.username.$pristine}">
<label class="form-control-label" for="username">{{'username'}}</label>
<input class="form-control" id="username" name="username" ng-model="$ctrl.data.username" required/>
<div class="form-control-feedback" ng-messages="form.username.$error" ng-if="form.username.$invalid && !form.username.$pristine">
<p ng-message="required">Username is required</p>
</div>
</div>
<div class="form-group" ng-class="{'has-danger':form.password.$invalid && !form.password.$pristine}">
<label class="form-control-label" for="password">{{'password'}}</label>
<input class="form-control" type="password" name="password" id="password" ng-model="$ctrl.data.password" required/>
<div class="form-control-feedback" ng-messages="form.password.$error" ng-if="form.password.$invalid && !form.password.$pristine">
<p ng-message="required">Password is required</p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success btn-lg" ng-disabled="form.$invalid || form.$pending"> {{'SIGN IN'}}
</div>
</form>
</div>
<div class="card-footer">
Not registered, <a href="#" ui-sref="app.signup">{{'signup'}}</a>
</div>
</div>
</div>
</div>
Declare signin as an Angular module.
/components/signin/index.js:
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import commonSevices from '../../common/services/';
import signinComponent from './signin.component';
let signinModule = angular.module('signin', [
commonSevices,
uiRouter
])
.config(($stateProvider) => {
"ngInject";
$stateProvider
.state('app.signin', {
url: '/signin',
component: 'signin'
});
})
.component('signin', signinComponent)
.name;
export default signinModule;
ComponentsModule
.//...
import Signin from './signin/';
//...
let componentsModule = angular.module('app.components', [
//...
Signin,
//...
])
.name;
signup.controller.js:
class SignupController {
constructor(Auth, $state) {
'ngInject';
this._Auth = Auth;
this._$state = $state;
this.name = 'signup';
this.data = {
firstName: '',
lastName: '',
username: '',
password: ''
};
}
signup() {
console.log('sign up with data @' + this.data);
this._Auth.attempAuth('signup', this.data)
.then((res) => {
this._$state.go('app.posts');
});
}
}
export default SignupController;
<div class="row">
<div class="offset-md-3 col-md-6">
<div class="card">
<div class="card-header">
<h1>{{ $ctrl.name }}</h1>
</div>
<div class="card-block">
<form id="form" name="form" class="form" ng-submit="$ctrl.signup()" novalidate>
<div class="row">
<div class="col-md-6">
<div class="form-group" ng-class="{'has-danger':form.firstName.$invalid && !form.firstName.$pristine}">
<label class="form-control-label" for="firstName">{{'firstName'}}</label>
<input class="form-control" id="firstName" name="firstName" ng-model="$ctrl.data.firstName" required/>
<div class="form-control-feedback" ng-messages="form.firstName.$error" ng-if="form.firstName.$invalid && !form.firstName.$pristine">
<p ng-message="required">FirstName is required</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group" ng-class="{'has-danger':form.lastName.$invalid && !form.lastName.$pristine}">
<label class="form-control-label col-md-12" for="lastName">{{'lastName'}}</label>
<input class="form-control" id="lastName" name="lastName" ng-model="$ctrl.data.lastName" required/>
<div class="form-control-feedback" ng-messages="form.lastName.$error" ng-if="form.lastName.$invalid && !form.lastName.$pristine">
<p ng-message="required">LastName is required</p>
</div>
</div>
</div>
</div>
<div class="form-group" ng-class="{'has-danger':form.username.$invalid && !form.username.$pristine}">
<label class="form-control-label" for="username">{{'username'}}</label>
<input class="form-control" id="username" name="username" ng-model="$ctrl.data.username" required ng-minlength="6" ng-maxlength="20" />
<div class="form-control-feedback" ng-messages="form.username.$error" ng-if="form.username.$invalid && !form.username.$pristine">
<p ng-message="required">Username is required</p>
<p ng-message="minlength">Username is too short(at least 6 chars)</p>
<p ng-message="maxlength">Username is too long(at most 20 chars)</p>
</div>
</div>
<div class="form-group" ng-class="{'has-danger':form.password.$invalid && !form.password.$pristine}">
<label class="form-control-label" for="password">{{'password'}}</label>
<input class="form-control" type="password" name="password" id="password" ng-model="$ctrl.data.password" required ng-minlength="6" ng-maxlength="20" />
<div class="form-control-feedback" ng-messages="form.password.$error" ng-if="form.password.$invalid && !form.password.$pristine">
<p ng-message="required">Password is required.</p>
<p ng-message="minlength,maxlength">Password should be consist of 6 to 20 chars.</p>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success btn-lg" ng-disabled="form.$invalid || form.$pending"> {{'SIGN UP'}}
</div>
</form>
</div>
<div class="card-footer">
Already registered, <a href="#" ui-sref="app.signin">{{'signin'}}</a>
</div>
</div>
</div>
</div>
Declare the signup Angular Module.
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import commonSevices from '../../common/services/';
import signupComponent from './signup.component';
let signupModule = angular.module('signup', [
commonSevices,
uiRouter
])
.config(($stateProvider) => {
"ngInject";
$stateProvider
.state('app.signup', {
url: '/signup',
component: 'signup',
data: {
requiresAuth: false
}
});
})
.component('signup', signupComponent)
.name;
export default signupModule;
import Signup from './signup/';
//...
let componentsModule = angular.module('app.components', [
//...
Signup,
//...
])
.name;
$httpProvider
.app.config.js
function jwtInterceptor(JWT, AppConstants, $window, $q) {
'ngInject';
return {
// automatically attach Authorization header
request: function (config) {
if (/*config.url.indexOf(AppConstants.api) === 0 &&*/ JWT.get()) {
config.headers.Authorization = 'Bearer ' + JWT.get();
}
return config;
},
// Handle 401
responseError: function (rejection) {
if (rejection.status === 401) {
// clear any JWT token being stored
JWT.destroy();
// do a hard page refresh
$window.location.reload();
}
return $q.reject(rejection);
}
};
}
//...in AppConfig function
$httpProvider.interceptors.push(jwtInterceptor);
Next, we will try to protect the pages requires authentication, such as new-post and edit-post.
In the state definition, add a
requiresAuth
property in state data
to identify if a state should be authenticated.Add the following code to
app
state.$stateProvider
.state('app', {
abstract: true,
component: 'app',
data: {
requiresAuth: true
}
});
app
is the root component of the
component tree. Here we assume all component should be authenticated
before route to it. But the data attribute can be inherited and
overriden.Add the following codes to posts, post-details, signin, signup state definitions.
data: {
requiresAuth: true
}
Finally, observes the state change event in
AppRun
. //processing auth redirecting
$transitions.onStart({
to: (state) => {
return !!state.data.requiresAuth;
}
}, function (trans) {
var $state = trans.router.stateService;
var _Auth = trans.injector().get('Auth');
_Auth.ensureAuthIs(true);
});
Add signin, signup and logout button/links in navbar.html.
<ul class="nav navbar-nav pull-md-right">
<li class="nav-item" show-authed="false"><button class="btn btn-outline-success" ng-click="$ctrl.onSignin()">{{'signin'}}</button></li>
<li class="nav-item" show-authed="false"><a class="nav-link" href="#" ui-sref="app.signup">{{'signup'}}</a></span>
</li>
<li class="nav-item" show-authed="true"><button type="button" class="btn btn-outline-danger" ng-click="$ctrl.onLogout()">{{'logout'}}</button></span>
</li>
</ul>
onSignin
and onLogout
.class NavbarController {
constructor($scope) {
'ngInject';
this._$scope = $scope;
this.name = 'navbar';
}
$onInit() {
console.log("initializing NavbarController...");
}
$onDestroy() {
console.log("destroying NavbarController...");
}
onSignin() {
console.log("on signin...");
this._$scope.$emit("event:signinRequest");
}
onLogout() {
console.log("on logout...");
this._$scope.$emit("event:logoutRequest");
}
}
export default NavbarController;
$state
to route the target state. We use Angular event publisher/subcribers to archive the purpose.If you are writing the legacy Angular application, you could know well about the
$scope
.In Angular $scopes are treeable, there is a
$rootScope
of an application, and all $scope
s are inherited from it. Every $scope
has a $parent
property to access its parent scope, except $rootScope
.$scope
has two methods to fire an event.-
$scope.emit
will fire an event up the scope. -
$scope.broadcast
will fire an event down scope.
$scope.on
will observes events.We use
emit
in our case, and we can use $rootScope
to observe these events in AppRun
. $rootScope.$on("event:signinRequest", function (event, data) {
console.log("receviced:signinRequest");
$state.go('app.signin');
});
$rootScope.$on("event:logoutRequest", function (event, data) {
console.log("receviced:logoutRequest");
Auth.logout();
$state.go('app.signin');
});
show-authed
directive determines if show or hide button/links according to the authentication info.Have a look at the show-authed.directive.js under common/diretives/ folder.
function ShowAuthed(Auth) {
'ngInject';
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.Auth = Auth;
scope.$watch('Auth.current', function(val) {
// If user detected
if (val) {
if (attrs.showAuthed === 'true') {
element.css({ display: 'inherit'})
} else {
element.css({ display: 'none'})
}
// no user detected
} else {
if (attrs.showAuthed === 'true') {
element.css({ display: 'none'})
} else {
element.css({ display: 'inherit'})
}
}
});
}
};
}
export default ShowAuthed;
common/diretives/index.js:
import angular from 'angular';
import ShowAuthed from './show-authed.directive';
let directivesModule = angular.module('app.common.directives', [])
.directive('showAuthed', ShowAuthed)
.name;
export default directivesModule;
//...
import commonDirectivesModule from './directives';
let commonModule = angular.module('app.common', [
//...
commonDirectivesModule
])
//...
Check the sample codes.
评论