用 Flask 和 AngularJs 实现HTTP认证

最近接触到angularjs这个前端框架,也学习到了用restful API的方式进行前后端的交互,在接下来的一段时间中边学习边总结这段时间的学习成果。

要实现登录认证功能,常用的有以下这两种方法:

  1. Basic access authentication
  2. Digest access authentication

这两种方法各有优劣,

  • Basic access authentication 主要是胜在简单,只需要在Requset Header中加入 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==,其中字符串”Basic “后面的是”username:password”字符串的Base64编码,如果验证不成功,server会在Response Header中返回WWW-Authenticate: Basic realm="domain",状态码为401。但是这个方法不安全,在生产环境中须要在HTTPS下使用。但也因为他够简单,我们可以在开发测试环境中使用。
  • Digest access authentication 顾名思义,主要是通过server端返回的随机码和摘要算法对username和password、realm以及需要发送的请求内容提取摘要,发送给server,该方法相比Basic access authentication来说稍稍复杂,但是却有较强的安全性。

为简单和实用性,本文实现第一种方法,HTTP Basic Auth,主要用到以下插件:

  1. front-end

    AngularJs
    AngularJs-storage
    Bootstrap

  2. back-end

    Flask==0.10.1
    Flask-HTTPAuth==2.5.0
    Flask-SQLAlchemy==2.0
    Flask-Script==2.0.5
    Jinja2==2.8
    MarkupSafe==0.23
    SQLAlchemy==1.0.8
    Werkzeug==0.10.4
    itsdangerous==0.24

angularJs 的 AuthService

AuthService 是我们自己定义的一个service,提供核心的认证功能,包括登录、登出、以及用token认证,关于token,需要强调的是,我们第一次登录的时候使用用户名和密码,server端返回一个token,以后进行认证的时候就使用token,以减少用户名和密码在网上传输的次数。token有使用时间限制,一般是几个小时之内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
myApp.service('AuthService', function($q, $http){
var self = this;
self.profile = null;
self.isAuthenticated = false;

function request(url, username_or_token, password) {
return {
method: 'GET',
url: url,
headers: {
Accept: 'application/json',
Authorization: 'Basic ' + btoa(username_or_token + ':' + password)
}
};
}

self.signin = function (options, successCallback, errorCallback) {
var req = request('/auth/login', options.username, options.password);
$http(req).
then(function(response) {
var token = response.data.token;
if (token) {
self.isAuthenticated = true;
self.profile = response.data.profile;
successCallback(token);
}

}, function(error) {
console.log(error.message);
errorCallback();
}
);

};

self.signout = function () {
self.isAuthenticated = false;
self.profile = null;
};

self.authenticate = function (token) {
var req = request('/auth/login_with_token', token, '');
var deferred = $q.defer();
$http(req)
.then(function (response) {
self.profile = response.data.profile;
self.isAuthenticated = true;
deferred.resolve();
}, function () {
self.isAuthenticated = false;
deferred.reject();
});
return deferred.promise;
}
});
  1. signin

    首先用requst函数把username:password的base64编码加入http 请求头Authorization中,然后在认证成功后把server端返回的token发送给succesCallback。

  2. authenticate

    与signin一样,首先调用request函数,不过把username替换成token,password置空,用token的方式来认证。与signin提供回调函数不一样,这里使用$q返回promise,可以让调用then方法进一步处理结果。

Angular-Storage 存储token

server返回的token用angular-storage保存,angular-storage使用浏览器的localStorage或者sessionStorage保存,如果浏览器不支持这些功能,就要ngCookies保存。

function onLoginSuccess(token) {
    store.set('token', token);
    $scope.hitMsg = 'login success';
    $location.path('/');
}

function onLoginFailed() {
   $scope.hitMsg = 'username or password incorrect';
}

$scope.submit = function() {
    AuthService.signin({
        username: $scope.user.username,
        password: $scope.user.password
    }, onLoginSuccess, onLoginFailed)
};

上面是LoginCtrl的一段代码,可以看到,当登录成功,用store.set('token', token)保存token。

