认证授权的设计与实现

标签: 认证 授权 设计 | 发表时间:2021-05-02 15:39 | 作者:狼爷
出处:https://juejin.cn/tag/%E6%9E%B6%E6%9E%84

一、前言

每个网站,小到一个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:访问资源

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
  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
  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
  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

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

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实现

   
io.jsonwebtoken
jjwt
0.9.1

复制代码
   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;
}

}
复制代码

六、参考资料

相关 [认证 授权 设计] 推荐:

认证授权的设计与实现

- - 掘金 架构
每个网站,小到一个H5页面,必有一个登录认证授权模块,常见的认证授权方式有哪些呢. 下面我们将来讲解SSO、OAuth等相关知识,并在实践中的应用姿势. 二、认证 (authentication) 和授权 (authorization). 这两个术语通常在安全性方面相互结合使用,尤其是在获得对系统的访问权限时.

HTTP API 认证授权术

- - 酷 壳 – CoolShell
我们知道,HTTP是无状态的,所以,当我们需要获得用户是否在登录的状态时,我们需要检查用户的登录状态,一般来说,用户的登录成功后,服务器会发一个登录凭证(又被叫作Token),就像你去访问某个公司,在前台被认证过合法后,这个公司的前台会给你的一个访客卡一样,之后,你在这个公司内去到哪都用这个访客卡来开门,而不再校验你是哪一个人.

新浪微博Android客户端SSO授权认证缺陷

- - BlogJava-首页技术区
转载请注明出处:  http://www.blogjava.net/zh-weir/archive/2013/09/08/403829.html. 新浪微博Android客户端SSO授权认证缺陷. 从最近几年开始,做平台的公司都流行起Open API. 这是一个非常好的理念,也受到广大开发者的欢迎.

使用OAUTH2+Zuul实现认证和授权 GitHub - wiselyman/uaa-zuul:

- -
在 Spring Cloud需要使用 OAUTH2来实现多个微服务的统一认证授权,通过向 OAUTH服务发送某个类型的 grant type进行集中认证和授权,从而获得 access_token,而这个token是受其他微服务信任的,我们在后续的访问可以通过 access_token来进行,从而实现了微服务的统一认证授权.

一文讲透认证授权的那些事

- - 掘金 架构
「这是我参与2022首次更文挑战的第2天,活动详情查看: 2022首次更文挑战」. 权限管理一直都是初级程序员学习的一大重点,也是一大难点,有单点登录,有联合登录,有session有Token,有各种权限框架,还有什么是RBAC,以及分布式下如何做权限管理. 联合登录,就是通过开放认证平台由第三方应用做身份担保,使用户可以活得本系统的相关权限,最常见的身份担保平台就是微信、QQ,当然阿里系的一些应用,支付宝,淘宝也可以为用户做身份担保.

分布式系统下的认证与授权 (insights.thoughtworks.cn)

- - IT瘾-jianshu
在软件系统设计中,如何让应用能够在各种环境中安全高效的访问是个复杂的问题,这个问题的背后是一系列软件设计时需要考虑的架构安全问题: 架构安全性 | 凤凰架构. 认证:系统如何识别合法用户,也就是解决. 授权:系统在识别合法用户后,还需要解决. 凭证:系统如何保证它与用户之间的承诺是双方真实意图的体现,是准确、完整且不可抵赖的;.

微信开发之获取OAuth2.0网页授权认证和获取用户信息进行关联

- - ITeye博客
        最近有做了关于微信公众号和自己网站用户进行用户关联授权登录的一个功能,主要是用户关注该公众号,点击会员中心,则会弹出需要关联授权的网页授权:OAuth2.0网页授权,然后用户同意获取用户信息,进行用户和网站的关联,然后用户则可以使用微信进行登录.         本次做的是一个在Java的Action层处理各个返回参数获取数据.

理解OAuth2.0认证与客户端授权码模式详解 - 唐成勇 - SegmentFault 思否

- -
OAuth协议为用户资源的授权提供了一个安全又简易的标准. OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此. Open Authorization的简写. OAuth本身不存在一个标准的实现,后端开发者自己根据实际的需求和标准的规定实现.

全方面解析微服务架构下的统一身份认证和授权 - 知乎

- -
本文讨论基于微服务架构下的身份认证和用户授权的技术方案,在阅读之前,最好先熟悉并理解以下几个知识点:. 微服务架构相关概念:服务注册、服务发现、API 网关. 身份认证和用户授权:SSO、CAS、OAuth2、JWT. 文章在涉及到上述知识内容时,会附上参考链接. 当企业的应用系统逐渐增多后,每个系统单独管理各自的用户数据容易行成信息孤岛,分散的用户管理模式阻碍了企业应用向平台化演进.

身份认证设计的基本准则

- - ITeye博客
密码认证作为当前最流行的身份验证方式,在安全方面最值得考虑的因素就是密码的长度. 一个强度高的密码使得人工猜测或者暴力破解密码的难度增加. 下面定义了高强度密码的一些特性. 对于重要的应用,密码长度最少为6;对于关键的应用,密码长度最少为8;对于那些最关键的应用,应该考虑多因子认证系统. 有的时候仅有长度约束是不够的,比如说12345678、11111111这样的密码,长度的确是8位,但极容易被猜测和字典攻击,所以这时候就需要增加密码复杂度.