【Web总结】用户认证

个人原文:www.hijerry.cn/p/61701.htm…php

前言

用户认证就是判断一个用户是否为合法用户的过程。html

目前用户认证大都是基于Cookie、Session实现的。对于HTTP协议还不熟悉的话,能够参考《HTTP权威指南》。laravel

应用场景

注册、登录几乎是全部Web站点都具有的两个功能。数据库

以商城系统为例,用户输入登陆名、密码进行注册、登录,这样系统内就能够为用户保存如:购物车、订单、商品喜爱等个性化信息。跨域

用户认证的最主要目的是保存个性化信息。浏览器

用户认证是用户受权的基础。以商城系统为例,商家须要先进行用户认证,系统才能判断他是否有某个店铺的管理权。缓存

API调用和网页浏览同样,也须要用户认证。服务器

版本1:基于Session

Session是一种将数据存储在服务器端的会话控制技术,咱们可使用它实现用户认证。微信

下面是一个基于Laravel5的PHP版本的用户认证:session

/** * 用户登陆 * @param string $login 登陆名 * @param string $password 登陆密码 * @return UserModel|false */
function userLogin($login, $password) {
    $user = UserModel::where('login', $login)->first();
    if ($user && $user->checkPassword($password)) {
        session()->put('_user', $user);
        return $user;
    } else {
        return false;
    }
}

/** * 获取已经登陆的用户实例 * @return UserModel|null */
function getLoginUser() {
    return session()->get('_user');
}
复制代码

userLogin函数接受用户名、密码两个参数进行用户认证工做,认证成功返回用户实例,失败返回false

getLoginUser函数用于获取已经登陆的用户,已登陆返回用户实例,未登陆返回null(由session()->get函数返回的)。

第8行:按$login从数据库中取出匹配的第一个用户实例

第9行:判断是否定证成功,checkPassword用于判断$password是否符合$user的密码。

第10行:将$user存入session中,键为_user

第11行:认证成功,返回用户实例$user

第13行:认证失败,返回false

第22行:从session中取出用户实例。

这种作法的核心思想是把用户数据直接交由Session保管。

Session能够基于Cookie或URL实现,不论哪一种形式,都须要先由服务器种下session-id(种在Cookie里或是重在URL里),后续请求带上这个session-id,服务器才能实现Session。

版本2:基于令牌Token

API请求大多会使用HTTP Client完成,它是不带浏览器的Cookie(除非手动设置)。同时,API请求大都都只有一个请求和一个响应,session-id是来不及种的。

基于令牌的用户认证,本质是将登陆时随机生成的token写在HTTP头或是写在URL上,服务器经过鉴别token来进行用户认证。

上代码:

/** * 用户登陆 * @param string $login 登陆名 * @param string $password 登陆密码 * @return UserModel|false */
function userLogin($login, $password) {
    $user = UserModel::where('login', $login)->first();
    if ($user && $user->checkPassword($password)) {
        $token = $user->generateAuthToken();
        session()->put('_token', $token);
        cache()->put('user_' . $token, $user);
        return $user;
    } else {
        return false;
    }
}

/** * 获取已经登陆的用户实例 * @return UserModel|null */
function getLoginUser($token = null) {
    if (! $token) $token = session()->get('_token');
    $cache_key = 'user_' . $token;
    return cache()->get($cache_key);
}
复制代码

这个版本的userLogin函数,在认证成功后,经过用户实例生成一个token放入session,再把用户实例$user放入缓存系统中(如Redis、Memcache)。token通常都是32位的md5值。

getLoginUser 函数也有所变化,它能够接受指定的$token来获取用户实例,默认状况下它会从session中取出token。

第10~12行:使用$user生成token,将用户实例存入缓存系统中。

第24~26行:使用token从缓存系统中获取用户实例。

的一种可用的用于生成token的方法:

/** * 生成认证token * @return string 认证token */
public function generateAuthToken() {
    if ($this->token) return $this-token;
    return $this->token = md5(md5($this->id . time()));
} 
复制代码

time()函数返回当前unix时间戳。能够看到,token与用户id登陆时间有关,这能够保证惟一性。

这样的用户认证下,API请求怎么作呢?

咱们先建立一个接口 /login 用于登陆,接口的返回值里,附上登陆成功后的 token,HTTP Client将这个token缓存起来,在以后的请求中带上这个token便可。这样以来,用户认证就不是基于Cookie而是基于token了。

这样的用户认证已经能够知足大部分应用场景了如Cookie失效、API请求和统一认证。但还有一个场景没法知足,那就是多终端数据共享。好比用户在电脑上登陆了一次,在手机上登陆了一次,系统会生成2个token,这两个token对应的用户实例是不同的,因此用户在电脑上设置的个性化信息(好比性别,名称)没法共享到手机上。

