# oauth2-sample
**Repository Path**: mqb98/oauth2-sample
## Basic Information
- **Project Name**: oauth2-sample
- **Description**: oauth2 学习记录。
https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI1NDY0MTkzNQ==&action=getalbum&album_id=1319833457266163712&scene=173&from_msgid=2247488209&from_itemidx=2&count=3&nolastread=1#wechat_redirect
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2021-04-08
- **Last Updated**: 2023-10-20
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# oauth2-sample
oauth2 学习记录。
https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI1NDY0MTkzNQ==&action=getalbum&album_id=1319833457266163712&scene=173&from_msgid=2247488209&from_itemidx=2&count=3&nolastread=1#wechat_redirect
## OAuth2
> Open Authorization [.ɔθərɪ'zeɪʃ(ə)n]
### 1. 应用场景
OAuth 不是一个框架,而是一个关于授权(authorization)的开放网络标准。
举一个例子来理解 OAuth的适用场景。
有一个 "云冲印" 的网站,可以将用户存储在 Google 的照片,冲印出来。用户为了使用该服务,必须让 "云冲印"读取自己存储在 Google 上的照片。
问题是只有得到用户的授权,Google 才会同意 "云冲印" 读取这些照片。那么,"云冲印" 怎样获得用户的授权呢?
传统方法是,用户将自己的 Google 用户名和密码告诉 "云冲印",后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。
1. "云冲印" 为了后续的服务,会保存用户的密码,这样很不安全。
2. Google 得部署 用户名/密码 登录,但是,单纯的 用户名/密码 的登录并不安全。
3. "云冲印" 拥有了获取用户存储在 Google 的所有资料的权力,用户无法限制 "云冲印" 获得的授权范围和有效期。
4. 用户只有修改密码,才能收回赋予 "云冲印" 的权力。但是这样做,会使得其他所有获得授权的第三方应用程序全部失效。
5. 只要有一个第三方应用程序被破解,就会导致用户名/密码泄露。
OAuth 就是为了解决上面的这些问题而诞生的。
### 2. 名词定义
在详细了解 OAuth2 之前,需要了解几个专用名词。
>1. Third-part application:第三方应用程序。在本文中又称客户端(client),即上面例子中的 "云冲印"
>2. HTTP service:HTTP服务的提供商,本文中简称服务提供商,即上面例子中的 Google
>3. Resource Owner:资源所有者,本文中又称为用户(user)
>4. User Agent:用户代理,本文中值浏览器
>5. Authorization server:认证服务器,即服务提供商专门用于处理认证的服务器。
>6. Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。他与认证服务器可以是同一台服务器,也可以是不同的服务器。
在了解了上面这些名词之后,就不难理解,OAuth的作用就是让 "客户端" 安全可控的获得"用户" 授权,与"服务提供商"进行互动。
### 3. OAuth的思路
OAuth在 客户端 与服务提供商 之间,设置了一个授权层(authorization layer)。
客户端不能直接登录服务提供商,只能登录授权层,以此将用户和客户端区分开来。客户端登录授权层所用的令牌(token),与用户的密码不同,用户可以在登录时,指定授权层令牌的权限范围和有效期。
客户端登录授权层以后,服务提供商根据令牌的权限范围和有效期,向客户端开放用户存储的资料。
### 4. 运行流程
OAuth2 的运行流程如下:

(A) 用户打开客户端(云冲印)以后,客户端要求用户给予授权。
(B) 用户同意给予客户端授权。
(C) 客户端使用上一步获得的授权,向认证服务器申请令牌。
(D) 认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E) 客户端使用令牌,向资源服务器申请获取资源。
(F) 资源服务确认令牌无误,同意向客户端开放资源。
不难看出,上面六个步骤之中,B是关键,即用户怎样才能给客户端授权。有了这个授权之后,客户端就可以获取令牌,进而凭令牌获取资源。
### 5. 客户端授权模式
> http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth2 定义了四种授权方式。
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
### 6. 授权码模式
授权码模式(authorization code) 是功能最完整,流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与 服务提供商 的认证服务器 进行互动。

