认证授权的设计与实现

一、前言

每个网站,小到一个H5页面,必有一个登录认证授权模块,常见的认证授权方式有哪些呢?又该如何实现呢?下面我们将来讲解SSO、OAuth等相关知识,并在实践中的应用姿势。

二、认证 (authentication) 和授权 (authorization)

这两个术语通常在安全性方面相互结合使用,尤其是在获得对系统的访问权限时。两者都是非常重要的主题,通常与网络相关联,作为其服务基础架构的关键部分。然而,这两个术语在完全不同的概念上是非常不同的。虽然它们通常使用相同的工具在相同的上下文中使用,但它们彼此完全不同。

身份验证意味着确认您自己的身份,而授权意味着授予对系统的访问权限。简单来说,身份验证是验证您的身份的过程,而授权是验证您有权访问的过程。

authentication 证明你是你,authorization 证明你有这个权限。身份验证是授权的第一步,因此始终是第一步。授权在成功验证后完成。

例子:你要登陆论坛,输入用户名张三,密码1234,密码正确,证明你张三确实是张三,这就是 authentication;再一check用户张三是个版主,所以有权限加精删别人帖,这就是 authorization。

三、单点登录(SSO)

单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

举例来说,QQ音乐和腾讯新闻是腾讯公司旗下的两个不同的应用系统,如果用户在腾讯新闻登录过之后,当他访问QQ音乐时无需再次登录,那么就说明QQ音乐和腾讯新闻之间实现了单点登录。

3.1 父域Cookie

最简单是实现方式是,将 Cookie 的 domain 属性设置为当前域的父域,那么就认为它是父域 Cookie。Cookie 有一个特点,即父域中的 Cookie 被子域所共享,换言之,子域会自动继承父域中的 Cookie。

  • 系统1:a.zxy.com
  • 系统2:b.zxy.com
  • 登录系统:login.zxy.com
sequenceDiagram
系统1->>系统1:已登录状态,登录cookie在zxy.com域
系统2->>系统2:需要登录
系统2->>登录系统:登录(携带登录cookie信息)
登录系统->>登录系统:登录验证
登录系统-->>系统2:登录成功
系统2->>系统2:访问资源
CAS流程

3.2 CAS

还有一种方式,那就是CAS(Central Authentication Service)(中心认证服务) 。可参考OAuth2.0,应用系统检查当前请求有没有 Ticket,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心。由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了。如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录,如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Ticket,拼接在目标 URL 的后面,回传给目标应用系统。

CAS 流程图

四、OAuth

4.1 四种方式

OAuth 2.0定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

4.1.1 授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与“服务提供商”的认证服务器进行互动。

sequenceDiagram
Resource Owner->>Client: 1. 用户访问客户端
Client->>User Agent: 2. 客户端将用户导向认证服务器
User Agent->>Authorization Server: 3. response_type=code&client_id={客户端的ID}&redirect_uri={重定向URI}&scope={权限范围}&state={state}
User Agent->>Resource Owner: 4. 用户选择是否给予客户端授权
User Agent->>Authorization Server: 5. 用户给予授权
Authorization Server-->>User Agent: 6. 重定向URL?code={code}&state={state}
User Agent-->>Client: 7. 重定向URL?code={code}&state={state}
Client->>Authorization Server: 8. grant_type=authorization_code&client_id={client_id}&code={code}&state={state}&redirect_uri={redirect_uri}
Authorization Server-->>Client: 9. expires_in access_token refresh_token scope
OAuth2.0-授权码模式
  1. response_type=code&client_id={客户端的ID}&redirect_uri={重定向URI}&scope={权限范围}&state={state}
  2. grant_type=authorization_code&client_id={client_id}&code={code}&state={state}&redirect_uri={redirect_uri}

4.1.2 简化模式

简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