版本3:多终端数据共享

多终端共享须要明确两点:

  • 各个终端的登陆时长互不影响
  • 各个终端的用户数据一致

实现多终端数据共享还有其余方法,下面举例一个我在项目中用的方法。

代码以下:

/** * 用户登陆 * @param string $login 登陆名 * @param string $password 登陆密码 * @return UserModel|false */
function userLogin($login, $password) {
    $user = UserModel::where('login', $login)->first();
    if ($user && $user->checkPassword($password)) {
        $token = $user->generateAuthToken();
        session()->put('_token', $token);
        // 认证
        cache()->put('user_token_' . $token, $user->id);
        // 数据
        cache()->put('user_' . $user->id, $user);
        return $user;
    } else {
        return false;
    }
}

/** * 获取已经登陆的用户实例 * @return UserModel|null */
function getLoginUser($token = null) {
    if (! $token) $token = session()->get('_token');
    $token_cache_key = 'user_token_' . $token;
    $user_id = cache()->get($token_cache_key);
    if (! $user_id) return null; // token失效,认证过时
    
    $user_cache_key = 'user_' . $user_id;
    $user = cache()->get($user_cache_key);
    if (! $user) {
        // 缓存失效,从新缓存
        $user = UserModel::find($user_id);
        cache()->put($user_cache_key, $user);
    }
    return $user;
}
复制代码

这种认证方式下,token只能解析出user_id,这就比如是一个用户指针,系统再由user_id解析出用户实例。这样能够保证,不一样终端拿到不一样的token,这些token的过时时间不会相互影响,而不一样token能够拿到同一个用户数据,从而实现多终端用户数据共享。

getLoginUser函数,先检查token是否失效,再进一步检查用户实例缓存是否失效。

帐号激活

多终端数据共享的应用场景也很普遍,好比帐号激活,发一份Email邮件,让用户点击连接进行帐号激活。在激活操做里,系统须要知道用户想要激活那个帐号,一个一般的作法以下:

/** * 生成用于激活帐号的连接 * @return string 用于激活的uri */
function generateActivateLink() {
	$code = md5('activate' . Auth::id() . time());
    cache()->put($code, Auth::id());
    return url('/user/activate?code=' . $code);
}

/** * 激活用户 * @param string $code 激活码 * @return string 用于激活的uri */
function activateUser($code) {
    $user_id = cache()->get($code);
    if (! $user_id) return false;
    // 修改数据库
    $user = UserModel::find($user_id);
    $user->status = UserModel::STATUS_ACTIVATED;
    $user->save();
    // 修改缓存
    $user_cache_key = 'user_' . $user_id;
    if (cache()->get($user_cache_key)) {
        cache()->put($user_cache_key, $user);
    }
    return $user;
}
复制代码

能够看到,生成的激活连接中的code实际上是缓存键,使用code能够获取到用户id,这样系统就知道了须要激活哪一个用户。

在激活时,系统只须要修改缓存中的用户实例便可,用户不须要从新登陆帐号以刷新缓存中的数据。

第8行:url()函数,是laravel中用于生成完整url的函数。

第21行:修改用户的status字段值为STATUS_ACTIVATED对应的值。

第22行:保存修改的信息到数据库。

OAuth和第三方登陆认证

OAuth协议可让第三方在不知道用户敏感信息的前提下,获取服务器内用户的资源。第三方登陆就可使用OAuth协议来完成,如微信、QQ、微博等社交平台都提供第三方登陆接入服务。

OAuth2.0

OAuth2.0的受权能够简单分为三步:

  1. 获取用户受权码Code
  2. 获取用户受权令牌Token
  3. 使用受权令牌Token获取用户信息

第一步,又称用户登陆引导页面。在微信登陆时,这个页面的域名是在微信下的,用户赞成受权后,微信会把受权码Code送到服务器(经过回调URI的形式)。拿到这个Code表示用户赞成了受权

第二步,在微信登陆时,这个token又叫access_token。拿到这个Token表示服务器是合法的

第三步,在微信登陆时,这一步能够拿到用户的open_id

在微信登陆中,若是要获取用户基本信息,须要用open_id+access_token才能获得。

关于OAuth2.0协议更多内容,能够参考这2篇文章:深刻理解OAuth2.0协议理解OAuth 2.0

如何集成

一个用户能够"绑定"多个第三方帐号,这是一个比较好的处理第三方用户的方式。第三方用户的管理必须重视,若是管理混乱,绑定的信息不能指向同一个用户,就会出现多身份问题,好比用户使用手机登陆购买的东西,在使用微信登陆时却提示没有购买。

