Springboot + oauth2 单点登录 - 原理篇
一、前言1.什么是OAuth2?OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。二、Oauh2详细介绍1.OAuth2四种授权模式授权码模式
本文借鉴了网上一些资料,如有侵权,请指出
一、前言
1.什么是OAuth2?
OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。
二、Oauh2详细介绍
1.OAuth2四种授权模式
授权码模式(authorization code)
密码模式(resource owner password credentials)
客户端模式(client credentials) 不常用
简化模式/隐式授权模式(implicit) 不常用
在进入正题前,先了解 OAuth2.0 授权过程中几个重要的参数
esponse_type:code 表示要求返回授权码,token 表示直接返回令牌
client_id:客户端身份标识
client_secret:客户端密钥
redirect_uri:重定向地址
scope:表示授权的范围,read只读权限,all读写权限
grant_type:表示授权的方式,AUTHORIZATION_CODE(授权码)、password(密码)、client_credentials(凭证式)、refresh_token 更新令牌
state:应用程序传递的一个随机数,用来防止CSRF攻击。
在理解OAuth 2.0作用之前,需要了解几个专用名词
(1)Third-party application:第三方应用程序,简称"客户端"(client);
(2)HTTP service:HTTP服务提供商,简称服务端;
(3)Resource Owner:资源所有者,简称"用户"(user);
(4)User Agent:用户代理,是指浏览器;
(5)Authorization server:认证服务器,即服务端专门用来处理认证的服务器;
(6)Resource server:资源服务器,即服务端存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
1.1 授权码模式(authorization code)
- 用户访问应用前端页面(验证是否登录);
- 访问的页面将请求重定向到认证服务器;
- 认证服务器等待用户授权(输入账号、密码);
- 用户授权,认证服务器验证(Clinet_id和身份认证),生成一个code(授权码)--->认证前端--->应用前端--->应用服务器;
- 应用服务器使用client_id、client_secret、code去认证服务器获取token,refresh_token(我们默认应用服务器和认证服务器之间的通信是安全的);
- 然后,应用服务器拿到code, 并用client_id去后台查询对应的client_secret;
- 第七步:验证token,访问真正的资源页面。
主流第三方验证使用的模式
优点:安全:token授权,refresh_token换取新token;4、5步为服务器之间访问难以被截获;用户信息存在引用服务器暴露风险小、secret存在服务端所以完全性高。
缺点:多次请求
1.1.1 请求示例
OAuth2.0四种授权中授权码方式是最为复杂,但也是安全系数最高的,比较常用的一种方式。这种方式适用于兼具前后端的Web项目,因为有些项目只有后端或只有前端,并不适用授权码模式。
下图以用WX登录掘金为例,详细看一下授权码方式的整体流程。
用户选择WX登录掘金,掘金会向WX发起授权请求,接下来 WX询问用户是否同意授权(常见的弹窗授权)。response_type 为 code 要求返回授权码,scope 参数表示本次授权范围为只读权限,redirect_uri 重定向地址。
https://wx.com/oauth/authorize? response_type=code& client_id=CLIENT_ID& redirect_uri=http://juejin.im/callback& scope=read
1.1.2 用户同意授权后,根据 redirect_uri重定向并带上授权码。
http://juejin.im/callback?code=AUTHORIZATION_CODE
1.1.3 当掘金拿到授权码(code)时,带授权码和密匙等参数向WX申请令牌。grant_type表示本次授权为授权码方式 authorization_code ,获取令牌要带上客户端密匙 client_secret,和上一步得到的授权码 code。
https://wx.com/oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& grant_type=authorization_code& code=AUTHORIZATION_CODE& redirect_uri=http://juejin.im/callback
1.1.4 最后 WX 收到请求后向 redirect_uri 地址发送 JSON 数据,其中的access_token 就是令牌。
{ "access_token":"ACCESS_TOKEN", "token_type":"bearer", "expires_in":2592000, "refresh_token":"REFRESH_TOKEN", "scope":"read", ...... }
1.2 oauth简化模式 (隐藏式 implicit)
- 用户访问应用页面,(验证是否已登录,如果未登录)--->重定向到认证服务器页面
- 认证服务器给用户一个认证页面,等待用户授权(用户输入账号密码)
- 认证中心前端把数据返回其服务器,验证其账号和密码,返回前端token
- 认证中心前端重定向(重定向地址是第二步传入的)返回应用前端,把数据--->应用前端。
- 应用前端使用token访问应用的服务器,--->验证token是否合法or过期。
- 如果token验证通过,可以成功访问资源。
优点:简单
缺点:应用只有界面,没后后台管理 client_id、client_secret无法管理、不能存储refresh_token等必要信息,没有refresh_token无法刷新token,token过期不好处理,只能使用第三方认证后,直接访问。
例子:问卷调查、评论
1.2.1 示例:
前端应用直接获取 token,response_type 设置为 token,要求直接返回令牌,跳过授权码,WX授权通过后重定向到指定 redirect_uri 。
https://wx.com/oauth/authorize? response_type=token& client_id=CLIENT_ID& redirect_uri=http://juejin.im/callback& scope=read
1.3 密码模式 (resource owner password credentials)
- 用户访问用页面时,输入第三方认证所需要的信息(认证中心账号密码),直接在应用的前端页面输入账号密码(不算安全,需要对这个第三方极度的信任)
- 应用服务器使用账号密码,访问认证中心来获取token
- 认证服务器授权通过,拿到token,验证token,访问真正的资源页面
优点:不需要多次请求转发,额外开销,同时可以获取更多的用户信息。(都拿到账号密码了)
缺点:局限性,认证服务器和应用方必须有超高的信赖。
应用场景:自家公司搭建的认证服务器
1.3.1 请求示例
密码模式比较好理解,用户在掘金直接输入自己的WX用户名和密码,掘金拿着信息直接去WX申请令牌,请求响应的 JSON结果中返回 token。grant_type 为 password 表示密码式授权。
https://wx.com/token? grant_type=password& username=USERNAME& password=PASSWORD& client_id=CLIENT_ID
1.4 oauth客户端模式(client credentials)
- 用户访问应用客户端
- 通过客户端定义的验证方法,拿到token,(无需用户授权)
- 验证token,访问资源
优点:这是一种最简单的模式,只要client请求,我们就将AccessToken发送给它。
缺点:这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。
因此这种模式一般用来提供给我们完全信任的服务器端服务。在这个过程中不需要用户的参与。
1.4.1 请求示例
凭证式和密码式很相似,主要适用于那些没有前端的命令行应用,可以用最简单的方式获取令牌,在请求响应的 JSON 结果中返回 token。
grant_type 为 client_credentials 表示凭证式授权,client_id 和 client_secret 用来识别身份。
https://wx.com/token? grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET
2. oauth2 内置接口
/oauth/authorize:授权端点
/oauth/token:获取令牌端点
/oauth/confirm_access:用户确认授权提交端点
/oauth/error:授权服务错误信息端点
/oauth/check_token:用于资源服务访问的令牌解析端点
/oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
3. 参数说明
3.1 grant_type参数说明
grant_type | 说明 |
authorization_code | 标准的Server授权模式 |
password | 基于用户密码的授权模式 |
client_credentials | 基于APP密钥的授权模式 |
refresh_token | 刷新accessToken |
3.2 response_type参数说明
response_type | 说明 |
code | 标准的Server授权模式响应模式 |
token | 脚本的授权响应模式,直接返回token,需要对回调进行校验 |
3.3 oauth2 oauth_client_details表字段的详细说明
client_id | 必须唯一,不能为空. |
resource_ids | 客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: "unity-resource,mobile-resource". 可以根据上图知道,我们有Resource Server资源服务器。,资源服务器可以有多个,我们可以为每一个Resource Server(一个微服务实例)设置一个resourceid。 Authorization Server给client第三方客户端授权的时候,可以设置这个client可以访问哪一些Resource Server资源服务,如果没设置,就是对所有的Resource Server都有访问权限。 |
client_secret | 用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成). |
scope | 指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔,如: "read,write". @EnableGlobalMethodSecurity(prePostEnabled = true)启用方法级权限控制 然后在方法上注解标识@PreAuthorize("#oauth2.hasScope('read')") |
authorized_grant_types | 指定客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔,如: "authorization_code,password". 在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: "authorization_code,refresh_token"(针对通过浏览器访问的客户端); "password,refresh_token"(针对移动设备的客户端). implicit与client_credentials在实际中很少使用,可以根据自己的需要,在OAuth2.0 提供的地方进行扩展自定义的授权 |
web_server_redirect_uri | 客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明: 当grant_type=authorization_code时, 第一步 从 spring-oauth-server获取 'code'时客户端发起请求时必须有redirect_uri参数, 该参数的值必须与 web_server_redirect_uri的值一致. 第二步 用 'code' 换取 'access_token' 时客户也必须传递相同的redirect_uri. 在实际应用中, web_server_redirect_uri在注册时是必须填写的, 一般用来处理服务器返回的code, 验证state是否合法与通过code去换取access_token值. 在spring-oauth-client项目中, 可具体参考AuthorizationCodeController.java中的authorizationCodeCallback方法. 当grant_type=implicit时通过redirect_uri的hash值来传递access_token值.如: http://localhost:7777/spring-oauth-client/implicit#access_token=dc891f4a-ac88-4ba6-8224-a2497e013865&token_type=bearer&expires_in=43199
|
authorities | @PreAuthorize("hasAuthority('admin')")可以在方法上标志 用户或者说client 需要说明样的权限 指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔, 如: "ROLE_UNITY,ROLE_USER". 对于是否要设置该字段的值,要根据不同的grant_type来判断, 若客户端在Oauth流程中需要用户的用户名(username)与密码(password)的(authorization_code,password), 则该字段可以不需要设置值,因为服务端将根据用户在服务端所拥有的权限来判断是否有权限访问对应的API. 但如果客户端在Oauth流程中不需要用户信息的(implicit,client_credentials), 则该字段必须要设置对应的权限值, 因为服务端将根据该字段值的权限来判断是否有权限访问对应的API. (请在spring-oauth-client项目中来测试不同grant_type时authorities的变化) |
access_token_validity | 设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时). 在服务端获取的access_token JSON数据中的expires_in字段的值即为当前access_token的有效时间值. 在项目中, 可具体参考DefaultTokenServices.java中属性accessTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义. |
refresh_token_validity | 设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 若客户端的grant_type不包括refresh_token,则不用关心该字段 在项目中, 可具体参考DefaultTokenServices.java中属性refreshTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义. |
additional_information | 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据,如: {"country":"CN","country_code":"086"} 按照spring-security-oauth项目中对该字段的描述 Additional information for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information. (详见ClientDetails.java的getAdditionalInformation()方法的注释) 在实际应用中, 可以用该字段来存储关于客户端的一些其他信息,如客户端的国家,地区,注册时的IP地址等等. |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
autoapprove | 设置用户是否自动Approval操作, 默认值为 'false', 可选值包括 'true','false', 'read','write'. 该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为'true'或支持的scope值,则会跳过用户Approve的页面, 直接授权. |
3.4 oauth_client_token
字段名 | 字段说明 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 从服务器端获取到的access_token的值. |
token | 这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进 |
authentication_id | 该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密 |
user_name | 登录时的用户名 |
3.5 oauth_access_token
字段名 | 字段说明 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 从服务器端获取到的access_token的值. |
token | 这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进 |
authentication_id | 该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密 |
user_name | 登录时的用户名 |
client_id | |
authentication | 存储将OAuth2Authentication.java对象序列化后的二进制数据. |
refresh_token | 该字段的值是将refresh_token的值通过MD5加密后存储的. 在项目中,主要操作 |
3.6 oauth_refresh_token
字段名 | 字段说明 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
token_id | 该字段的值是将refresh_token的值通过MD5加密后存储的. |
token | 存储将OAuth2RefreshToken.java对象序列化后的二进制数据. |
authentication | 存储将OAuth2Authentication.java对象序列化后的二进制数据. |
3.7 oauth_code
字段名 字段说明 | 字段名 字段说明 |
create_time | 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
code | 存储服务端系统生成的code的值(未加密). |
authentication | 存储将AuthorizationRequestHolder.java对象序列化后的二进制数据. |
三、前端原理说明
单点登录主流都是基于共享 cookie 来实现的,下面分别介绍 同域 和 跨域 下的两种场景具体怎样实现共享 cookie 的。
3.1. 同域单点登录
适用场景:都是企业自己的系统,所有系统都使用同一个一级域名通过不同的二级域名来区分。
举个例子:公司有一个一级域名为 smartby.com ,我们有三个系统分别是:门户系统(sso.xxx.com)、应用1(app1.xxx.com)和应用2(app2.xxx.com),需要实现系统之间的单点登录,实现架构如下:
核心原理:
- 门户系统设置 Cookie 的 domain 为一级域名也就是 smartby.com,这样就可以共享门户的 Cookie 给所有的使用该域名(xxx.smartby.com)的系统
- 使用 Redis 、Cookie等技术让所有系统共享 token
- 这样只要门户系统登录之后无论跳转应用1或者应用2,都能通过门户 Cookie 读取到 登录信息实现单点登录
2.2. 跨域单点登录
单点登录之间的系统域名不一样,例如第三方系统。由于域名不一样不能共享 Cookie 了,这样就需要通过一个单独的授权服务(UAA)来做统一登录,并基于共享UAA的 Cookie 来实现单点登录。
举个例子:有两个系统分别是:应用1(webApp.com)和应用2(webApp2.com)需要实现单点登录,另外有一个UAA授权中心(sso.com),实现架构如下:
核心原理:
- 访问系统1判断未登录,则跳转到UAA系统请求授权
- 在UAA系统域名 sso.com 下的登录地址中输入用户名/密码完成登录登录成功后,UAA系统把登录信息保存到 Redis 中,并在浏览器写入域为 sso.com 的 Cookie
- 访问系统2判断未登录,则跳转到UAA系统请求授权
- 由于是跳转到UAA系统的域名 sso.com 下,所以能通过浏览器中UAA的 Cookie 读取到 之前的登录信息完成单点登录。
2.3. 基于Oauth2的跨域单点登录流程
更多推荐
所有评论(0)