步骤如下:
(A) 用户访问客户端,后者将前者导向认证服务器。(我打开云冲印页面,选择Google登录,跳转到Goole登录授权界面)
(B)用户选择是否给客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的 "重定向URI"(Redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的 "重定向URI",向认证服务器申请令牌。这一步是在客户端后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)
下面是上面这些步骤所需要的参数。
A 步骤中,客户端申请认证的 URI,包含以下参数
- response_type:表示授权类型,必选项,此处的值固定为 "code"
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的授权范围,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动
示例如下
```http
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
```
C 步骤中,服务器回应客户端的URI,包含以下参数:
- code:表示授权码,必选项,该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应的关系。
- state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
示例如下:
```http
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
```
D 步骤中,客户端向认证服务器申请令牌的 HTTP 请求,包含以下参数:
- grant_type:表示使用的授权模式,必须选,此处的值固定为 "authorization_code"
- code:表示上一步获得的授权码,必须项
- redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致
- client_id:表示客户端ID,必选项
示例如下:
```http
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
```
E 步骤中,认证服务器发送的HTTP回复,包含以下参数:
- access_token:表示访问令牌,必须项
- token_type:表示令牌类型,该值大小写不敏感,必选项
- expires_in:表示过期时间,单位为秒。如果省略该参数,必须使用其他方式设置过期时间。
- refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项
- scope:表示授权范围,如果客户端申请的范围一致,此项可省略
示例如下:
```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
```
可以看到,相关参数使用 JSON 格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存。
### 7. 简化模式
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了授权码的步骤,因此得名。因为所有步骤都在浏览器完成,令牌对访问者是可见的,且客户端不需要认证。