我介绍一下个人作法,数据库两张表:

  • user表,记录用户信息。这里有telephoneemail等可用于登陆的字段
  • user_third表,记录用户绑定的第三方帐号信息。

登陆逻辑以下:

  • 当用户使用如手机号、邮箱、登陆名登陆时,在user表里查询信息。
  • 当用户使用第三方登陆时,系统先去user_third里查询信息,若是未找到,则在user表里新建用户,再将第三方帐号信息保存到user_third里,最后把新建的用户与第三方帐号信息绑定;若是能找到,则返回第三方帐号所绑定的user表里的数据。

这种作法,能够保证用户数据均来自user表,就不会有多身份问题,同时一个用户也能够绑定多个第三方帐号,更加便于管理。

还有一种状况是绑定信息冲突,好比用户第一个帐号绑定了手机号和微信帐号,过段时间后,他用QQ帐号登陆时(此时这个QQ号没有对应系统内的用户)系统会建立第二个帐号,此时他再去绑定手机号或微信号的时候,会由于user表的telephone字段、user_third表中已有信息,而致使绑定失败。

处理这种状况经常使用的方法是解绑,用户能够解绑QQ号,再绑定QQ号至第一次建立的帐号;也能够选择解绑手机、微信,再将手机、微信绑到第二个帐号上。

单点登陆

单点登陆(Single Sign On,SSO)经常使用于多服务器共存的大型网站,即一次用户认证,便可访问旗下全部网站。

豆瓣网为例,它有豆瓣读书豆瓣电影子网站,这两个子网站部署在不一样服务器上。

基于Token的认证

首先,用户数据不能放在Session里,因此基于Token的认证方式很快进入咱们的视野,也就是版本2和版本3的认证方式。须要注意的是,不一样服务器必须使用同一个缓存系统。能够单独起一个服务器用做数据存储。这样一来,系统均可以根据token从缓存系统中解析出用户实例

同源:共享Cookie

仔细的同窗会发现,版本3的token是存在Session里的,就算在子网A中登陆完了,在子网B的Session中并无这个token。一个常见的作法是共享Cookie,让子网A的Cookie可让子网B使用,再将token放在Cookie中,而不是放在Session里。

例如豆瓣读书域名为:book.douban.com,豆瓣电影域名为:movie.douban.com,如今要种一个Cookie,使得这两个域名都能使用。由于他们是属于同一个二级域名douban.com下的,因此可让用户在域名www.douban.com下登陆,把Cookie的路径设置为.douban.com,便可实现Cookie的共享。

跨域:统一认证网站

若是遇到www.taobao.com和www.douban.com要作统一身份认证怎么办呢?由于没有共同的二级域名,因此将认证系统建于第三个网站中,这个网站也叫统一认证网站(简称认证网)。

mark

咱们先假设一个未登陆的用户。

  1. 第一次请求。请求网站A的/home网页,网站A检测出用户未登陆,因而使用HTTP重定向,引导用于至认证网的登陆页面去。
  2. 第二次请求。这是由浏览器自主发起的,认证网响应出登陆页面。
  3. 第三次请求。用户输入帐号密码进行登陆,服务器认证成功后,种下Cookie,并重定向至网站的A的/home页面,可是带上了token。接收这次响应后,浏览器已有了认证网的Cookie,因此用户在认证网处于登陆状态。
  4. 第四次请求。浏览器自主发起的,网站A必须识别出token参数,并保存起来。在响应中,种下网站A的Cookie。此时用户在网站A也处于登陆状态。

咱们假设这个已经认证过的用户,去访问网站B。

mark

能够看到,在引导用户至认证网的登陆页面时,由于用户在认证网处于登陆状态,因此认证网直接重定向到网站B的/profile页面。

有朋友会发现,认证网的功能其实能够融合到网站A或网站B中。确实能够这样作,可是不推荐,由于要秉持低耦合的原则,将认证系统独立出来会更加方便使用和管理。

进一步理解,使用OAuth协议也能够实现单点登陆功能,它就是API版本的单点登陆。

令牌管理

在基于令牌的认证里,token是最为关键的信息,若是有第三方窃取到了用户的token,他就能够冒充用户的进行操做。

隐藏Token

啥意思呢?就是把token放在HTTP头里,尽可能让用户感受不到token的存在。好比下面的HTTP头:

...
X-AUTH-TOKEN: 340c6f730612769b71075d4fbbe5d337 
...
复制代码

可是若是HTTP包被黑客获取,他仍然可以窃取到token

使用HTTPS

HTTPS会将数据包加密,因此黑客就算截取到数据包到也没法获取token

文章内容是本身结合理论,在实践中总结出来的,欢迎你们留言交流、讨论~~

相关文章
相关标签/搜索