一、理论
OAuth 是一个关于授权(authorization)的开放网络标准,用来授权第三方应用获取用户数据,是目前最流行的授权机制,它当前的版本是 2.0。
假如你正在“网站 A”上冲浪,看到一篇帖子表示非常喜欢,当你情不自禁的想要点赞时,它会提示你进行登录操作。
打开登录页面你会发现,除了最简单的账户密码登录外,还为我们提供了微博、微信、QQ 等快捷登录方式。假设选择了快捷登录,它会提示我们扫码或者输入账号密码进行登录。
登录成功之后便会将 QQ / 微信 的昵称和头像等信息回填到“网站 A”中,此时你就可以进行点赞操作了。
在详细讲解 OAuth2 之前,我们先来了解一下它里边用到的名词定义吧:
如图是 OAuth2 官网的认证流程图,我们来分析一下:
为了大家更好的理解,我特地画了一张图:
到这儿,相信大家对理论知识已经掌握的差不多了,接下来我们就进入实战训练吧。
在正式开始搭建项目之前我们先来做一些准备工作:要想使用 OAuth2 的服务,我们得先创建几张表。
OAuth2 相关的建表语句可以参考官方初始化 SQL,也可以查看项目中的 init.sql 文件。
至于表结构,大家可以先大体了解下,其中字段的含义,在 init.sql 文件已经做了说明。
在 oauth_client_details 表中添加一条数据:
数据库中对密码进行了加密处理,大家可以在此路径下自行生成:
用户角色相关的表也在 init.sql 文件中,表结构非常简单,大家自行查阅。我的初始化数据为:
依赖引入:
至于其它依赖,大家可以根据需要自行引入,不再赘述。
配置文件对服务端口、应用名称、数据库、MyBatis 和日志进行了配置。
写了一个简单的控制层代码,用来模拟资源访问:
接着创建配置类继承 ResourceServerConfigurerAdapter 并增加 @EnableResourceServer 注解开启资源服务,重写两个 configure 方法:
当然我们也可以配置忽略校验的 url,在上边的 public void configure(HttpSecurity http) throws Exception 中进行配置:
因为我们是需要进行校验的,所以我把对应的代码给注释掉了。
然后将实现了 UserDetails的SysUser 和实现了 GrantedAuthority的SysRole 放到项目中,当请求发过来时,oauth2 会帮我们自行校验。
配置文件对服务端口、应用名称、数据库、MyBatis 和日志进行了配置。
Security 配置
第一步,将继承了 UserDetailsService 的 ISysUserService 的实现类 SysUserServiceImpl 重写 loadUserByUsername 方法:
第二步,继承 WebSecurityConfigurerAdapter 类,增加 @EnableWebSecurity 注解并重写方法:
AuthorizationServer 配置
第一步,继承 AuthorizationServerConfigurerAdapter 类,增加 @EnableAuthorizationServer 注解开启认证服务。
第二步、依赖注入,注入 7 个实例 Bean 对象。
第三步、重写方法进行配置。
我们前边所讲的内容都是基于授权码模式,授权码模式被称为最安全的一种模式,它获取令牌的操作是在两个服务端进行的,极大的减小了令牌泄漏的风险。
启动两个服务,当我们再次请求 127.0.0.1:9002/product/findAll 接口时会提示以下错误:
调用接口获取授权码
发送 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 按钮,会发现我们设置的回调地址(oauth_client_details 表中的 web_server_redirect_uri)后边拼接了 code 值,该值就是授权码。
查看数据库发现 oauth_approvals 和 oauth_code 表已经存入数据了。
拿着授权码去获取 token
获取到 token 之后 oauth_access_token 和 oauth_refresh_token 表中会存入数据以用于后边的认证。而 oauth_code 表中的数据被清除了,这是因为 code 值是直接暴漏在网页链接上的,OAuth2 为了防止他人拿到 code 非法请求而特意设置为仅用一次。
拿着获取到的 token 去请求资源服务的接口,此时有两种请求方式:
接下来我们再来看一下 oauth2 的其它模式。
所谓简化模式是针对授权码模式进行的简化,它将授权码模式中获取授权码的步骤省略了,直接去请求获取 token。
流程 :发送请求 127.0.0.1:9001/oauth/authorize?response_type=token&client_id=cheetah_one 跳转到登录页进行登录,response_type=token 表示获取 token。
输入账号密码登录之后会直接在浏览器返回 token,我们就可以像授权码方式一样携带 token 去请求资源了。
该模式的弊端就是 token 直接暴漏在浏览器中, 非常不安全,不建议使用 。
密码模式下,用户需要将账户和密码提供给客户端向认证服务器申请令牌,所以该种模式需要用户高度信任客户端。
流程:请求如下
获取成功之后可以去访问资源了。
客户端模式已经不太属于 OAuth2 的范畴了,用户直接在客户端进行注册,然后客户端去认证服务器获取令牌时不需要携带用户信息,完全脱离了用户,也就不存在授权问题了。
发送请求如下:
获取成功之后可以去访问资源了。
除了我们在数据库中为客户端配置资源服务外,我们还可以动态的给用户分配接口的权限。
第一步,开启 Security 内置的动态配置。
在开启资源服务时给 ResourceServerConfig 类增加注解 @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
第二步,给接口增加权限。
第三步,在用户登录时设置用户权限。
然后测试会发现可以正常访问。
当我在创建项目的时候,给 product 和 server 两个模块设置了不同的包名,导致发送请求获取资源时报错。
经过分析得知,在登录账号时会将用户的信息存储到 oauth_access_token 表的 authentication 中,在进行 token 校验时会根据 token_id 取出该字段进行反序列化,如果此时发现包名不一致便会导致解析 token 失败,因此请求资源失败。
解决思路
当我在进行权限校验测试时,在设置权限时发现少打了一个单词,导致请求一直出错。修改完成之后继续请求,仍提示权限不足。
于是我将数据库中 oauth_refresh_token 和 oauth_access_token 的数据清除,重新开始测试就可以了。
个人认为是生成 token 时发现数据库中 token 存在,故不刷新 token,但进行校验时却用带有权限标识的 token 前去校验导致失败。