## 授权码模式demo
### 1. 案例架构
授权码模式的这个demo,有如下几个角色
- 第三方应用
- 授权服务器
- 资源服务器
- 用户
| 项目 | 端口 | 备注 |
| --------------- | ---- | ---------- |
| auth-server | 8080 | 授权服务器 |
| resource-server | 8081 | 资源服务器 |
| client-app | 8082 | 第三方应用 |
下面开始搭建项目
### 2. 授权服务器
创建一个名为 auth-server 的 springboot 项目,选择如下依赖:
- web
- spring cloud security
- spring cloud OAuth2
创建完成之后,首先提供一个 SpringSecurity 的基本配置
```java
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("spencer")
.password(new BCryptPasswordEncoder().encode("123"))
.roles("admin")
.and()
.withUser("maqb")
.password(new BCryptPasswordEncoder().encode("123"))
.roles("user");
}
```
为了代码简洁,此处就直接将用户配置在了内存之中。
这里配置的目的,实际上就是配置用户。当我们使用 Google 登录云冲印时,首先要登录 Google,这里配置的相当于就是登录Google要的用户名和密码。
接下来配置授权服务器,重点。
```java
@Configuration
public class AccessTokenConfig {
@Bean
TokenStore tokenStore(){
return new InMemoryTokenStore();
}
}
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Autowired
ClientDetailsService clientDetailsService;
@Bean
AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService);
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
services.setAccessTokenValiditySeconds(60 * 60 * 2);
services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3);
return services;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("myClient")
.secret(new BCryptPasswordEncoder().encode("123"))
.resourceIds("res1")
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.redirectUris("http://localhost:8082/index.html");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authorizationCodeServices(authorizationCodeServices())
.tokenServices(tokenServices());
}
@Bean
AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
}
```
来挨个解释下
1. 首先我们提供了一个 `TokenStore` 的实例,这个是指定你生成的 Token 要往哪里存储,我们可以存储在 Redis 中,也可以存储在内存中,也可以结合 JWT 等等,这里就先把它存在内存中了,所以提供一个 `InMemoryTokenStore`
2. 接下来创建 `AuthorizationServer` 类继承自 `AuthorizationServerConfigurerAdapter`,来对授权服务器做进一步的详细配置。注意这个类得加上 `@EnableAuthorizationServer` 注解,表示开启授权服务器的自动化配置。
3. 在 `AuthorizationServer` 中,主要就是重写了三个 `configure` 方法。
4. `AuthorizationServerSecurityConfigurer` 用来配置令牌端点的安全约束,也就是这个端点谁可以访问,谁不能访问。checkAccess 是指一个 Token 校验的端点,这个端点我们设置为可以直接访问(在后面,当资源服务器收到 Token 后,需要校验 Token 的合法性,就放访问这个端点)
5. `ClientDetailsServiceConfigurer ` 用来配置客户端的详细信息。
授权服务器要进行两方面的校验,一方面是校验客户端,另一方面是校验用户。校验用户这个我们前面在`SecurityConfig` 以及配置了,这里配置的是校验客户端。客户端的信息我们可以存储在数据库中,和用户信息保存到数据库类似。这里为了简单就直接配置在了内存中。
这里分别配置了 id、secret、资源id、授权类型、授权范围以及重定向 uri。授权类型分为4种, refresh_token 不在其中,但是实际操作种,refresh_token 也被算作一种。
6. `AuthorizationServerEndpointsConfigurer` 用来配置令牌的访问端点和令牌服务。authorizationCodeServices 用来配置授权码的存储,这里配置的存储在内存中。tokenServices 用来配置令牌的存储,即 access_token 的存储位置,这里也先配置在内存中。
授权码和令牌区别?授权码是用来获取令牌的,使用一次就失效,令牌则是用来获取资源的。
7. tokenServices 这个 Bean 主要用来配置 Token 的一些基本信息,例如 Token 是否支持刷新、Token 的存储位置、Token 的有效期 以及刷新 token 的有效期等等。Token 有效期这个好理解,刷新 Token 的有效期我说一下,当 Token 快要过期的时候,我们需要获取一个新的 Token,在获取新的 Token 时候,需要有一个凭证信息,这个凭证信息不是旧的 Token,而是另外一个 refresh_token,这个 refresh_token 也是有有效期的。
### 3. 资源服务器搭建
接下来搭建一个资源服务器。网上的 demo 一般是将授权服务器和资源服务器放在一起,如果项目比较小的话,这样做没有问题,但是如果项目比较大,则就需要分开了。
资源服务器就是用来存放用户的资源的,例如在 Goolge 种存储的照片、微信上的图片、openid等信息。用户从授权服务器上拿到 access_token 之后,接下来就可以通过 access_token 来资源服务器请求数据。
创建一个新的 Springboot 项目,名为 resource-server,作为资源服务器,创建时添加如下依赖
项目创建成功后,添加如下配置:
```java
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
RemoteTokenServices tokenServices(){
RemoteTokenServices services = new RemoteTokenServices();
services.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
services.setClientId("myClient");
services.setClientSecret("123");
return services;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("res1").tokenServices(tokenServices());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated();
}
}
```
简单描述下:
1. tokenServices 我们配置了一个 `RemoteTokenServices` 的实例,这是因为资源服务器和授权服务器是分开的,资源服务器和授权服务器如果是放在一起的,就不需要配置 RemoteTokenServices 了
2. RemoteTokenServices 中配置了 access_token 的校验地址、client_id、client_secret 这三个信息,当用户来资源服务器请求资源时,会携带上一个 access_token,通过这里的配置,就能校验 token 是否正确。
3. 最后配置下拦截规则,就是 SpringSecurity 的写法配置。
接下来再配置两个测试接口:
```java
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "admin";
}
}
```
### 4. 第三方应用搭建
第三方应用其实不是必须的,也可以使用 POSTMAN 来进行测试
第三方应用就是一个普通的 Springboot 项目,创建时加入 Thymeleaf 和 Web 依赖
在 resources/template目录下,创建 index.html
```html
你好,maqb!
第三方登录
```
这是一段 Thymeleaf 模板,点击超链接就可以实现第三方登录,超链接参数如下:
- client_id 客户端ID,根据授权服务器中的配置填写
- response_type 表示响应类型,这里是code表示一个授权码
- redirect_uri 表示授权成功后的重定向地址,这里表示回第三方应用的首页
- scope 表示授权范围
h1 标签中的数据是从资源服务器来的,当授权服务器通过之后,我们拿着 access_token 去资源服务器加载数据,加载到的数据在 h1 标签中显示出来。
接下来在定义一个 HelloController
```java
@Controller
public class HelloController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/index.html")
public String index(String code, Model model){
if (code != null) {
MultiValueMap map = new LinkedMultiValueMap<>();
map.add("code", code);
map.add("client_id", "myClient");
map.add("client_secret", "123");
map.add("redirect_uri", "http://localhost:8082/index.html");
map.add("grant_type", "authorization_code");
Map resp =
restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
String access_token = resp.get("access_token");
System.out.println(access_token);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + access_token);
HttpEntity