Oauth2.0认证模式
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Oauth2.0认证模式相关的知识,希望对你有一定的参考价值。
参考技术A背景
首先token认证方式的出现是伴随着系统架构的发展而来的。
最初的单机应用使用sessionId+cookie完全可以满足问题。
随着系统业务访问量加大,为满足服务高可用需求,系统引入了分布式架构,但是session只能存在单机节点上,为了解决这一问题,首先使用过 session粘贴 技术(通过hash等手段让请求始终访问之前生成sessionId的节点),这个方案的问题是万一存sessionId的节点挂了,整个系统都不能登陆了,也违背了高可用的设计思路;后来使用 session复制 的方案,即每台节点都copy一份sessionId,这引出2个问题,第一个就是分布式一致性的问题,节点同一时间重复创建了怎么办?另一个就是更头疼的系统扩展问题,随着系统越来越庞大,节点增多,session复制消耗的性能也越来越大,整个系统因为session出现了可遇见的性能瓶颈;再后来出现了session集中存储到一个地方,这样解放了服务器,但是session存储的高可用问题又来了,简直就是套娃式困局。
既然 有状态服务 出现瓶颈,大佬们开始提出 无状态服务 ,就是使用token验证方式替代较重的session,token就是一个字符串,只要服务根据约定好的算法解析出的结果跟预期一致就可以通过验证,也不需要保存在内存中,无状态服务的好处是系统扩展轻松,新追加的服务轻装上阵,完全没有顾虑。Oauth就是一种规范,用来保证token机制的可行,目前在web端和移动端都是主流登陆验证方案,Oauth2.0于2010年正式提出,一直沿用到现在,可见其稳定。
随着互联网行业迅猛发展,各种app,各种网站层出不穷,传统登陆方式就要求用户在每个想登陆的站点都要注册自己的账号信息和密码,这也留下了一定隐患,有的小站点可能技术不那么强,这就有可能导致用户信息泄漏,Oauth正好可以解决这种对一些站点即想用又不信任问题。从使用体验的角度,单点登陆的方式也可以简化用户对账号密码的管理。
Oauth2认证模式的4种类型
这种模式最常用,也是最安全的模式,相关的角色有3个:
客户端 (web站点/app,从认证服务的视角,不管你是web页面还是后台服务,都属于客户端范畴)、 资源服务 (存放个人信息的服务,比如微信存放你个人信息的服务)、 认证服务 (某可信的账户平台如微信平台)
使用之前需要在认证服务注册下自己的app信息(主要包括app名称、app站点、认证callback地址等),认证平台会给客户端创建一个clientId(客户端id)和client_secret(客户端秘钥)。这是为了解决认证平台不信任客户端的问题。
使用的时候用户首先会点击客户端登陆页上的一个链接,打开认证服务上的一个登陆确认页面,用户认证的时候会上传以下请求参数:
认证成功后,认证服务会重定向到redirect_uri,
同时认证服务还会按照之前注册好的callback地址,向客户端发一个get请求,参数:
这样客户端拿到了code,会再向认证平台的授权服务器发起请求,参数:
授权服务验证code没问题后,会给客户端返回token。
客户端拿着token就可以去资源服务器请求用户信息了(头像、姓名等)。
这种模式大致流程是:
这个模式不经过code验证的过程,这种token可以直接在callback的uri上看到,并且客户端后台也不会验证token,这种模式安全性显然没有授权码模式好,应用也不多。
流程:
用户直接在客户端上输入账号密码,然后由客户端向认证服务请求要token。
要使用这种模式就表明用户完全信任客户端了,如果信任客户端,直接在客户端上注册账号不就好了,何必把其他平台的账号告诉客户端,万一客户端保存了这个账号密码呢?
流程:
用客户端自己的名义向认证服务获取token,跟用户没什么关系。这也不会是登陆会考虑的场景吧...
http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
https://www.bilibili.com/video/BV1zt41127hX?spm_id_from=333.999.0.0
Spring Security---Oauth2详解
Spring Security---Oauth2详解
OAuth2需求场景
在说明OAuth2需求及使用场景之前,需要先介绍一下OAuth2授权流程中的各种角色:
- 资源拥有者(User) - 指应用的用户,通常指的是系统的登录用户
- 认证服务器 (Authorization Server)- 提供登录认证接口的服务器,比如:github登录、QQ登录、微信登录等
- 资源服务器 (Resources Server) -提供资源接口及服务的服务器,比如:用户信息接口等。通常和认证服务器是同一个应用。
- 第三方客户端(Client) - 第三方应用,希望使用资源服务器提供的资源
- 服务提供商(Provider): 认证服务和资源服务归属于一个机构,该机构就是服务提供商
如果您对这些角色承担的作用还不清晰,也请先记住这些角色,继续往下看:
- 从资源拥有者,即系统用户的角度:举个例子,用户在X应用上,想使用自己在QQ中的保存的用户信息等资源。所以用户希望QQ开放接口给X应用,从而该用户可以在X应用中使用自己在QQ上的用户信息。即:实现QQ登录效果。
- 从服务提供商的角度,如QQ:我想让其他厂商的应用都使用我提供的资源,以增强用户对我的的粘性。越多的第三方应用依赖于我开放的接口,就表示会有越多的用户依赖于我。参考:微信平台开放扫码登录功能。
- 从第三方客户端,即资源申请者的角度:QQ微信是一个大厂开发的,它那里用户量大。微信既然提供了基于OAuth2的接口,我可以获取一些基本用户数据信息,我干嘛不用呢。特别是扫码登录功能接口,给我自己的用户也带来了极大的方便,增强了用户在我的应用上的体验。
OAuth2授权的流程
OAuth2授权的流程的授权流程还是有点复杂的,用专业的术语很容易把大家弄糊涂,所以我希望给大家举一个生活中的例子,来帮助理解。
背景:我经营着一个考研自习室,向考研学生出租提供自习室资源。李小明是一位考研学生,自习室资源拥有者,我的用户。
- 资源拥有者 - 考研同学李小明
- 资源服务器 - 考研自习室及自习室内的资源(书包)
- 认证服务器 - 我(考研自习室管理员)
- 第三方客户端 - 考研同学李小明家长,第三方申请者
下面我们来结合这张图理解OAuth2授权的流程:
- 第一步(第三方申请资源):一个自称是考研学生家长的人给我打电话:“李小明是在你这里自习吧?他的书包放在自习室了,我要帮他取一下。”
- 第二步(验证资源拥有者): 我此时将信将疑,于是让家长等一下,同时拨通了李小明视频,李小明向我确认,的确有这回事。
- 第三步(认证通过发授权码):我一看这情况,就和小明家长说:李小明的自习室是“XXXX”地址,但是我不在那,你来我这取一下钥匙吧
- 第四部(申请token令牌):小明家长来到我的地址,告诉我说:来取“XXXX”地址自习室的钥匙。哦,我一听就明白了。
- 第五步(颁发token令牌):于是我找出自习室的钥匙交给了小明的家长。
从上面的例子中我们看到,小明(用户)是明显受益方,他不用跑腿了。我作为自习室经营者(认证服务器),对外提供这种服务的目的是为了增加用户粘性,增强用户体验。小明的家长作为第三方,他获取了资源(自习室书包),是为了增强自己的儿子小明的用户体验。
以上的授权模式,就是OAuth2最典型的最常被使用的授权码模式。“XXXX”地址是授权码,钥匙是Access Token。用相对专业的说法再说明一次,大家可以对比学习:
- 第三方应用,向认证服务器请求授权。
- 用户告知认证服务器同意授权(通常是通过用户扫码或输入“服务提供商”的用户名密码的方式)
- 认证服务器向第三方应用告知授权码(code)
- 第三方应用使用授权码(code)申请Access Token
- 认证服务器验证授权码,颁发Access Token
这样第三方应用就可以使用Access Token,访问服务提供商的接口资源了。(小明家长用钥匙去自习室取书包)
OAuth2四种授权模式
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
密码模式也很简单:
- 用户将用户名密码交给第三方客户端应用
- 客户端将用户名密码发送给认证服务器,认证服务器验证后颁发AccessToken
- 客户端请求资源接口携带AccessToken,服务端对AccessToken进行校验。
- 校验通过,才能获得接口正确的数据结果响应。
密码模式与授权码模式最大的区别在于:
- 授权码模式申请授权码的过程是用户直接与认证服务器进行交互,然后授权结果由认证服务器告知第三方客户端,也就是不会向第三方客户端暴露服务提供商的用户密码信息。
- 密码模式,是用户将用户密码信息交给第三方客户端,然后由第三方向服务提供商进行认证和资源请求。绝大多数的服务提供商都会选择使用授权码模式,避免自己的用户密码暴漏给第三方。所以密码模式只适用于服务提供商对第三方厂商(第三方应用)高度信任的情况下才能使用,或者这个“第三方应用”实际就是服务提供商自己的应用。
其他两种模式的应用很少,我们讲到OAuth2.0认证服务器的时候再给大家介绍。
回顾OAuth2.0
比如实现QQ登录,实际上我们实现的是第三方应用客户端的功能
认证服务器是由腾讯QQ实现的,资源服务器(qq用户信息)接口也是腾讯QQ提供的。
并且我们的第三方应用是基于web的、基于session的。
那么一个问题出现了:android、IOS、或者纯前端应用vue之类的能使用Spring Social作为服务端OAuth2.0的实现么?
答案是或许可以,但是我没这么做过,这样做也是没有必要的。
因为QQ或者微信等已经针对这些应用提供了JDK(jsJDK、androidJDK等等),这些OAuth2.0的换取AccessToken的过程都在前端进行,而不是像Spring Social的web应用一样在服务端进行。
OAuth2.0与Spring 社区现状
目前Spring 社区内支持OAuth2.0的项目有:
- Spring Social
- Spring Security OAuth
- Spring Cloud Security
- Spring Security 5.2新引入的OAuth支持
作为一个OAuth的开发者,你可能在一开始完全不知道该使用哪一个进行项目的开发?
Spring 社区也意识到这个问题,所以发布了下一代的OAuth2.0支持,此文是项目负责人在社区内发布的博文,核心内容就是:
- Spring Security OAuth项目进入维护状态,不再做新特性的开发。只做功能维护和次要特性开发。
- 未来所有的基于Spring的OAuth2.0的支持都基于Spring Security 5.2版本开发。即:Spring Security 5.2以后的版本是正统的OAuth2.0支持库,是“正统的皇位继承人”。
也就是说Spring Security 5.2中的OAuth2支持,是用来替换Spring Security OAuth项目项目的。在Spring Cloud Security中,Spring Security 5.2中的OAuth2支持和Spring Security OAuth项目是可选的。
Spring Security5.2不支持认证服务器
Spring社区好不容易搞出来一个OAuth2.0集大成者Spring Security5.2,竟然不支持实现认证服务器,只对客户端和资源服务器予以支持。给出的理由是:Spring Security作为框架不应该提供产品级别的支持
说白了我们Spring社区的框架都是为了开发者而存在的,认证服务器是一个产品,我们不是商业机构,不做产品。而且目前有很多的这种产品了,我们就不开发了。比如:Keycloak、Okta。
实现授权码模式认证服务器
maven坐标
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
- 注意,spring-security-oauth2因为已经进入维护阶段,所以其新版本更新及bug修正速度很慢,尽量不要用新版本,就用2.3.6.RELEASE即可。
- 我使用的是Spring Boot2.x版本,在这个版本中spring-security-oauth2不再是父项目默认整合的软件包,所以需要我们需要手动指定version版本。
加载资源拥有者数据----用户
UserDetails
public class MyUserDetails implements UserDetails, CredentialsContainer
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
....
//剩余代码实现,参考UserDetails的默认实现子类User
UserDetailsService
@Service
public class MyUserDetailService implements UserDetailsService
private PasswordEncoder passwordEncoder;
public void setPasswordEncoder(PasswordEncoder passwordEncoder)
this.passwordEncoder = passwordEncoder;
//模拟数据库,不真实连接
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
if(!username.equals("大忽悠"))
throw new UsernameNotFoundException(username);
//对密码进行加密
String encode = passwordEncoder.encode("123456");
//授予超级管理员的角色和访问hello请求的权限
List<GrantedAuthority> grantedAuthorities
= AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_superAdmin,hello");
//返回myUserDetails对象
return new MyUserDetails(username,encode,grantedAuthorities);
认证服务器(授权码模式)
@Configuration
@EnableAuthorizationServer//开启Oauth2认证服务
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter
@Resource
PasswordEncoder passwordEncoder;
//这个位置我们将Client客户端注册信息写死,后面章节我们会讲解动态实现
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception
clients.inMemory()
.withClient("client1").secret(passwordEncoder.encode("123456")) // Client 账号、密码。
.redirectUris("https://blog.csdn.net/m0_53157173?spm=1000.2115.3001.5343") // 配置回调地址,选填。
.authorizedGrantTypes("authorization_code") // 授权码模式
.scopes("all"); // 可授权的 Scope
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
- @EnableAuthorizationServer注解表示开启认证服务器功能。
- 这里的配置实际上和我们在QQ互联上的注册信息,client就是APP ID,secret就是APP Key,回调地址就是我们在QQ互联配置的应用回调地址。
- 指定使用授权码模式,进行认证
- scopes是一组权限的集合,表示可以申请的权限范围,该权限可以被验证,我们后续会讲
记得放行oauth2相关的请求:
//放行oauth2的请求
http.authorizeRequests().antMatchers("/oauth/**").permitAll();
获取授权码(授权码模式)
使用如下链接获取授权码
http://localhost:8080/oauth/authorize?client_id=client1&redirect_uri=https://blog.csdn.net/m0_53157173&response_type=code&scope=all
- /oauth/authorize为获取授权码的地址,由Spring Security OAuth项目提供
- client_id即我们认证服务器中配置的client
- redirect_uri即回调地址,授权码的发送地址该地址为第三方客户端应用的地址。要和我们之前配置的回调地址对上。
- response_type=code表示希望获取的响应内容为授权码
- scope表示申请的权限范围
请开启至少一种认证方式,下面开启的是表单登录,只有用户登录过后,才能进行相关oauth2认证操作
@EnableWebSecurity加不加都可以,原因在于,springboot的在进行相关自动配置的过程中,SpringBoot 会 自动 通过 autoconfigure 自动来加载注入 相关的security 配置信息
Spring security源码解析系列02—要不要配置@EnableWebSecurity
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
@Autowired
SecurityConfig(MyUserDetailService myUserDetailService,JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter)
this.userDetailsService=myUserDetailService;
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
//加密密码
@Bean
public PasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder();
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
this.passwordEncoder=passwordEncoder();
((MyUserDetailService)userDetailsService).setPasswordEncoder(passwordEncoder);
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
@Override
protected void configure(HttpSecurity http) throws Exception
//开启表单登录
http.formLogin();
//配置hello请求的访问权限
http.authorizeRequests().antMatchers("/hello").hasAuthority("hello");
//方向oauth2相关认证请求
http.authorizeRequests().antMatchers("/oauth/**").permitAll().anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
当我们在浏览器上输入上面的获取授权码的地址,会显示如下视图。该视图是用户授权界面,可以参考QQ扫码或输入用户名密码授权的页面。
这里输入的就是用户的用户名和密码,不是客户端配置的用户名和密码(APPID,APPKEY)
在这里我们输入资源拥有者的用户名和密码,显示如下内容,询问是否针对client1进行授权
如果我们勾选Approve(同意),即可完成认证,向第三方客户端应用发放授权码,如下图中的红色框框所示。
根据授权码换取AccessToken(授权码模式)
两种测试方式任选其一
通过CURL发送POST请求
curl -X POST --user client1:123456 http://localhost:8001/oauth/token -H "content-type: application/x-www-form-urlencoded" -d "code=2gMHpI&grant_type=authorization_code&redirect_uri=http://localhost:8888/callback&scope=all"
通过PostMan发送请求’
密码模式
如图配置OAuth2AuthorizationServer:
- 依赖注入AuthenticationManager ,不注入会报错
- 配置支持password密码模式
因为要使用AuthenticationManager ,所以在Spring Security全局配置SecurityConfig.java中加入如下代码,将其初始化为Spring bean
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
使用curl发送请求测试
curl -X POST --user client1:123456 http://localhost:8001/oauth/token -H "accept:application/json" -H "content-type:application/x-www-form-urlencoded" -d "grant_type=password&username=admin&password=123456&scope=all"
需要注意的是,上面的测试请求中有两种密码。
第一种是client1:123456(客户端ID:客户端密码),这个是应用在OAuth2 Server注册时候的密码(即:App Secret或APP Key),对应上一节中的ClientDetailsServiceConfigurer配置 。假如你的应用使用微信登录,你的应用在开放平台注册的APP ID和APP Key就是这个client1:123456
第二种是username=admin&password=123456,这个是用户的自己的用户名密码。这个就是某一个微信用户的用户名密码。(当然微信只支持授权码模式,不支持用户密码模式)。
响应结果如下:
“access_token”:“c7c07c0c-f692-4182-a9a8-f5c400f697f7”,
“token_type”:“bearer”,
“expires_in”:43121,
“scope”:“all”
简化模式
简化模式是授权码模式的“简化”,所以只需要在以上配置的基础上,为authorizedGrantTypes加上implicit配置即可。
使用如下链接获取授权码,浏览器打开
http://localhost:8001/oauth/authorize?client_id=client1&redirect_uri=http://localhost:8888/callback&response_type=token
浏览器打开之后,和授权码模式一样,要求输入用户名密码进行授权。用户授权之后直接向回调地址响应accessToken,而不是授权码code。省去了使用授权码code再去申请accessToken的步骤。
客户端模式
- 客户端模式实际上是密码模式的简化,无需配置或使用资源拥有者账号。因为它没有用户的概念,直接与授权服务器交互,通过 Client的编号(client_id)和密码(client_secret)来保证安全性。
- 配置方式为authorizedGrantTypes加上client_credentials配置即可。
使用curl请求测试。可以看到相对于密码模式,我们没有传递username=admin&password=123456,因为客户端模式没有用户的概念。
curl -X POST "http://localhost:8001/oauth/token" --user client1:123456 -d "grant_type=client_credentials&scope=all"
请求结果如下:
"access_token":"5e2d9b84-c2f4-475b-9ee3-136b6978149f",
"token_type":"bearer",
"expires_in":43018,
"scope":"all"
AccessToken令牌的刷新
在前面为大家介绍了,如何使用Spring Security OAuth实现认证服务器的四种授权模式:授权码模式、简化模式、密码模式、客户端模式。每一个模式的最终认证结果都是我们获取到了一个AccessToken,后续我们可以使用这个Token访问资源服务器。需要注意的一点是AccessToken是有有效期的,如请求结果中的expires_in字段。
"access_token":"5e2d9b84-c2f4-475b-9ee3-136b6978149f",
"token_type":"bearer",
"expires_in":43018,
"scope":"all"
那么,我们如何防止令牌过期,造成用户频繁登录,体验不佳的情况?为此、Spring Security OAuth为我们提供了刷新AccessToken的方法。
一、配置令牌刷新
配置方式为authorizedGrantTypes加上refresh_token配置
为OAuth2AuthorizationServer配置类加入UserDetailsService,刷新令牌的时候需要用户信息
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
endpoints.authenticationManager(authenticationManager)
.userDetailsService(myUserDetailsService);
获取AccessToken
这样当我们通过授权码模式和密码模式请求AccessToken的时候,返回结果中将多出一个字段refresh_token
。(客户端模式和简化模式是不支持refresh_token)
"access_token":"5dc705af-a8b4-4b72-a1ac-5e0cf0c8df67",
"token_type":"bearer",
"refresh_token":"2787e701-cf54-41bc-82ab-9b19a0356445",
"expires_in":43199,
"scope":"all"
刷新AccessToken
发起刷新令牌请求
curl -i -X POST --user client1:123456
http://localhost:8001/oauth/token
-H "accept:application/json" -d "grant_type=refresh_token&refresh_token=ffa97063-217e-401d-8f8c-bbd19be2d44e"
请求结果:可以看到access_token被刷新,并且其有效期回归初始值43199(实际是43200秒、12小时)
"access_token":"5286b54d-8e07-4bdd-a641-1e1ace7af6d2",
"token_type":"bearer",
"refresh_token":"2787e701-cf54-41bc-82ab-9b19a0356445",
"expires_in":43199,
"scope":"all"
令牌的有效期
通常情况下,refresh_token的有效期要远大于access_token的有效期。因为access_token是经常在网络上传输的,所以暴露的可能性相对高一些。所以通常有效期比较短。比如
平台 | access_token的有效期 | refresh_token的有效期 |
---|---|---|
小米开放平台 | 90天 | 10年 |
微信开放平台 | 2小时 | 未知 |
腾讯开放平台 | 90天 | 未知 |
当然最重要的还是要保护client_d和client_secret,不管哪种认证模式,获取refresh_token、access_token都需要提供client_d和client_secret才能访问。
我们可以通过如下的方式配置refresh_token、access_token的有效期。
编码实现资源服务器
“合二为一”还是“分而治之”
- 合二为一:我们可以将认证服务器AutherizationServer和资源服务器ResourceServer定义到同一个SpringBoot应用中。这种方式的好处在于:Token信息存在内存中,二者都可以使用,认证服务器发放AccessToken,并将其保存在内存里面;资源服务器可以获取内存中的AccessToken,进行资源的访问鉴权
- 分而治之:就是将资源服务器ResourceServer与认证服务器AutherizationServer分成两个SpringBoot应用。这样做的好处在于:降低资源和认证之间的耦合程度,适合分布式微服务的资源授权与鉴权。因为两个Spring Boot应用使用的是两块内存,所以Token信息无法共享。如果需要实现共享,需做额外的工作
本节为了将知识点集中到资源服务器ResourceServer的实现上,就使用“合二为一”的方式。不用另外引入maven依赖。
配置资源服务器
随便写一个业务Api接口,代表该应用对外提供的服务资源:
@RestController
@RequestMapping("/api")
public class HelloController
@RequestMapping("/hello")
public String hello()
return "Hello Oauth2 Resource Server";
配置资源服务器,对任何“/api/**”接口的访问,都必须经过OAuth2认证服务器认证
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter
@Override
public void configure(HttpSecurity http) throws Exception OAuth2.0的四种授权模式
Springboot2.0 + OAuth2.0之授权码模式
springboot+spring security +oauth2.0 demo搭建(password模式)(认证授权端与资源服务端分离的形式)