appauth4cj 库用于安全的用户登录和访问受保护资源,遵循 OAuth 2.0 和 OpenID Connect 业界标准协议。
介绍
appauth4cj 库,帮助应用安全地实现用户登录和访问受保护资源的功能。 它遵循 OAuth 2.0 和 OpenID Connect 这两个业界标准协议。
效果展示
特性
- 🚀 帮助用户通过第三方服务登录你的应用
- 🚀 获取访问用户数据的权限,比如获取用户的邮箱、头像等信息
- 🚀 支持兼容 OAuth 2.0/OpenID Connect 的服务
- 🚀 提供授权信息的序列化和反序列化,方便存储
软件架构
OAuth 2.0

注:图片来自华为账号服务OAuth 2.0鉴权说明文档
具体步骤
-
用户点击登录按钮:用户在你的应用中点击"使用 华为账号 登录"
-
打开浏览器进行认证:AppAuth 自动打开 华为账号 的登录页面
-
用户输入凭据:用户在页面输入用户名和密码
-
获取授权码:登录成功后,华为账号服务器 返回一个临时的授权码给你的应用
-
换取访问令牌:AppAuth 使用授权码向 华为账号服务器 换取访问令牌
-
访问用户信息:使用访问令牌获取用户信息(如姓名、邮箱等)
-
自动刷新令牌:当令牌过期时,自动刷新获取新令牌
OpenID Connect
OpenID Connect 在OAuth2.0协议上完善了身份认证
只使用 OAuth2.0 时的请求和响应结构结构,授权服务器正常返回令牌
请求
POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
code=xxxxxxxxxxx&
client_id=your_client_id&
client_secret=your_client_secret_only_known_by_server&
redirect_uri=https%3A//oauth2.example.com/code
响应
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "dasdqwdqd",
"token_type": "Bearer",
"refresh_token": "casdqwfw",
"expires_in": 3600
}
增加OpenID Connect后,请求和响应都有改变,授权服务器会多返回ID令牌
请求
POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
response_type=code&
scope=openid%20profile%20email // 增加openid
client_id=your_client_id&
client_secret=your_client_secret_only_known_by_server&
redirect_uri=https%3A//oauth2.example.com/code
响应
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "dasdqwdqd",
"token_type": "Bearer",
"refresh_token": "casdqwfw",
"expires_in": 3600,
"id_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // id令牌是jwt结构的编码令牌,可以解码出如邮箱、用户标识、令牌有效时间等信息
}
源码目录
/appauth4cj # 项目根目录
├── doc # 文档目录
│ ├── assets # 文档资源目录
│ └── feature_api.md # API接口文档
├── entry # 示例代码文件夹
├── appauth # appauth库文件夹
│ └─ src/main/cangjie # 核心代码
├── README.md # 安装使用方法
接口说明
主要类和函数接口说明详见 API
编译构建
stdx的配置:
-
下载 stdx 并解压到 ${DEVECO_OH_NATIVE_HOME} 目录下
- 如果是单独安装的cangjie插件,那么 ${DEVECO_OH_NATIVE_HOME} 目录一般在 C:\Users\你的用户名.cangjie-sdk\5.1\cangjie
- 如果是一体化的版本,那么 ${DEVECO_OH_NATIVE_HOME} 目录一般在 你的DevEco安装目录\plugins\cangjie\sdk\cangjie
-
在 entry 和 appauth 文件夹下的 cjpm.toml 文件中添加stdx的配置 初始cjpm.toml文件
[target]
[target.aarch64-linux-ohos]
compile-option = "......"
[target.aarch64-linux-ohos.bin-dependencies]
path-option = [ "...", "...", "..."]
[target.aarch64-linux-ohos.bin-dependencies.package-option]
[target.x86_64-linux-ohos]
compile-option = "......"
[target.x86_64-linux-ohos.bin-dependencies]
path-option = [ "...", "...", "..."]
[target.x86_64-unknown-windows-gnu]
[target.x86_64-unknown-windows-gnu.bin-dependencies]
path-option = [ "...", "..." ]
[target.x86_64-unknown-windows-gnu.bin-dependencies.package-option]
分别在每个 xxx.bin-dependencies 的 path-option 里面添加 stdx 的路径,注意对应不同的文件夹名称
[target]
[target.aarch64-linux-ohos]
compile-option = "......"
[target.aarch64-linux-ohos.bin-dependencies]
path-option = [ "...", "...", "...", "${DEVECO_CANGJIE_HOME}/linux_ohos_aarch64_llvm/dynamic/stdx" ]
[target.aarch64-linux-ohos.bin-dependencies.package-option]
[target.x86_64-linux-ohos]
compile-option = "......"
[target.x86_64-linux-ohos.bin-dependencies]
path-option = [ "...", "...", "...", "${DEVECO_CANGJIE_HOME}/linux_ohos_x86_64_llvm/dynamic/stdx" ]
[target.x86_64-unknown-windows-gnu]
[target.x86_64-unknown-windows-gnu.bin-dependencies]
path-option = [ "...", "...", "${DEVECO_CANGJIE_HOME}/windows_x86_64_llvm/dynamic/stdx" ]
[target.x86_64-unknown-windows-gnu.bin-dependencies.package-option]
1.module方式引入
- 克隆下载项目
- 将 appauth 模块拷贝到应用项目下
- 修改自身应用 entry 下的 oh-package.json5 文件,在 dependencies 字段添加 "appauth": "file:../appauth"
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {
"appauth": "file:../appauth"
}
}
- 同步应用 entry/ 下的 cjpm.toml 文件,会自动添加依赖,如果没有可以手动在 [dependencies] 字段下添加 appauth = {path = "../../../../appauth", version = "1.0.0"},这里需要配置自己机器上的stdx,具体配置步骤见stdx说明。
[dependencies]
appauth = {path = "../../../../appauth", version = "1.0.0"}
- 在项目中引用
import appauth.*
2.三方库方式引入
- 目标工程把 appauth4cj 依赖库作为 git submodule 引入
> cd $工程根目录
> mkdir third-party
> cd third-party
> git submodule add "https://gitcode.com/Cangjie-TPC/appauth4cj.git"
- 修改自身应用 entry/ 下的 cjpm.toml 文件,添加依赖
[dependencies]
appauth = {path = "../../../../third-party/appauth4cj/appauth", version = "1.0.0"}
- 在项目中引用
import appauth.*
使用说明
功能示例
@Entry
@Component
public class MainRegistration {
// -------- 授权平台的相关信息
// 在授权平台上注册的应用的id
var cliendId: String = ""
// 授权平台生成的应用的密钥
var clientSecret = ""
// 授权信息回调地址(一般在授权平台上自定义填写)
var redirectUri: String = ""
// 授权范围(不同授权平台的范围可能有不同,参考授权平台的相关文档)
var authorizationScope: String = "openid email"
// 授权请求地址
var authorizationEndpointUri: String = ""
// 令牌请求地址
var tokenEndpointUri: String = ""
// discovery文档地址(discovery文档一般会保存所有的授权相关的信息)
var discoveryUri: String = ""
// 授权模式,code表示授权码模式
var responseType: String = "code"
// 用户信息请求地址---非必须,有的平台可能不提供
var userInfoEndpointUri: String = ""
// 动态注册请求地址---非必须,有的平台可能不提供
var registrationEndpointUri: String = ""
// 令牌注销请求地址---非必须,有的平台可能不提供
var endSessionEndpoint: String = ""
public let TAG: String = "MainRegistration"
let KEY_STATE: String = "state"
// 上下文对象
let stageContext = Global.getCurrentStageContext()
// Preferences轻量级数据库,用于本地持久化保存
let preferences = Preferences.getPreferences(stageContext, "myPreferences")
// Webview控制器
var webController: WebviewController = WebviewController()
@State // Webview初始加载地址
var currentUrl: String = "https://www.test.com"
// 授权信息记录对象,用于持久化保存
var authState: ?AuthState = Option<AuthState>.None
// 授权请求服务
var service: AuthorizationService = AuthorizationService()
var authorizationRequestString: String = ""
// 本地保存时需要用到的锁
let mPrefsLock: ReentrantMutex = ReentrantMutex()
func build() {
Column {
Button("webView加载授权请求界面")
.fontSize(14).width(100.percent).margin(5)
.onClick {
=>
Hilog.error(666, TAG, "------------> webView加载授权请求界面")
var request = createAuthRequest()
authorizationRequestString = request.jsonSerializeString()
webController.loadUrl(request.toUri().toString())
}
Web(src: currentUrl, controller: webController)
.width(100.percent)
.onPageBegin({event: OnPageEvent =>
Hilog.error(666, TAG, "------------> onPageBegin")
})
.onPageEnd({event: OnPageEvent =>
Hilog.error(666, TAG, "------------> onPageEnd: ${event.url}")
// webView上登录/授权后,接收到的返回信息,是授权平台填写的回调地址和code授权码、state状态标记
getToken(event.url)
})
.onLoadIntercept({event: WebResourceRequest =>
Hilog.error(666, TAG, "------------> onLoadIntercept")
return false
})
}
}
// 构建授权请求对象
public func createAuthRequest(): AuthorizationRequest {
var builderAuthorization = AuthUri.parse(authorizationEndpointUri).buildUpon()
var builderToken = AuthUri.parse(tokenEndpointUri).buildUpon()
var configuration = AuthorizationServiceConfiguration(builderAuthorization.build(), builderToken.build())
var redirectUriTmp = AuthUri.parse(redirectUri)
var builder = AuthorizationRequest_Builder(configuration, cliendId, responseType, redirectUriTmp).setScope(authorizationScope)
return builder.build()
}
// 处理授权请求返回,发起token请求
public func getToken(response: String) {
// 判断返回值是不是以回调地址开头
// 是,表示是需要处理的返回值
// 不是,表示可能是后续重定向之类的,不做处理
if (!response.startsWith(redirectUri)) {
Hilog.error(666, TAG, "------------> 拦截非token请求")
return
}
var responseUri = AuthUri.parse(response).buildUpon().build()
var errorMsg = responseUri.getQueryParameter("error")
if (!errorMsg.isEmpty()) {
Hilog.error(666, TAG, "------------> 返回值报错:${errorMsg}")
return
}
var authorizationRequest = AuthorizationRequest.jsonDeserialize(authorizationRequestString)
// 因为是直接用应用接收返回值,需要手动构造AuthorizationResponse对象
var authorizationResponse = AuthorizationResponse_Builder(authorizationRequest)
.setState(responseUri.getQueryParameter("state"))
.setAuthorizationCode(responseUri.getQueryParameter("code"))
.build()
var callBack = { response: ?TokenResponse, ex: ?AuthorizationException =>
if (ex.isNone() && response.isSome()) {
var currentState = getCurrentState()
currentState.update(response, ex)
replace(currentState)
Hilog.error(666, TAG, "------------> 请求令牌成功")
return true
} else {
Hilog.error(666, TAG, "------------> 请求令牌失败:${ex.getOrThrow().message}")
return false
}
}
var request: TokenRequest = authorizationResponse.createTokenExchangeRequest()
service.performTokenRequest(request, ClientSecretPost(clientSecret), callBack)
}
}
约束与限制
在下述版本验证通过:
DevEco Studio 5.1.1 Release (5.1.1.823)
Cangjie Support Plugin 5.1.1.823
Cangjie Compiler: 1.0.1 (cjnative)
开源协议
本项目基于 Apache License 2.0,请自由的享受和参与开源。
参与贡献
欢迎给我们提交PR,欢迎给我们提交Issue,欢迎参与任何形式的贡献。