虾皮面试原题:深入剖析OAuth2的核心概念及其在实际应用中的多种场景,帮助你更好地理解和运用这一授权机制
在此分享一个群友在面试虾皮时关于OAuth2的实际问题。
什么是OAuth2
OAuth
是一种开放网络标准,主要用于授权(authorization
),允许第三方应用安全地访问用户的私人信息。当前流行的版本为2.0。
使用场景分析
如果你正在“网站 A”浏览内容,发现一篇极具吸引力的帖子,想要点赞时,系统会提示你先登录。
在登录页面,除了基本的账户密码登录方式外,还有如微博、微信、QQ等便捷的登录选项。选择快捷登录后,系统会指引你扫码或输入账号密码进行身份验证。
当你成功登录后,相关的信息如昵称和头像会自动填充至“网站 A”中,此时你便可以顺利进行点赞操作。
基本概念解析
在深入探讨OAuth2之前,我们需要了解几个关键名词:
- Client:客户端,负责请求资源服务器的资源,但不存储用户的快捷登录信息。
- Resource Owner:资源拥有者,通常是用户,指的是拥有QQ或微信账号的人。
- Authorization Server:认证服务器,提供用户身份验证和授权的服务,负责发放和验证
token
。 - Resource Server:资源服务器,存储用户信息的服务器,例如QQ和微信的数据。
认证流程详解
上述图展示了OAuth2的认证流程:
- 客户端向资源拥有者请求授权。
- 资源拥有者同意并返回授权码。
- 客户端使用授权码向认证服务器请求令牌(token)。
- 认证服务器验证客户端身份并发放令牌。
- 客户端使用获得的令牌向资源服务器请求数据。
- 资源服务器验证令牌后返回相应的信息给客户端。
为增强理解,以下是我为大家绘制的简图:
通过以上流程,相信大家对OAuth2的理论知识有了较为清晰的认知,接下来我们将进行实际操作。
实际操作步骤
在开始项目搭建前,我们首先需要进行一些准备工作。为了使用OAuth2服务,我们需要创建相关数据库表。
数据库设计
关于OAuth2相关的表结构,可以参考官方的初始化SQL或查看项目中的init.sql文件,回复“oauth2”获取源码。
以下是各个表的基本结构及其字段说明:
oauth_client_details
:存储客户端配置信息,由JdbcClientDetailsService.java
操作;oauth_access_token
:记录生成的令牌信息,由JdbcTokenStore.java
操作;oauth_client_token
:在客户端系统中存储从服务端获取的令牌,由JdbcClientDetailsService.java
操作;oauth_code
:存储授权码和认证信息,仅当grant_type
为authorization_code
时表中才会有数据,由JdbcAuthorizationCodeServices.java
操作;oauth_approvals
:存储用户的授权信息;oauth_refresh_token
:存储刷新令牌的refresh_token
,如果客户端的grant_type
不支持refresh_token
,则不使用该表,由JdbcTokenStore
操作。
例如,在oauth_client_details
表中添加一条数据:
client_id:cheetah_one // 客户端名称,必须唯一
resource_ids:product_api // 客户端访问的资源ID集合
client_secret:$2a$10$h/TmLPvXozJJHXDyJEN22ensJgaciomfpOc9js9OonwWIdAnRQeoi // 客户端的访问密码
scope:read,write // 客户端申请的权限范围
authorized_grant_types:client_credentials,implicit,authorization_code,refresh_token,password // 支持的grant_type
web_server_redirect_uri:http://www.baidu.com // 重定向URI
access_token_validity:43200 // 令牌有效时间
autoapprove:false // 用户是否自动Approval操作
密码经过加密处理,请根据路径自行生成。
用户角色相关表也在init.sql文件中,结构简单,大家自行查阅。
依赖引入
在项目中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
其他依赖可根据需求自行引入,回复“oauth2”可以获取源码。
配置资源服务
配置文件中对服务端口、应用名称、数据库、mybatis
和日志进行了相关设置。
编写一个简单的控制器代码,模拟资源访问。
接着,创建一个配置类,继承ResourceServerConfigurerAdapter
并使用@EnableResourceServer
注解开启资源服务,重写两个configure
方法。
同时,我们可以配置需要忽略校验的URL,相关代码可在public void configure(HttpSecurity http) throws Exception
中设置。
由于我们的需求是进行校验,因此我将对应的代码注释掉了,想要查看源码可以回复“oauth2”。
认证服务配置
在认证服务中,同样需要对服务端口、应用名称、数据库、mybatis
和日志进行配置。
安全配置
与之前的Security+JWT组合拳配置相似,不太了解的同学可以先查看该文。
- 将实现了
UserDetailsService
的ISysUserService
的SysUserServiceImpl
类重写loadUserByUsername
方法。
- 继承
WebSecurityConfigurerAdapter
类,增加@EnableWebSecurity
注解并重写相关方法。
AuthorizationServer配置
- 继承
AuthorizationServerConfigurerAdapter
类,使用@EnableAuthorizationServer
注解开启认证服务。 - 进行依赖注入,注入7个实例
Bean
对象。
- 重写相关方法进行配置。
关于用户表和权限表的代码可以参考源码,回复“oauth2”获取源码。
各种模式解析
授权码模式
上述内容均基于授权码模式,该模式被认为是最安全的方式,因为令牌获取操作发生在两个服务器之间,大大降低了令牌泄漏的风险。
启动两个服务后,当再次请求127.0.0.1:9002/product/findAll
接口时,可能会遇到以下错误:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
- 发送请求以获取授权码。
请求格式为127.0.0.1:9001/oauth/authorize?response_type=code&client_id=cheetah_one
,其中response_type=code
表示获取授权码,而client_id=cheetah_one
指的是我们在数据库中配置的客户端名称。
该页面是OAuth2的默认页面,输入用户账户密码后点击登录,将提示进行授权。此时,数据库中的oauth_client_details
表因设置autoapprove
为false
,用户需进行手动授权。
点击Approve
并选择Authorize
,将跳转到回调地址,并附带code
值,该值即为授权码。
查看数据库后,oauth_approvals
和oauth_code
表中已成功存入相关数据。
- 利用授权码获取
token
。
成功获取token
后,oauth_access_token
和oauth_refresh_token
表中将存储相关数据,而oauth_code
表中的数据将被清除,因代码只能使用一次,OAuth2设计为避免他人非法请求。
使用获取的token
请求资源服务接口,存在两种请求方式:
简化模式
简化模式是对授权码模式的简化,省略了获取授权码的步骤,直接请求获取token
。
请求格式为127.0.0.1:9001/oauth/authorize?response_type=token&client_id=cheetah_one
,response_type=token
表示直接获取令牌。
用户输入账号密码登录后,浏览器将直接返回token
,可用作访问资源。
该模式的缺点在于,token
直接在浏览器中暴露,极不安全,不建议使用。
密码模式
在密码模式下,用户需要向客户端提供账户和密码以申请令牌,这种模式要求用户对客户端有高度的信任。
请求格式如下:
成功后,可用于访问资源。
客户端模式
在客户端模式中,用户直接在客户端进行注册,客户端向认证服务器获取令牌时不需要提供用户信息,完全脱离用户信息。
请求格式如下:
成功后,同样可以访问资源。
刷新Token以及权限校验
权限校验
除了在数据库中为客户端配置资源服务外,我们还可以动态地为用户分配接口的访问权限。
- 在开启资源服务时,给
ResourceServerConfig
类增加注解@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
。 - 给特定接口增加权限。
- 在用户登录时,设置用户的权限。
测试后发现可以正常访问。
遇到的问题
包名问题
在创建项目时,我为product
和server
两个模块设置了不同的包名,导致请求资源时出错。原因在于,用户信息在登录时会存储到oauth_access_token
表的authentication
字段中,进行令牌校验时,如发现包名不一致,token解析会失败,因此请求资源时失败。
解决方案:
- 统一两个项目的包名;
- 将用户和权限实体抽成单独模块,以供其他模块使用;
- 在
loadUserByUsername
方法中,用户实体类不需继承UserDetailsService
,可直接用用户类封装。
数据库问题
在测试权限校验时,因少打一个单词而导致请求出错。修改后,依然显示权限不足。经过检查发现,需清除数据库中的oauth_refresh_token
和oauth_access_token
数据,重新测试即可。