신원 인증은 종종 웹에서 적용될 수 있습니다. 이 기사에서는 신원 인증을 적용하는 AngularJS의 기술을 소개합니다.
신분증 인증
가장 일반적인 본인 인증 방법은 사용자 이름(또는 이메일)과 비밀번호를 사용하여 로그인하는 것입니다. 이는 사용자가 자신의 개인 정보를 사용하여 로그인할 수 있도록 로그인 양식을 구현하는 것을 의미합니다. 양식은 다음과 같습니다.
<form name="loginForm" ng-controller="LoginController" ng-submit="login(credentials)" novalidate> <label for="username">Username:</label> <input type="text" id="username" ng-model="credentials.username"> <label for="password">Password:</label> <input type="password" id="password" ng-model="credentials.password"> <button type="submit">Login</button> </form>
이 양식은 Angular 기반 양식이므로 양식을 업로드할 때 ngSubmit 지시어를 사용하여 함수를 트리거합니다. $scope.credentials 개체를 직접 사용하는 대신 업로드 양식 기능에 개인 정보를 전달한다는 점에 유의하세요. 이렇게 하면 함수의 단위 테스트가 더 쉬워지고 현재 컨트롤러 범위에 대한 함수의 결합이 줄어듭니다. 이 컨트롤러는 다음과 같습니다.
.controller('LoginController', function ($scope, $rootScope, AUTH_EVENTS, AuthService) { $scope.credentials = { username: '', password: '' }; $scope.login = function (credentials) { AuthService.login(credentials).then(function (user) { $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); $scope.setCurrentUser(user); }, function () { $rootScope.$broadcast(AUTH_EVENTS.loginFailed); }); };javascript:void(0); })
여기서는 실제 논리가 부족하다는 점을 발견했습니다. 이 컨트롤러는 양식에서 인증 논리를 분리하기 위해 이렇게 만들어졌습니다. 컨트롤러에서 가능한 한 많은 로직을 추출하여 모두 서비스에 넣는 것이 좋습니다. AngularJS의 컨트롤러는 지나치게 많은 작업을 수행하는 대신 $scope(관찰 또는 수동 작업을 사용하여)의 개체만 관리해야 합니다.
세션 변경 알림
신원 인증은 전체 애플리케이션 상태에 영향을 미칩니다. 이러한 이유로 나는 사용자 세션 변경 사항을 알리기 위해 이벤트($broadcast 사용)를 사용하는 것을 선호합니다. 가능한 모든 이벤트 코드를 중간 지점에서 정의하는 것이 좋습니다. 저는 이를 위해 상수를 사용하는 것을 좋아합니다:
.constant('AUTH_EVENTS', { loginSuccess: 'auth-login-success', loginFailed: 'auth-login-failed', logoutSuccess: 'auth-logout-success', sessionTimeout: 'auth-session-timeout', notAuthenticated: 'auth-not-authenticated', notAuthorized: 'auth-not-authorized' })
상수의 좋은 특징은 서비스처럼 다른 곳에 마음대로 주입할 수 있다는 것입니다. 그런 식으로요. 이를 통해 단위 테스트에서 상수를 쉽게 호출할 수 있습니다. 또한 상수를 사용하면 나중에 많은 파일을 변경하지 않고도 쉽게 이름을 바꿀 수 있습니다. 동일한 방법이 사용자 역할에도 적용됩니다.
.constant('USER_ROLES', { all: '*', admin: 'admin', editor: 'editor', guest: 'guest' })
편집자와 관리자에게 동일한 권한을 부여하려면 'editor'를 'admin'으로 변경하면 됩니다. '.
AuthService
신원 인증 및 권한 부여(액세스 제어)와 관련된 로직은 동일한 서비스에 배치하는 것이 가장 좋습니다.
.factory('AuthService', function ($http, Session) { var authService = {}; authService.login = function (credentials) { return $http .post('/login', credentials) .then(function (res) { Session.create(res.data.id, res.data.user.id, res.data.user.role); return res.data.user; }); }; authService.isAuthenticated = function () { return !!Session.userId; }; authService.isAuthorized = function (authorizedRoles) { if (!angular.isArray(authorizedRoles)) { authorizedRoles = [authorizedRoles]; } return (authService.isAuthenticated() && authorizedRoles.indexOf(Session.userRole) !== -1); }; return authService; })
신원 인증 문제에서 벗어나기 위해 다른 서비스(서비스 스타일을 사용하는 싱글톤 개체)를 사용하여 사용자의 세션 정보를 저장합니다. 세션 정보의 세부 사항은 백엔드 구현에 따라 다르지만 보다 일반적인 예를 들어 보겠습니다.
.service('Session', function () { this.create = function (sessionId, userId, userRole) { this.id = sessionId; this.userId = userId; this.userRole = userRole; }; this.destroy = function () { this.id = null; this.userId = null; this.userRole = null; }; return this; })
사용자가 로그인하면 정보는 특정 위치(예: 오른쪽 상단에 있는 사용자 아바타)에 표시되어야 합니다. 이를 달성하려면 사용자 개체는 $scope 개체(전역적으로 호출할 수 있는 개체)에 의해 참조되어야 합니다. $rootScope가 확실한 첫 번째 선택이기는 하지만 $rootScope를 너무 많이 사용하지 않으려고 노력합니다(실제로 전역 이벤트 브로드캐스트에는 $rootScope만 사용합니다). 내가 선호하는 방식은 애플리케이션의 루트 노드나 적어도 DOM 트리보다 높은 위치에 컨트롤러를 정의하는 것입니다. 태그는 좋은 선택입니다.
<body ng-controller="ApplicationController"> ... </body>
ApplicationController는 애플리케이션의 전역 논리를 위한 컨테이너이자 Angular의 실행 메서드를 실행하기 위한 옵션입니다. 따라서 이는 $scope 트리의 루트에 있게 되며 다른 모든 범위는 이로부터 상속됩니다(격리 범위 제외). 이것은 currentUser 객체를 정의하기에 좋은 장소입니다:
.controller('ApplicationController', function ($scope, USER_ROLES, AuthService) { $scope.currentUser = null; $scope.userRoles = USER_ROLES; $scope.isAuthorized = AuthService.isAuthorized; $scope.setCurrentUser = function (user) { $scope.currentUser = user; }; })
실제로 currentUser 객체를 할당하지 않고 범위의 속성만 초기화합니다. currentUser가 나중에 액세스될 수 있도록 합니다. 안타깝게도 하위 범위의 currentUser에 새 값을 할당할 수는 없습니다. 그렇게 하면 섀도우 속성이 생성되기 때문입니다. 이는 기본 유형(문자열, 숫자, 부울, 정의되지 않음 및 null)을 참조 대신 값으로 전달한 결과입니다. 그림자 속성을 방지하려면 setter 함수를 사용해야 합니다. Angular 범위와 프로토타입 상속에 대해 자세히 알아보려면 범위 이해를 읽어보세요.
액세스 제어
신원 인증, 즉 액세스 제어는 실제로 AngularJS에 존재하지 않습니다. 우리는 클라이언트 애플리케이션이기 때문에 모든 소스 코드는 사용자의 손에 있습니다. 사용자가 인증된 인터페이스를 얻기 위해 코드를 변조하는 것을 방지할 수 있는 방법은 없습니다. 우리가 할 수 있는 일은 컨트롤을 보여주는 것뿐입니다. 실제 인증이 필요한 경우 서버 측에서 인증을 수행해야 하지만 이는 이 문서의 범위를 벗어납니다.
요소 표시 제한
AngularJS에는 범위나 표현식에 따라 요소 표시 또는 숨기기를 제어하는 지시어(ngShow, ngHide, ngIf 및 ngSwitch)가 있습니다. 처음 두 개는 c9ccee2e6ea535a969eb3f532ad9fe89 속성을 사용하여 요소를 숨기지만, 마지막 두 개는 DOM에서 요소를 제거합니다.
第一种方式,也就是隐藏元素,最好用于表达式频繁改变并且没有包含过多的模板逻辑和作用域引用的元素上。原因是在隐藏的元素里,这些元素的模板逻辑仍然会在每个 digest 循环里重新计算,使得应用性能下降。第二种方式,移除元素,也会移除所有在这个元素上的 handler 和作用域绑定。改变 DOM 对于浏览器来说是很大工作量的(在某些场景,和 ngShow/ngHide 对比),但是在很多时候这种代价是值得的。因为用户访问信息不会经常改变,使用 ngIf 或 ngShow 是最好的选择:
<div ng-if="currentUser">Welcome, {{ currentUser.name }}</div> <div ng-if="isAuthorized(userRoles.admin)">You're admin.</div> <div ng-switch on="currentUser.role"> <div ng-switch-when="userRoles.admin">You're admin.</div> <div ng-switch-when="userRoles.editor">You're editor.</div> <div ng-switch-default>You're something else.</div> </div>
限制路由访问
很多时候你会想让整个网页都不能被访问,而不是仅仅隐藏一个元素。如果可以再路由(在UI Router 里,路由也叫状态)使用一种自定义的数据结构,我们就可以明确哪些用户角色可以被允许访问哪些内容。下面这个例子使用 UI Router 的风格,但是这些同样适用于 ngRoute。
.config(function ($stateProvider, USER_ROLES) { $stateProvider.state('dashboard', { url: '/dashboard', templateUrl: 'dashboard/index.html', data: { authorizedRoles: [USER_ROLES.admin, USER_ROLES.editor] } }); })
下一步,我们需要检查每次路由变化(就是用户跳转到其他页面的时候)。这需要监听 $routeChangStart(ngRoute 里的)或者 $stateChangeStart(UI Router 里的)事件:
.run(function ($rootScope, AUTH_EVENTS, AuthService) { $rootScope.$on('$stateChangeStart', function (event, next) { var authorizedRoles = next.data.authorizedRoles; if (!AuthService.isAuthorized(authorizedRoles)) { event.preventDefault(); if (AuthService.isAuthenticated()) { // user is not allowed $rootScope.$broadcast(AUTH_EVENTS.notAuthorized); } else { // user is not logged in $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated); } } }); })
Session 时效
身份认证多半是服务器端的事情。无论你用什么实现方式,你的后端会对用户信息做真正的验证和处理诸如 Session 时效和访问控制的处理。这意味着你的 API 会有时返回一些认证错误。标准的错误码就是 HTTP 状态吗。普遍使用这些错误码:
401 Unauthorized — The user is not logged in
403 Forbidden — The user is logged in but isn't allowed access
419 Authentication Timeout (non standard) — Session has expired
440 Login Timeout (Microsoft only) — Session has expired
后两种不是标准内容,但是可能广泛应用。最好的官方的判断 session 过期的错误码是 401。无论怎样,你的登陆对话框都应该在 API 返回 401, 419, 440 或者 403 的时候马上显示出来。总的来说,我们想广播和基于这些 HTTP 返回码的时间,为此我们在 $httpProvider 增加一个拦截器:
.config(function ($httpProvider) { $httpProvider.interceptors.push([ '$injector', function ($injector) { return $injector.get('AuthInterceptor'); } ]); }) .factory('AuthInterceptor', function ($rootScope, $q, AUTH_EVENTS) { return { responseError: function (response) { $rootScope.$broadcast({ 401: AUTH_EVENTS.notAuthenticated, 403: AUTH_EVENTS.notAuthorized, 419: AUTH_EVENTS.sessionTimeout, 440: AUTH_EVENTS.sessionTimeout }[response.status], response); return $q.reject(response); } }; })
这只是一个认证拦截器的简单实现。有个很棒的项目在 Github ,它做了相同的事情,并且使用了 httpBuffer 服务。当返回 HTTP 错误码时,它会阻止用户进一步的请求,直到用户再次登录,然后继续这个请求。
登录对话框指令
当一个 session 过期了,我们需要用户重新进入他的账号。为了防止他丢失他当前的工作,最好的方法就是弹出登录登录对话框,而不是跳转到登录页面。这个对话框需要监听 notAuthenticated 和 sessionTimeout 事件,所以当其中一个事件被触发了,对话框就要打开:
.directive('loginDialog', function (AUTH_EVENTS) { return { restrict: 'A', template: '<div ng-if="visible" ng-include="\'login-form.html\'">', link: function (scope) { var showDialog = function () { scope.visible = true; }; scope.visible = false; scope.$on(AUTH_EVENTS.notAuthenticated, showDialog); scope.$on(AUTH_EVENTS.sessionTimeout, showDialog) } }; })
只要你喜欢,这个对话框可以随便扩展。主要的思想是重用已存在的登陆表单模板和 LoginController。你需要在每个页面写上如下的代码:
<div login-dialog ng-if="!isLoginPage"></div>
注意 isLoginPage 检查。一个失败了的登陆会触发 notAuthenticated 时间,但我们不想在登陆页面显示这个对话框,因为这很多余和奇怪。这就是为什么我们不把登陆对话框也放在登陆页面的原因。所以在 ApplicationController 里定义一个 $scope.isLoginPage 是合理的。
保存用户状态
在用户刷新他们的页面,依旧保存已登陆的用户信息是单页应用认证里面狡猾的一个环节。因为所有状态都存在客户端,刷新会清空用户信息。为了修复这个问题,我通常实现一个会返回已登陆的当前用户的数据的 API (比如 /profile),这个 API 会在 AngularJS 应用启动(比如在 “run” 函数)。然后用户数据会被保存在 Session 服务或者 $rootScope,就像用户已经登陆后的状态。或者,你可以把用户数据直接嵌入到 index.html,这样就不用额外的请求了。第三种方式就是把用户数据存在 cookie 或者 LocalStorage,但这会使得登出或者清空用户数据变得困难一点。