sequenceDiagram
Resource Owner->>Client: 1. 用户访问客户端
Client->>User Agent: 2. 客户端将用户导向认证服务器
User Agent->>Authorization Server: 3. authorize?response_type=token&client_id={客户端的ID}&redirect_uri={重定向URI}&scope={权限范围}&state={state}
User Agent->>Resource Owner: 4. 用户选择是否给予客户端授权
User Agent->>Authorization Server: 5. 用户给予授权
Authorization Server-->>User Agent: 6. expires_in access_token refresh_token scope state,并在URI的Hash部分包含了访问令牌
User Agent->>WebHosted Client Resource: 7. 浏览器向资源服务器发出请求
WebHosted Client Resource-->>User Agent: 8. 返回可以从Hash值中获取令牌的代码脚本
User Agent->>User Agent: 9. 根据脚本提取令牌
User Agent->>Client: 10. access_token
OAuth2.0-简化模式
  1. authorize?response_type=token&client_id={客户端的ID}&redirect_uri={重定向URI}&scope={权限范围}&state={state}
  2. expires_in access_token refresh_token scope state,并在URI的Hash部分包含了访问令牌

4.1.3 密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。

sequenceDiagram
Resource Owner->>Client: 1. 用户名和密码
Client->>Authorization Server: 2. grant_type=password&username={username}&password={password}&scope={权限范围}
Authorization Server-->>Client: 3. expires_in access_token refresh_token
OAuth2.0-密码模式
  1. grant_type=password&username={username}&password={password}&scope={权限范围}

4.1.4 客户端模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

sequenceDiagram
Client->>Authorization Server: 1. grant_type=client_credentials&scope={权限范围}
Authorization Server-->>Client: 2. expires_in access_token refresh_token
OAuth2.0-客户端模式

4.2 更新令牌

如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌。

sequenceDiagram
Client->>Authorization Server: 1. grant_type=refresh_token&refresh_token={refresh_token}
Authorization Server-->>Client: 2. expires_in access_token refresh_token
OAuth2.0-更新令牌

4.3 微信小程序登录的例子

小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。

使用的是OAuth2.0中的授权码模式。调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

微信小程序登录

五、JWT

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

JWT的最常见场景,一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。

JWT由三部分组成,它们之间用圆点“.”连接。这三部分分别是:Header、Payload、Signature。因此,一个典型的JWT看起来是这个样子的:“xxx.yyy.zzz”

JWT的第一部分Header典型的由两部分组成:类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

{
  "alg": "HS256",
  "typ": "JWT"
}

JWT的第二部分Payload,也就是我们数据的存放地方,特别注意不要在里面存放敏感信息。它包含声明,声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

JWT的第三部分Signature,为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

Java实现

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;

public class Test {
    private static final Logger logger = LoggerFactory.getLogger(Test.class);
    private static String secret = "zhongxy@123456";
    private static ObjectMapper objectMapper = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        UserInfo userInfo = new UserInfo(); // 自定义的登录对象
        userInfo.setId(6);
        userInfo.setName("测试");
        logger.info("UserInfo:" + objectMapper.writeValueAsString(userInfo));

        String token = generateToken(userInfo, 60 * 1000);
        logger.info("token:" + token);

        Object result = check(token);
        logger.info("check:" + objectMapper.writeValueAsString(result));
    }

    // 生成token
    public static String generateToken(UserInfo userInfo, long ttlSecs) {
        //The JWT signature algorithm we will be using to sign the token
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //We will sign our JWT with our ApiKey secret
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(secret);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        //Let's set the JWT Claims
        JwtBuilder builder = null;
        try {
            builder = Jwts.builder()
                    .setIssuedAt(now)
                    .setIssuer(objectMapper.writeValueAsString(userInfo))
                    .signWith(signatureAlgorithm, signingKey);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }

        //if it has been specified, let's add the expiration
        if (ttlSecs >= 0) {
            long expMillis = nowMillis + ttlSecs * 1000;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        //Builds the JWT and serializes it to a compact, URL-safe string
        return builder.compact();
    }

    // 从token中反向解析出UserInfo
    public static UserInfo check(String token) {
        try {
            //This line will throw an exception if it is not a signed JWS (as expected)
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(secret))
                    .parseClaimsJws(token).getBody();
            String userInfoStr = claims.getIssuer();
            return objectMapper.readValue(userInfoStr, UserInfo.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

六、参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容