# oauth2-demo **Repository Path**: demo_focus/oauth2-demo ## Basic Information - **Project Name**: oauth2-demo - **Description**: 使用Spring Security OAuth2实现OAuth2安全协议示例项目 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 3 - **Created**: 2019-05-21 - **Last Updated**: 2024-10-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # oauth2-demo #### 介绍 使用Spring Security OAuth2实现OAuth2安全协议示例项目 ## 演示三种授权许可类型 使用 Postman 来手工测试一下 OAuth 2.0 的授权码许可、资源拥有者凭据许可、客户端凭据许可这三种授权许可类型 ### 资源拥有者凭据许可类型 首先,我们测试的是资源拥有者凭据许可,POST 请求地址是: ``` http://localhost:8080/oauth/token?grant_type=password&client_id=userservice1&client_secret=1234&username=writer&password=writer ``` 得到如下图所示结果: ![img.png](docs/images/img.png) 再使用[JWT 解析工具](https://jwt.io/)看下请求 Token 中的信息: ![img.png](docs/images/img2.png) 可以看到,Token 中果然包含了 Token 增强器加入的 userDetails 自定义信息。如果我们把公钥粘贴到页面的话,可以看到这个 JWT 校验成功了: ![img.png](docs/images/img3.png) 除了本地校验外,还可以访问授权服务器来校验 JWT: ``` http://localhost:8080/oauth/check_token?client_id=userservice1&client_secret=1234&token=... ``` 得到如下结果: ![img.png](docs/images/img4.png) ### 客户端授权许可类型 我们再来测试下客户端授权许可类型。POST 请求地址: ``` http://localhost:8080/oauth/token?grant_type=client_credentials&client_id=userservice2&client_secret=1234 ``` 如下图所示,可以直接拿到 Token: ![img.png](docs/images/img5.png) 这里需要注意的是,并没有提供刷新令牌。这是因为,刷新令牌用于避免访问令牌失效后需要用户再次登录的问题,而客户端授权许可类型没有用户的概念,因此没有刷新令牌,也无法注入额外的 userDetails 信息。 ![img.png](docs/images/img6.png) 也可以试一下,如果我们的授权服务器没有开启 allowFormAuthenticationForClients 参数(允许表单提交认证)的话,客户端的凭证需要通过 Basic Auth 传过去而不是通过 Post: ![img.png](docs/images/img7.png) ### 授权码许可类型 最后,我们来测试下比较复杂的授权码许可。第一步,打开浏览器访问地址: ``` http://localhost:8080/oauth/authorize?response_type=code&client_id=userservice3&redirect_uri=https://baidu.com ``` 注意,客户端跳转地址需要和数据库中配置的一致(百度的 URL https://baidu.com 我们之前已经在数据库中有配置了)。访问后页面会直接跳转到登录界面,我们使用用户名“reader”、密码“reader”来登录: ![img.png](docs/images/img8.png) 由于我们在数据库中设置的是禁用自动批准授权的模式,所以登录后来到了批准界面: ![img.png](docs/images/img9.png) 点击同意后可以看到,数据库中也会产生授权通过记录: ![img.png](docs/images/img10.png) 第二步,我们可以看到浏览器转到了百度并且提供给了我们授权码: ``` https://www.baidu.com/?code=XKkHGY ``` 数据库中也记录了授权码: ![img.png](docs/images/img11.png) 然后 POST 访问下面的地址(code 参数替换为刚才获得的授权码): ``` http://localhost:8080/oauth/token?grant_type=authorization_code&client_id=userservice3&client_secret=1234&code=XKkHGY&redirect_uri=https://baidu.com ``` 可以通过授权码换取访问令牌: ![img.png](docs/images/img12.png) 虽然 userservice3 客户端可以有读权限和写权限,但是因为我们登录的用户 reader 只有读权限,所以最后拿到也只有读权限。 -------------------------------- ## 演示权限控制 现在我们来测试一下之前定义的两个账号,也就是读账号和写账号,看看它们的权限控制是否有效。首先,测试一下我们的安全配置,访问 /user/me 端点不需要认证可以匿名访问: ![img.png](docs/images/img13.png) 访问 /userOpt 需要身份认证: ![img.png](docs/images/img14.png) 不管以哪种模式拿到访问令牌,我们用具有读权限的访问令牌访问资源服务器的如下地址(请求头加入 Authorization: Bearer XXXXXXXXXX,其中 XXXXXXXXXX 代表访问令牌)。可以得到如下结果: ![img.png](docs/images/img15.png) 以 POST 方式访问 http://localhost:8081/userOpt/,显然是失败的: ![img_1.png](docs/images/img_1.png) - 因为这个接口要求有写权限 我们换一个具有读写权限的访问令牌来试试: ![img_2.png](docs/images/img_2.png) 可以发现,果然访问成功了。这里输出的内容是 Token 中的 userDetails 额外信息,说明资源服务器的权限控制有效。 -------------------------------- ## 演示单点登录 启动客户端项目,打开浏览器访问: - http://localhost:8082/ui/securedPage 可以看到,页面自动转到了授权服务器(8080 端口)的登录页面: ![img_3.png](docs/images/img_3.png) 登录后显示了当前用户名和权限: ![img_4.png](docs/images/img_4.png) 我们再启动另一个客户端网站,端口改为 8083,然后访问同样的地址: ![img_5.png](docs/images/img_5.png) 可以看到直接是登录状态,单点登录测试成功。是不是很方便?其实,为了达成单点登录的效果,程序在背后自动实现了多次 302 重定向,整个流程为: ``` http://localhost:8083/ui/securedPage -> http://localhost:8083/ui/login -> http://localhost:8080/oauth/authorize?client_id=userservice3&redirect_uri=http://localhost:8083/ui/login&response_type=code&scope=FOO&state=Sobjqe -> http://localhost:8083/ui/login?code=CDdvHa&state=Sobjqe -> http://localhost:8083/ui/securedPage ``` -------------------- ## 演示客户端请求资源服务器资源 在客户端程序中,我们还定义了一个 remoteCall 接口,直接使用 OAuth2RestTemplate 来访问远程资源服务器的资源。现在,我们来测试一下这个接口是否可以实现自动的 OAuth 流程。访问: - http://localhost:8082/ui/remoteCall 会先转到授权服务器登录,登录后自动跳转回来: ![img_6.png](docs/images/img_6.png) 可以看到输出了用户名,对应的资源服务器服务端接口是: ```java @PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')") @GetMapping("name") public String name(OAuth2Authentication authentication) { return authentication.getName(); } ``` 换一个 writer 用户登录试试,也能得到正确的输出: ![img_7.png](docs/images/img_7.png)