经验总结:关于为 JAP 开发不同语言的 Demo 的总结
# 经验总结:关于为 JAP 开发不同语言的 Demo 的总结
# 前言
各位参与开源的导师、同学们,大家好,我是来自四川成都的一名准大三学生,怀着忐忑的心情在大二暑假前向 JustAuth (opens new window)社区提交了简历,很荣幸成功参与进人生中的第一次开源软件计划。下面由我简单分享一下此次的开源项目经验。
# 社区简介
JustAuth开源社区 (opens new window), 致力于为开源的授权/身份认证技术开发、布道。以开源之名,赋能开发者。名下开源项目 JustAuth 深受开发者喜欢,在 Gitee (opens new window)和 Github (opens new window)中的累计关注量近 19K,并荣获 Gitee GVP 称号,目前已有多个企业(中文在线、北京市公园管理中心、前海人寿等)、组织和开源项目(Shiro Action、sika、七腾智能、BladeX、GUNS、MaxKey、mica)正在使用 JustAuth 完成极速的第三方登录集成。
JustAuth 致力于让开发者们脱离繁琐的第三方登录SDK,简化登录开发过程,做到开箱即用
同时基于 JustAuth,结合目前流行的登录技术(OAuth 2.0,SAML、OIDC、LDAP、CAS等)又开源出了 JustAuthPlus。
JustAuthPlus (opens new window)(以下简称"JAP")于 2021-01-12 开始筹划建立,是一款开源的登录认证中间件,基于模块化设计,为所有需要登录认证的 WEB 应用提供一套标准的技术解决方案,开发者可以基于 JAP 适配绝大多数的 WEB 系统(自有系统、联邦协议)。
JAP 从 JustAuth 中衍生而出,同样做到了开箱即用的程度,至 2021-09-24,已迭代更新5个版本,添加数十种功能。JAP 高度抽象各种登录场景,提供多套简单实用的 API,高度注重登录认证的安全性,为每一种登录场景(与开发语言无关)都提供了独有的模块化解决方案,支持 Form、 Auth2.0、OIDC、Http Basic、Digest、Bearer、LDAP、SAML、MFA、SSO 等。开发设计至今已有中文在线,符节科技,六牛科技,Mica 等多家企业/组织使用。已于2021年9月荣获 **GVP(码云最具有价值开源项目)**称号。
截止目前,JustAuth 和 JustAuthPlus 项目收藏量已接近 20K 左右,项目关注量持续增加。
JAP 功能模块说明:
# 项目介绍
项目名称:完成 JFinal、Blade、ActFramework 框架集成 JustAuthPlus 的 demo
项目仓库:
- GitHub:
- 后端:https://github.com/fujieid/jap-jfinal-blade-actframework-demo
- 前端:https://github.com/fujieid/jap-jfinal-blade-actframework-demo/tree/VuePro
此次开发我个人采用的是在合理的时间规划后,以前后端分离方式开发实现 第三方登录,OAuth2登录,OIDC登录,账号密码登录这几种登录方式。此次以后端的OAuth2和OIDC登录为主要分享经验。 以面向小白为主,我以浅显易懂的方式编写这篇文章,希望能够有所学有所思有所用。
- 什么是OAuth2.0?
- 什么是OIDC?
- 什么是令牌Token?
- 什么是CSRF?
- 什么是PKCE?有何作用?
- JAP如何做到开箱即用,如何简化开发者开发设计?
等等,以上这些都是基础程序员必须需要了解和掌握的基础知识。
# OAuth2.0协议
官方定义:
The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf.This specification replaces and obsoletes the OAuth 1.0 protocol described in RFC 5849.
简单来说就是一种授权机制(协议),形象化地说就是他人(第三方)需要进入资源区获得资源,而设立的一个中间屏障(授权层),用于验证第三方身份的安全性和保护资源的安全使用。而在能够成功使用资源这一目的之前,需要经过多次的身份验证和授权。结合一位经验丰富的取货人需要去仓库负责人处拿到入库取货凭证,并到仓库运货为例。注:取货人是该仓库的一名帮工,并之前已经登记注册帮工名单。
下面简单介绍在这验证授权期间使用的4个专有名词:
client:An application making protected resource requests on behalf of the resource owner and with its authorization.关键字:一个应用程序、资源请求、已被资源所有者授权。例如“取货人”,需要向仓库负责人请示:是否自己有任务去仓库取货。
resource owners:An entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end-user.关键字:接收授权请求,授权entity ,访问保护资源。例如“仓库负责人”,需要对取货人的请示进行验证:验证这个取货者是否是在帮工名册上,是否安全,若完成检查,则仓库负责人给取货人颁发一个凭证Authorization Grant 。
authorization server:The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization.关键字:接收Authorization Grant,颁发令牌Token。例如“仓库保安”,收到从取货人拿出的Authorization Grant,验证无误后,让其取货者找资源服务管理部门(该部门管理货物的搬运)。
resource server:The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens.关键字:接受使用来自”仓库保安“(授权服务器)令牌Token的访问,资源请求。例如“资源服务管理部门”,取货者拿出令牌,该资源服务部门验证收到Token的有效性和安全性后,给取货者运输货物。
另外,我以图例作为补充,帮助你了解这4段文字的具体含义:
官方示例图:
这里简单介绍什么是令牌Token?
简单来说就是用户第一次登录,服务器生成的一个字符串,以此作为客户端在进行请求的一个标志。而在以后的的请求数据,客户端只需要带上这个Token即可,不再需要每次带上用户名和密码。
Token的使用方式?
可以是你设备的MAC地址(设备唯一标识符,用于数据链路层的MAC子层)也可以是session值。
这里不展开细讲。你只需要知道,这是一个用于安全的字符串,以保证客户端和服务端的通信安全。
更细化地学习,你会知道,一个authorization grant总共有四种模式可以选择,以用于第三方获取令牌,换取受保护资源。四种模式如下:
- Authorization Code
- Implicit
- Resource Owner Password Credentials
- Client Credentials
此处只介绍Authorization Code模式,其余模式可去OAuth2.0协议rfc6749文件 (opens new window)学习。
代码分析:
第三方应用 A 由于在 Gitee 已经完成备案,获得了 clientId 和 clientSecret。
第三方应用发起授权:
https://gitee.com/oauth/authorize?response_type=code&redirect_uri=http://127.0.0.1:8091/oauth/code&state=bcf3c0a682877e15a0dd4d590aea8781&client_id=ec7b05fdebf7c29fbd3320b1309b34011298baf7f5ac99ca8945c7aaacfbbfa&scope=user_info
该链接这里有注意点:
https://gitee.com/oauth/authorize:是指用于对第三方应用进行授权的Authorization Endpoint
response_type=code:指进行code模式的OAuth2.0授权
redirect_uri:用于授权成功或者失败后的跳转地址
client_id:指备份应用的ID
scope:请求的授权范围
state:表示安全标志位,用来保证不是伪造的请求,始终在请求授权和响应授权的URL中作为参数;
这里简单介绍什么是伪造?即什么是CSRF?
CSRF(XSRF)又称跨站请求伪造,属于一种授权攻击行为,导致最终用户的受保护资源被攻击者使用。通常是针对客户端的重定向redirect_uri发起攻击,注入攻击者自己的授权码或访问令牌。 简单点说就是攻击者使用你的信息去访问并使用受保护资源,导致受害者出现重大损失。因此为了实现CSRF保护,客户端应该使用“state”请求参数在发起授权请求时向授权服务器传送该值,并授权成功后返回同一个status值,以保证是真正的双方在进行通信。
http://127.0.0.1:8091/oauth/code?code=c54b4dc9b5559a37ef9a24a0e76f92aa31beee666ba64a3a4f1bebc20f1cab21&state=bcf3c0a682877e15a0dd4d590aea8781
Resource Owner对请求验证成功,运行发布授权码Code,此时,你会发现携带的一个status参数与请求时的status参数值一致,以此实现CSRF保护。
private static AccessToken getAccessTokenOfAuthorizationCodeMode(HttpServletRequest request, OAuthConfig oAuthConfig) throws JapOauth2Exception {
String state = request.getParameter("state");
Oauth2Util.checkState(state, oAuthConfig.getClientId(), oAuthConfig.isVerifyState());
String code = request.getParameter("code");
Map<String, String> params = new HashMap<>(6);
params.put("grant_type", Oauth2GrantType.authorization_code.name());
params.put("code", code);
params.put("client_id", oAuthConfig.getClientId());
params.put("client_secret", oAuthConfig.getClientSecret());
if (StrUtil.isNotBlank(oAuthConfig.getCallbackUrl())) {
params.put("redirect_uri", oAuthConfig.getCallbackUrl());
}
if (Oauth2ResponseType.code == oAuthConfig.getResponseType() && oAuthConfig.isEnablePkce()) {
params.put(PkceParams.CODE_VERIFIER, PkceHelper.getCacheCodeVerifier(oAuthConfig.getClientId()));
}
Kv tokenInfo = Oauth2Util.request(oAuthConfig.getAccessTokenEndpointMethodType(), oAuthConfig.getTokenUrl(), params);
Oauth2Util.checkOauthResponse(tokenInfo, "Oauth2Strategy failed to get AccessToken.");
if (!tokenInfo.containsKey("access_token")) {
throw new JapOauth2Exception("Oauth2Strategy failed to get AccessToken." + tokenInfo.toString());
}
return mapToAccessToken(tokenInfo);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Client通过Token去Resource Server请求资源,该步骤已经由JAP(上述代码)完全实现,通过拼接参数,在后端发起请求Token实现,同时实现了PKCE授权码模式。
这里简单介绍什么是PKCE?
发生时间:官方示例图中的C步。
全称:Proof Key for Code Exchange。PKCE是对Authorization Code模式更进一步的安全措施,属于一种密码学手段,保证即使在C步code等被第三方截取,也无法获取用户保护资源,达到CSRF保护。
- 客户端:而请求参数 code_challenge 的得到方式是:客户端在请求 code 之前,准备随机生成一段字符串,保存于 code_verifier 变量,然后再将该字符串通过 SHA256 哈希和 URL-Safe 的 base 编码得到打值,存进code_challenge。才开始向授权服务器端发起请求code。
- 授权服务器端:通过请求去保存 code_challenge 和编码处理方法。在客户端拿到 code 后,再使用 code_verifier 等作为参数请求授权服务器端。授权服务器端最终使用 code_verifier 按照 code_challenge_method方式进行编码处理,将处理得到的值与请求code时传递的code_challenge进行比较。
- 若不同:说明在请求 code 时,遭受 CSRF 攻击,但被授权服务器端发现,用户受保护资源未被泄露。
- 若相同:说明此次换取 token 令牌的通信,未被攻击,客户端成功拿到令牌。
下面给出图例:
private JapUser getUserInfo(OAuthConfig oAuthConfig, AccessToken accessToken) throws JapOauth2Exception {
Map<String, String> params = new HashMap<>(6);
params.put("access_token", accessToken.getAccessToken());
Kv userInfo = Oauth2Util.request(oAuthConfig.getUserInfoEndpointMethodType(), oAuthConfig.getUserinfoUrl(), params);
Oauth2Util.checkOauthResponse(userInfo, "Oauth2Strategy failed to get userInfo with accessToken.");
JapUser japUser = this.japUserService.createAndGetOauth2User(oAuthConfig.getPlatform(), userInfo, accessToken);
if (ObjectUtil.isNull(japUser)) {
return null;
}
return japUser;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
拿取用户信息:
开发者自己需要做的操作: 配置相关接口(Authorization EndPoint和Token EndPoint等)、授权类型等
这里以使用JFinal框架为例:
引入依赖:
<dependency>
<groupId>com.fujieid</groupId>
<artifactId>jap-oauth2</artifactId>
<version>1.0.2</version>
</dependency>
2
3
4
5
//OAuth2.0需要的各个配置(相应的接口、授权类型等)
private static OAuthConfig config = new OAuthConfig();
//OAuth2.0 授权策略类,用于接收重定向redirect_uri和接收code换取token,拿到用户信息等,属于JAP验证授权的最外部接口。
private Oauth2Strategy oauth2Strategy = new Oauth2Strategy(japUserService, new JapConfig());
@ActionKey("/oauth/code/getData")
public void getBaseData(){
//获取参数,这里是为了方便测试者以页面通用的方式进行输入,而不是修改源代码。但参数clientSecret等一般很重要,实际开发下不会这样使用
String clientId = this.getRequest().getParameter("clientId");
String clientSecrect = this.getRequest().getParameter("clientSecret");
String redirectURI = this.getRequest().getParameter("redirectURI");
config.setPlatform("gitee")
.setState(UuidUtils.getUUID())
.setClientId(clientId)
.setClientSecret(clientSecrect)
.setCallbackUrl(redirectURI)
.setAuthorizationUrl("https://gitee.com/oauth/authorize")
.setTokenUrl("https://gitee.com/oauth/token")
.setUserinfoUrl("https://gitee.com/api/v5/user")
.setScopes(new String[]{"user_info"})
.setResponseType(Oauth2ResponseType.code)
.setGrantType(Oauth2GrantType.authorization_code)
.setUserInfoEndpointMethodType(Oauth2EndpointMethodType.GET);
this.renderAuth();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
redirect_uri
也将是 /oauth/code
请求接口,验证成功后,会由后端该函数接收响应,由oauth2Strategy类的authenticate函数进入,并开始由JAP在后端进行向授权服务器Authorization Server换取token,这段数据将会是JSON数据,JAP也进行了响应的映射处理,向资源服务器Resource Server 用Token换取用户信息等受保护资源。
@ActionKey("/oauth/code")
public void renderAuth() {
String code = this.getRequest().getParameter("code");
JapResponse japResponse = oauth2Strategy.authenticate(config, this.getRequest(), this.getResponse());
if (!japResponse.isSuccess()) {
renderJson("/?error=" + URLUtil.encode(japResponse.getMessage()));
}
if (japResponse.isRedirectUrl()) {
renderJson(RetKit.ok("toAuth",(String)japResponse.getData()));
} else {
JapUser japUser = (JapUser) japResponse.getData();
renderJson(RetKit.ok("userInfos",japUser));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# OIDC协议
官方定义:
OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 [RFC6749] (opens new window) protocol. It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.
简单点说,就在在OAuth2.0的基础之上加了一层身份层。全称:OpenID Connect,用于客户端验证用户的身份并获取用户的基本信息。详细信息可见:OIDC官方文档 (opens new window)。
此处只简单描述增加的身份层是什么?
- 前面几个步骤和 OAuth2.0 大致相同,但在认证请求时的参数需要 scope 必须包含 OpenID 字段。
- 当 Client 向授权服务器使用code向其 Token EndPoint 请求 Token 时,Token EndPoint 接口返回 ID Token 和 Access Token。
- Client 校验 ID Token,并从中提取用户的身份标识符(End-User分配的唯一标识符)后,向资源服务器的UserInfo EndPoint接口请求用户信息资源,返回End-User的Claims。
官方例图:
Jap实现:开发者在完成 OIDC 的基本配置后:OidcConfig 对象的配置,后续的授权重定向以及接收用户信息均可以由 oidcStrategy.authenticate
函数实现,其内部以及完成重定向前的各个基本配置检验、用户缓存 cache 值,完成对 issuer
的拼接 /.well-known/openid-configuration
等各个操作,后续直接走向 OAuth2.0 的相关过程,简化开发者设计过程,做到开箱即用。
开发者自己需要做的操作:配置相关接口(Scopes和Issuer等)、授权类型等
这里以使用JFinal框架为例:
引入依赖:
<dependency>
<groupId>com.fujieid</groupId>
<artifactId>jap-oidc</artifactId>
<version>1.0.1</version>
</dependency>
2
3
4
5
private OidcStrategy oidcStrategy = new OidcStrategy(japUserService, new JapConfig());
private static OidcConfig config = new OidcConfig();
@ActionKey("/oidc/getData")
public void getBaseData(){
// 获取参数,这里是为了方便测试者以页面通用的方式进行输入,而不是修改源代码。但参数clientSecret等一般很重要,实际开发下不会这样使用
String clientId = this.getRequest().getParameter("clientId");
String clientSecrect = this.getRequest().getParameter("clientSecret");
String redirectURI = this.getRequest().getParameter("redirectURI");
// 配置 OIDC 的 Issue 链接
config.setIssuer("https://oauth.aliyun.com")
.setPlatform("aliyun")
.setState(UuidUtils.getUUID())
.setClientId(clientId)
.setClientSecret(clientSecrect)
.setCallbackUrl(redirectURI)
.setScopes(new String[]{"aliuid","openid","profile"})
.setResponseType(Oauth2ResponseType.code)
.setGrantType(Oauth2GrantType.authorization_code);
logger.info("拿到参数,开始准备重定向授权页面");
this.renderAuth();
}
@ActionKey("/oidc/auth")
public void renderAuth() {
JapResponse japResponse = oidcStrategy.authenticate(config, this.getRequest(), this.getResponse());
if (!japResponse.isSuccess()) {
System.out.println(japResponse.getMessage());
renderText("/?error=" + URLUtil.encode(japResponse.getMessage()));
}
if (japResponse.isRedirectUrl()) {
renderJson(RetKit.ok("toAuth",(String)japResponse.getData()));
logger.info("授权成功");
} else {
JapUser japUser = (JapUser) japResponse.getData();
Map<String,StringuserInfos = new HashMap<>();
userInfos.put("token",japUser.getToken());
userInfos.put("username",japUser.getUsername());
userInfos.put("userId",japUser.getUserId());
userInfos.put("password",japUser.getPassword());
renderJson(RetKit.ok("userInfos",userInfos));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
另外,在前端方面,在这里也有几点注意的地方:
第一点:前端项目,如何实现与多个后端项目相互通信?信息类型又是什么?如何自定义实现一个专门用于传递信息的类?
第二点:当框架支持的请求响应与JAP使用的请求响应类型不一致时,应该如何做?怎么实现结构型模式中的适配器模式?
希望读者能够有所获。
# 放在最后
总结,很感谢 JustAuth 社区张亚东导师的信任,能够让学生有机会参与此次的开源计划,在这几个月的活动中,毋庸置疑的是,学到了很多很多很多知识和从导师那里了解到了很多很多开发经验。活动虽终,开发道路仍旧前行。
其次,学习的内容有很多,从适配器模式到23种经典设计模式,从 OAuth2.0 协议到各种登录协议、安全协议及措施,从 SpringBoot 框架到 JFinal、Blade 和 ActFramework 框架,从 HTML 到 VUE 实现,从 Ajax 到 axios 跨域处理等等知识点和难解点,都一一解决和学习。从开发前构想到最终实现成果完全一致,“不辱使命” 的完成了此次的项目任务,丰富了自己开发经历与经验。
# 参考文献
OAuth2标准RFC6749文件 (opens new window)
- 01
- jap-spring-boot-starter 使用帮助10-28
- 02
- 使用jap-ldap10-25