【携手共建 智慧共享】 本帖支持编辑,欢迎参与创作 【你我同行 勠力同心】
Part I. The Philosophy of Oauth2
什么是Oauth
Oauth 全称 Open Authorization,当前版本2,简称Oauth2,是一个关于授权的开放网络标准。日常使用到的第三方登录方式,如Google账号登录,使用的就是这个标准。这个标准允许用户授权第三方应用访问用户存储在其他服务提供者(如Google)上的信息,而不必在不同的平台重复填写注册表单,平台获得用户授权后可直接授权访问用户的Google账户资料核实身份,这个过程中用户并不需要将密码提供给第三方应用。
Oauth涉及到的六方
- Resource Owner (User)
资源所有者,又称 用户
这里的 Resource 指用户的信息,如昵称,头像等等 - User Agent
用户代理,比如浏览器 - Third-Party Application (Client)
第三方应用程式,又称 客户端,比如各大公益站 - HTTP Service
服务提供商,比如 Linux do 论坛 - Authorization Server
授权服务器,即服务提供商提供的用于处理认证授权的服务器 - Resource Server
资源服务器,即服务提供商存储用户资源的服务器
Oauth提供的四种授权模式
Oauth提供了四种不同的授权模式,依次是:授权码模式、简单模式、密码模式和客户端模式。下面将解释各个模式的运行原理和步骤:
Authorization Code
授权码模式
第一步,获取授权码
该步骤涉及的几个重要参数:
- cliet_id: 客户端id,为授权服务器中注册的id
- response_type: 值为code返回授权码用于授权码模式,值为token则返回令牌用于简单模式
- scope: 权限范围,在授权服务器配置
- redirect_url: 重定向 URL,用于授权成功后跳转
授权码请求链接格式如下:
http://localhost:8080/oauth/authorize?client_id=123456&response_type=code&scope=all&redirect_url=http://localhost:8080/oauth/token
返回的json格式如下:
{
“code”: “123456”,
“state”: “123456”
}
第二步,申请令牌
该步骤涉及的几个重要参数:
- cliet_id: 客户端id,为授权服务器中注册的id
- client_secrect: 客户端密码,在授权服务器配置
- grant_type: 授权模式
- code: 授权码,授权码模式独有
- redirect_url: 重定向 URL,用于授权成功后跳转
令牌请求链接格式如下:
http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=authorization_code&code=123456&redirect_url=http://localhost:8080/oauth/callback
返回的json格式如下:
{
“access_token”: “123456”,
“token_type”: “bearer”,
“scope”: “read”,
“refresh_token”: “123456”
}
资源请求链接格式如下:
http://localhost:8080/oauth/resource?access_token=123456
返回的json格式如下:
{
“resource”: “123456”
}
Implicit
简单模式
跳过授权码直接拿到访问令牌,适用于没有后台方服务程序的单页面应用
该步骤涉及的几个重要参数:
- cliet_id: 客户端id,为授权服务器中注册的id
- client_secrect: 客户端密码,在授权服务器配置
- grant_type: 授权模式
- redirect_url: 重定向 URL,用于授权成功后跳转
令牌请求链接格式如下:
http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&response_type==token&scope=all&redirect_url=http://localhost:8080/oauth/callback
返回的json格式如下:
{
“access_token”: “123456”,
“token_type”: “bearer”,
“scope”: “read”,
“refresh_token”: “123456”
}
资源请求同上
Password
密码模式
用户通过客户端使用用户名、密码向授权服务器请求授权,授权服务器向客户端发送访问令牌和更新令牌
该步骤涉及的几个重要参数:
- cliet_id: 客户端id,为授权服务器中注册的id
- client_secrect: 客户端密码,在授权服务器配置
- grant_type: 授权模式
- username: 用户名
- password: 密码
请求链接格式如下:
http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=password&username=admin&password=admin
返回的json格式如下:
{
“access_token”: “123456”,
“token_type”: “bearer”,
“scope”: “read”,
“refresh_token”: “123456”
}
CLient Credentials
客户端模式
最简单的模式,客户端以自己的名义使用用户名、密码向授权服务器请求授权
该步骤涉及的几个重要参数:
- cliet_id: 客户端id,为授权服务器中注册的id
- client_secrect: 客户端密码,在授权服务器配置
- grant_type: 授权模式
请求链接格式如下:
http://localhost:8080/oauth/token?client_id=123456&client_secret=123456&grant_type=client_credentials
返回的json格式如下:
{
“access_token”: “123456”,
“token_type”: “bearer”,
“scope”: “read”,
“refresh_token”: “123456”
}
资源请求同上
Linux do 实战
下面,我将以始皇提供的测试接口为例进行解析,注意:本站使用的是授权码模式。
第一步,获取授权码
authorize endpoint: https://connect.linux.do/oauth2/authorize
示例:
第二步,获取令牌
token endpoint: https://connect.linux.do/oauth2/token
示例:
第三步,获取资源
resource endpoint: https://connect.linux.do/api/user
请求示例:
curl -H "Authorization: Bearer eyJhb_BCDEFGHIJKLMNOPQRSTUVWXYZ" https://connect.linux.do/api/user
返回示例:
{
"id": 124,
"username": "Bee",
"name": "( ⩌ ˰ ⩌)",
"active": true,
"trust_level": 2,
"silenced": false
}
Part II. Software Development Kit (SDK)
Python篇
Requirements
pip install Flask requests
Body
from flask import Flask, session, redirect, request, jsonify
import os
import requests
app = Flask(__name__)
app.secret_key = os.urandom(24)
# OAuth2 参数
CLIENT_ID = 'hi3geJYfTotoiR5S62u3rh4W5tSeC5UG'
CLIENT_SECRET = 'VMPBVoAfOB5ojkGXRDEtzvDhRLENHpaN'
REDIRECT_URI = 'http://localhost:8181/oauth2/callback'
AUTHORIZATION_ENDPOINT = 'https://connect.linux.do/oauth2/authorize'
TOKEN_ENDPOINT = 'https://connect.linux.do/oauth2/token'
USER_ENDPOINT = 'https://connect.linux.do/api/user'
@app.route('/oauth2/initiate')
def initiate_auth():
session['oauth_state'] = os.urandom(16).hex()
authorization_url = f"{AUTHORIZATION_ENDPOINT}?client_id={CLIENT_ID}&response_type=code&redirect_uri={REDIRECT_URI}&state={session['oauth_state']}"
return redirect(authorization_url)
@app.route('/oauth2/callback')
def callback():
code = request.args.get('code')
state = request.args.get('state')
if state != session.get('oauth_state'):
return 'State value does not match', 401
# 请求token
auth = requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET)
data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': REDIRECT_URI
}
headers = {'Accept': 'application/json'}
response = requests.post(TOKEN_ENDPOINT, auth=auth, data=data, headers=headers)
if response.status_code == 200:
# 这里获取的rt根据自己的实际进行处理 比如放入会话或数据库里
user_response = requests.get(USER_ENDPOINT, headers={'Authorization': 'Bearer ' + response.json()['access_token']})
if user_response.status_code == 200:
return jsonify(user_response.json())
else:
return 'Failed to fetch user info', user_response.status_code
else:
return 'Failed to fetch access token', response.status_code
if __name__ == '__main__':
app.run(debug=True, port=8181)
浏览器访问 http://localhost:8181/oauth2/initiate
Java篇
Requirements
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>demo-OAuth2</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
Application.yml
server:
port: 8181
linux-do:
oauth2:
client:
registration:
client-id: hi3geJYfTotoiR5S62u3rh4W5tSeC5UG
client-secret: VMPBVoAfOB5ojkGXRDEtzvDhRLENHpaN
redirect-uri: http://localhost:8181/oauth2/callback
authorization-grant-type: authorization_code
scope: read,write
provider:
authorization-uri: https://connect.linux.do/oauth2/authorize
token-uri: https://connect.linux.do/oauth2/token
user-info-uri: https://connect.linux.do/api/user
user-name-attribute: id
Body
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Map;
@RestController
@RequestMapping("/oauth2")
public class OAuth2Controller {
@Value("${linux-do.oauth2.client.registration.client-id}")
private String clientId;
@Value("${linux-do.oauth2.client.registration.client-secret}")
private String clientSecret;
@Value("${linux-do.oauth2.client.registration.redirect-uri}")
private String redirectUri;
@Value("${linux-do.oauth2.client.provider.authorization-uri}")
private String authorizationEndpoint;
@Value("${linux-do.oauth2.client.provider.token-uri}")
private String tokenEndpoint;
@Value("${linux-do.oauth2.client.provider.user-info-uri}")
private String userEndpoint;
@GetMapping("/initiate")
public void initiateAuth(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
String state = new BigInteger(130, new SecureRandom()).toString(32);
session.setAttribute("oauth2State", state);
response.sendRedirect(String.format("%s?client_id=%s&response_type=code&redirect_uri=%s&scope=%s&state=%s",
authorizationEndpoint, clientId, redirectUri, "read,write", state));
}
@GetMapping("/callback")
public String handleAuthorizationCode(@RequestParam("code") String code, @RequestParam("state") String state, HttpServletRequest request) {
String sessionState = (String) request.getSession().getAttribute("oauth2State");
if (sessionState == null || !sessionState.equals(state)) {
return "State mismatch error";
}
// 创建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
String auth = clientId + ":" + clientSecret;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
headers.add("Authorization", "Basic " + encodedAuth);
// 使用授权码请求访问令牌
MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();
requestBody.add("grant_type", "authorization_code");
requestBody.add("code", code);
requestBody.add("redirect_uri", redirectUri);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Map> response = restTemplate.postForEntity(tokenEndpoint, requestEntity, Map.class);
Map<String, Object> responseBody = response.getBody();
// 处理响应,例如提取和返回访问令牌
if (responseBody != null && responseBody.containsKey("access_token")) {
HttpHeaders userHeaders = new HttpHeaders();
userHeaders.setBearerAuth(responseBody.get("access_token").toString());
HttpEntity<String> entity = new HttpEntity<>(userHeaders);
ResponseEntity<Map> userResponse = restTemplate.exchange(userEndpoint, HttpMethod.GET, entity, Map.class);
Map<String, Object> userResBody = userResponse.getBody();
if (userResBody != null) {
return userResBody.toString();
} else {
return "Failed to obtain user details";
}
} else {
return "Failed to obtain access token";
}
}
}
Nodejs篇
Requirements
npm install express axios express-session qs
Body
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const qs = require('qs');
const app = express();
const port = 8181;
// OAuth2 参数
const CLIENT_ID = 'hi3geJYfTotoiR5S62u3rh4W5tSeC5UG';
const CLIENT_SECRET = 'VMPBVoAfOB5ojkGXRDEtzvDhRLENHpaN';
const REDIRECT_URI = 'http://localhost:8181/oauth2/callback';
const AUTHORIZATION_ENDPOINT = 'https://connect.linux.do/oauth2/authorize';
const TOKEN_ENDPOINT = 'https://connect.linux.do/oauth2/token';
const USER_ENDPOINT = 'https://connect.linux.do/api/user';
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 使用 session 来保存 state 和其他 OAuth2 相关信息
const session = require('express-session');
app.use(session({
secret: crypto.randomBytes(24).toString('hex'),
resave: false,
saveUninitialized: true
}));
app.get('/oauth2/initiate', (req, res) => {
req.session.oauthState = crypto.randomBytes(16).toString('hex');
const authorizationUrl = `${AUTHORIZATION_ENDPOINT}?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${REDIRECT_URI}&state=${req.session.oauthState}`;
res.redirect(authorizationUrl);
});
app.get('/oauth2/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.oauthState) {
console.error('State验证失败');
return res.status(401).send('State value does not match');
}
try {
const data = qs.stringify({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI
});
const tokenResponse = await axios.post(TOKEN_ENDPOINT, data, {
auth: {
username: CLIENT_ID,
password: CLIENT_SECRET
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
const userResponse = await axios.get(USER_ENDPOINT, {
headers: { 'Authorization': `Bearer ${tokenResponse.data.access_token}` }
});
console.log('User response:', userResponse.data);
res.json(userResponse.data);
} catch (error) {
console.error('Error during token fetch or user info retrieval:', error.message);
// 更详细地输出错误信息
if (error.response) {
console.error('Error response data:', error.response.data);
console.error('Error response status:', error.response.status);
console.error('Error response headers:', error.response.headers);
} else if (error.request) {
console.error('No response received:', error.request);
} else {
console.error('Error', error.message);
}
return res.status(500).send('Failed to fetch access token');
}
});
app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
未完待续,SDK还需完善,欢迎各位指正