$rootScope.$on('$routeChangeStart', function (event, next, current) {
      if (!$rootScope.isLogin && next.templateUrl) {
          var token = store.get('token');
          if (token) {
              AuthService.authenticate().then(function() {
              }, function() {
                  store.remove('token');
        $location.path('/login');
              });
          } else if (next.loginRequired) {
              $location.path('/login');
          }
      }
});

上面的代码监听了全局的$routeChangeStart事件,如果用户没有登录,我们提取token进行认证,如果认证失败就删除本地的token(表明token过期了),并提醒用户登录。loginRequired是我们自定义的一个属性,用来标识该route是否需要登录。见下面的route代码:

$routeProvider
.when('/', {
    templateUrl: 'static/partials/welcome.html',
    controller: 'WelcomeCtrl',
    loginRequired: false
})
.when('/login', {
    templateUrl: 'static/partials/login.html',
    controller: 'LoginCtrl',
    loginRequired: false
})
.when('/logout', {
    templateUrl: 'static/partials/welcome.html',
    controller: 'LogoutCtrl',
    loginRequired: true
})
.otherwise({
    rediretTo: '/'
});

拦截 401 错误

如果认证失败,server会返回WWW-Authenticate: Basic realm="domain",状态码为401的错误,此时我们需要拦截这个错误,并要求用户重新登录。

myApp.factory('tokenInvalidInterceptor', ['$q', '$location', '$rootScope', 'store',
    function ($q, $location, $rootScope, store) {
    return {
        'responseError': function (rejection) {
            if (rejection.status == 401) {
                if (store.get('token'))
                    $rootScope.$broadcast('unauth_token');
                else
                    $rootScope.$broadcast('unauth');
            }
            return $q.reject(rejection);
        }
    }
}]);

// 在myApp.config中
$httpProvider.interceptors.push('tokenInvalidInterceptor');

上面这段代码创建了一个Interceptror,如果在responseError中出现401错误码,广播事件给监听者,这里如果store中包含token就广播unauth_token事件,反之,广播unauth事件,下面的代码会根据这两个事件给用户提供相应的提示信息

$rootScope.$on('unauth_token', function () {
    store.remove('token');
    $location.path('/login');
    $scope.hitMsg = 'token expire, please login';
});

$rootScope.$on('unauth', function () {
    $location.path('/login');
    $scope.hitMsg = 'please login';
});

此外,如果server端返回WWW-Authenticate: Basic realm="domain",浏览器会弹出一个对话框
提示用户登录,要避免这种情况,我们可以让服务端返回WWW-Authenticate: xBasic realm="Authentication Required", 如下

http_auth = HTTPBasicAuth()

@http_auth.error_handler
def unauthorized():
    response = make_response()
    response.status_code = 401
    response.headers['WWW-Authenticate'] = 'xBasic realm="{0}"'.format('Authentication Required')
    return response

关于token的生成和认证

  • 生成token

    from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
    def generate_auth_token(self):
      s = Serializer(current_app.config['SECRET_KEY'], expires_in=3600)
      return s.dumps({'id': self.id})
    

把用户id序列化为一个默认一小时expire的字符串

  • 认证token

    @http_auth.verify_password
    def verify_password(username_or_token, password):
        if password == '':
            g.current_user = User.verify_auth_token(username_or_token)
            g.token_used = True
            return g.current_user != None
        user = User.query.filter_by(name=username_or_token).first()
        if not user:
            return False
        g.current_user = user
        g.token_used = False
        return user.verify_password(password)
    

这里是指HTTPAuth的密码验证函数,该函数接收两个参数,如果第二个参数,即 password 为空,则认为是以token认证,否则以username和password认证。

让程序跑一跑

要让程序成功跑起来,首先创建virtualenv,然后安装依赖

pip install -r requirements.txt

然后创建数据库

$ python manager.py create_db

然后创建用户名和密码

$ python manager.py shell

>>> u = User(name='admin')
>>> db.session.add(u)
>>> db.session.commit()

最后运行服务

$ python manager.py runserver
  • 欢迎界面
    welcome.jpg

  • 登录界面
    login.png

  • 登录成功跳转到欢迎界面
    logged.png

附上程序源码

https://github.com/khalily/flask-angular-http-auth