虾皮面试原题:深入剖析OAuth2的核心概念及其在实际应用中的多种场景,帮助你更好地理解和运用这一授权机制

在此分享一个群友在面试虾皮时关于OAuth2的实际问题。

什么是OAuth2

OAuth是一种开放网络标准,主要用于授权(authorization),允许第三方应用安全地访问用户的私人信息。当前流行的版本为2.0。

使用场景分析

如果你正在“网站 A”浏览内容,发现一篇极具吸引力的帖子,想要点赞时,系统会提示你先登录。

图片 在登录页面,除了基本的账户密码登录方式外,还有如微博、微信、QQ等便捷的登录选项。选择快捷登录后,系统会指引你扫码或输入账号密码进行身份验证。

图片 当你成功登录后,相关的信息如昵称和头像会自动填充至“网站 A”中,此时你便可以顺利进行点赞操作。

基本概念解析

在深入探讨OAuth2之前,我们需要了解几个关键名词:

  • Client:客户端,负责请求资源服务器的资源,但不存储用户的快捷登录信息。
  • Resource Owner:资源拥有者,通常是用户,指的是拥有QQ或微信账号的人。
  • Authorization Server:认证服务器,提供用户身份验证和授权的服务,负责发放和验证token
  • Resource Server:资源服务器,存储用户信息的服务器,例如QQ和微信的数据。

认证流程详解

图片 上述图展示了OAuth2的认证流程:

  1. 客户端向资源拥有者请求授权。
  2. 资源拥有者同意并返回授权码。
  3. 客户端使用授权码向认证服务器请求令牌(token)。
  4. 认证服务器验证客户端身份并发放令牌。
  5. 客户端使用获得的令牌向资源服务器请求数据。
  6. 资源服务器验证令牌后返回相应的信息给客户端。

为增强理解,以下是我为大家绘制的简图:

图片 通过以上流程,相信大家对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_typeauthorization_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组合拳配置相似,不太了解的同学可以先查看该文。

  1. 将实现了UserDetailsServiceISysUserServiceSysUserServiceImpl类重写loadUserByUsername方法。

图片

  1. 继承WebSecurityConfigurerAdapter类,增加@EnableWebSecurity注解并重写相关方法。

图片

AuthorizationServer配置

  1. 继承AuthorizationServerConfigurerAdapter类,使用@EnableAuthorizationServer注解开启认证服务。
  2. 进行依赖注入,注入7个实例Bean对象。

图片

  1. 重写相关方法进行配置。

图片

关于用户表和权限表的代码可以参考源码,回复“oauth2”获取源码。

各种模式解析

授权码模式

上述内容均基于授权码模式,该模式被认为是最安全的方式,因为令牌获取操作发生在两个服务器之间,大大降低了令牌泄漏的风险。

启动两个服务后,当再次请求127.0.0.1:9002/product/findAll接口时,可能会遇到以下错误:

{  
    "error": "unauthorized",  
    "error_description": "Full authentication is required to access this resource"  
}  
  1. 发送请求以获取授权码。

请求格式为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表因设置autoapprovefalse,用户需进行手动授权。

图片 点击Approve并选择Authorize,将跳转到回调地址,并附带code值,该值即为授权码。

图片 查看数据库后,oauth_approvalsoauth_code表中已成功存入相关数据。

  1. 利用授权码获取token

图片 成功获取token后,oauth_access_tokenoauth_refresh_token表中将存储相关数据,而oauth_code表中的数据将被清除,因代码只能使用一次,OAuth2设计为避免他人非法请求。

使用获取的token请求资源服务接口,存在两种请求方式:

图片图片

简化模式

简化模式是对授权码模式的简化,省略了获取授权码的步骤,直接请求获取token

图片 请求格式为127.0.0.1:9001/oauth/authorize?response_type=token&client_id=cheetah_oneresponse_type=token表示直接获取令牌。

用户输入账号密码登录后,浏览器将直接返回token,可用作访问资源。

图片

该模式的缺点在于,token直接在浏览器中暴露,极不安全,不建议使用

密码模式

在密码模式下,用户需要向客户端提供账户和密码以申请令牌,这种模式要求用户对客户端有高度的信任。

图片 请求格式如下:

图片 成功后,可用于访问资源。

客户端模式

在客户端模式中,用户直接在客户端进行注册,客户端向认证服务器获取令牌时不需要提供用户信息,完全脱离用户信息。

图片 请求格式如下:

图片 成功后,同样可以访问资源。

刷新Token以及权限校验

图片

权限校验

除了在数据库中为客户端配置资源服务外,我们还可以动态地为用户分配接口的访问权限。

  1. 在开启资源服务时,给ResourceServerConfig类增加注解@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  2. 给特定接口增加权限。

图片

  1. 在用户登录时,设置用户的权限。

图片

测试后发现可以正常访问。

遇到的问题

包名问题

在创建项目时,我为productserver两个模块设置了不同的包名,导致请求资源时出错。原因在于,用户信息在登录时会存储到oauth_access_token表的authentication字段中,进行令牌校验时,如发现包名不一致,token解析会失败,因此请求资源时失败。

解决方案

  • 统一两个项目的包名;
  • 将用户和权限实体抽成单独模块,以供其他模块使用;
  • loadUserByUsername方法中,用户实体类不需继承UserDetailsService,可直接用用户类封装。

数据库问题

在测试权限校验时,因少打一个单词而导致请求出错。修改后,依然显示权限不足。经过检查发现,需清除数据库中的oauth_refresh_tokenoauth_access_token数据,重新测试即可。