暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

项目经验分享:开发 JFinal、Blade、ActFramework 集成 JustAuthPlus 的 Demo

开源之夏 2021-11-08
1631



开源之夏公众号持续面向社区和学生征稿,欢迎大家分享项目经验开源心得!


投稿方式:

E-mail :summer@iscas.ac.cn

or 关注公众号,后台回复“投稿”。


本期分享JustAuth社区吴豪琪同学的项目经验:开发 JFinal、Blade、ActFramework 集成 JustAuthPlus 的 Demo







前言


各位参与开源的导师、同学们,大家好,我是来自四川成都的一名准大三学生,怀着忐忑的心情在大二暑假前向 JustAuthhttps://justauth.wiki/)社区提交了简历,很荣幸成功参与进人生中的第一次开源软件计划。下面由我简单分享一下此次的开源项目经验。


社区简介

JustAuth开源社区, 致力于为开源的授权/身份认证技术开发、布道。以开源之名,赋能开发者。名下开源项目 JustAuth 深受开发者喜欢,在 Giteehttps://gitee.com/yadong.zhang/JustAuth)和 Githubhttps://github.com/justauth/JustAuth)中的累计关注量近 19K,并荣获 Gitee GVP 称号,目前已有多个企业(中文在线、北京市公园管理中心、前海人寿等)、组织和开源项目(Shiro Action、sika、七腾智能、BladeX、GUNS、MaxKey、mica)正在使用 JustAuth 完成极速的第三方登录集成。

JustAuth 致力于让开发者们脱离繁琐的第三方登录SDK,简化登录开发过程,做到开箱即用
同时基于 JustAuth,结合目前流行的登录技术(OAuth 2.0,SAML、OIDC、LDAP、CAS等)又开源出了 JustAuthPlus。

JustAuthPlushttps://github.com/fujieid/jap)(以下简称"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 功能模块说明:
详情:
 https://justauth.plus/paper/jap-paper-latest.pdf)

项目介绍

项目名称
完成 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
tips:根据该项目的产出要求,JAP的实现过程,已在后端代码进行了大致解释。

此次开发我个人采用的是在合理的时间规划后,以前后端分离方式开发实现 第三方登录,OAuth2登录,OIDC登录,账号密码登录这几种登录方式。此次以后端的OAuth2和OIDC登录为主要分享经验。

以面向小白为主,我以浅显易懂的方式编写这篇文章,希望能够有所学有所思有所用。
1. 什么是OAuth2.0?
2. 什么是OIDC?
3. 什么是令牌Token?
4. 什么是CSRF?
5. 什么是PKCE?有何作用?
6. 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的有效性和安全性后,给取货者运输货物。

    tips:这里请注意凭证、令牌的使用对象和作用对象。

    另外,我以图例作为补充,帮助你了解这4段文字的具体含义:

官方示例图:

这里简单介绍什么是令牌Token?

简单来说就是用户第一次登录,服务器生成的一个字符串,以此作为客户端在进行请求的一个标志。而在以后的的请求数据,客户端只需要带上这个Token即可,不再需要每次带上用户名和密码。
Token的使用方式?
可以是你设备的MAC地址(设备唯一标识符,用于数据链路层的MAC子层)也可以是session值。
这里不展开细讲。你只需要知道,这是一个用于安全的字符串,以保证客户端和服务端的通信安全。

更细化地学习,你会知道,一个authorization grant总共有四种模式可以选择,以用于第三方获取令牌,换取受保护资源。四种模式如下:
  • Authorization Code
  • Implicit
  • Resource Owner Password Credentials
  • Client Credentials

此处只介绍Authorization Code模式,其余模式可去OAuth2.0协议rfc6749文件学习https://datatracker.ietf.org/doc/html/rfc6749)


代码分析
第三方应用A由于在Gitee已经完成备案,获得了clientId和clientSecret。
第三方应用发起授权:


第一步:Client确认登录,跳转Resource Owner,请求授权码,用户确认授权。

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值,以保证是真正的双方在进行通信。

第二步:用户确认授权,Resource Owner响应给Client授权码code。
http://127.0.0.1:8091/oauth/code?code=c54b4dc9b5559a37ef9a24a0e76f92aa31beee666ba64a3a4f1bebc20f1cab21&state=bcf3c0a682877e15a0dd4d590aea8781
Resource Owner对请求验证成功,运行发布授权码Code,此时,你会发现携带的一个status参数与请求时的status参数值一致,以此实现CSRF保护。

第三步:Client使用授权码Code去请求Authorization Server,code有效,响应给Client一个Token。
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);
}


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进行比较。

    tips:就算攻击者拿到code_challenge (不能逆推得到code_verifier)、code等值,也不能逆推拿到code_verifier。

    • 若不同:说明在请求code时,遭受CSRF攻击,但被授权服务器端发现,用户受保护资源未被泄露。
    • 若相同:说明此次换取token令牌的通信,未被攻击,客户端成功拿到令牌。

下面给出图例:


第四步:Client使用令牌Token去请求Resource Server,Token有效,响应Client需要请求的资源,如基本的用户信息,给与第三方应用。
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;
}

拿取用户信息:


开发者自己需要做的操作:   
配置相关接口(Authorization EndPoint和Token EndPoint等)、授权类型等
这里以使用JFinal框架为例:


JFinal官网https://jfinal.com/doc)
引入依赖:
<dependency>
<groupId>com.fujieid</groupId>
<artifactId>jap-oauth2</artifactId>
<version>1.0.2</version>
</dependency>
//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();
}


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));
 }
}



OIDC协议



官方定义:
OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 [RFC6749] 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官方文档https://openid.net/specs/openid-connect-core-1_0.html)


此处只简单描述增加的身份层是什么?
  • 前面几个步骤和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值,完成对isser的拼接"/.well-known/openid-configuration"等各个操作,后续直接走向OAuth2.0的相关过程,简化开发者设计过程,做到开箱即用。

开发者自己需要做的操作:配置相关接口(Scopes和Issuer等)、授权类型等

这里以使用JFinal框架为例:
引入依赖:
<dependency>
<groupId>com.fujieid</groupId>
<artifactId>jap-oidc</artifactId>
<version>1.0.1</version>
</dependency>


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,String> userInfos = 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));
}
}

另外,在前端方面,在这里也有几点注意的地方:
第一点:前端项目,如何实现与多个后端项目相互通信?信息类型又是什么?如何自定义实现一个专门用于传递信息的类?
第二点:当框架支持的请求响应与JAP使用的请求响应类型不一致时,应该如何做?怎么实现结构型模式中的适配器模式?
希望读者能够有所获。

放在最后

总结,很感谢 JustAuth 社区张亚东导师的信任,能够让学生有机会参与此次的开源计划,在这几个月的活动中,毋庸置疑的是,学到了很多很多很多知识和从导师那里了解到了很多很多开发经验。活动虽终,开发道路仍旧前行。
其次,学习的内容有很多,从适配器模式到23种经典设计模式,从 OAuth2.0 协议到各种登录协议、安全协议及措施,从 SpringBoot 框架到 JFinal、Blade 和 ActFramework 框架,从 HTML 到 VUE 实现,从 Ajax 到 axios 跨域处理等等知识点和难解点,都一一解决和学习。从开发前构想到最终实现成果完全一致,“不辱使命” 的完成了此次的项目任务,丰富了自己开发经历与经验。

参考文献:

OAuth2标准RFC6749文件
https://datatracker.ietf.org/doc/html/rfc6749
PKCE加密RFC7636文件
https://datatracker.ietf.org/doc/html/rfc7636
OIDC文档
https://openid.net/specs/openid-connect-core-1_0.html



本文为吴豪琪同学原创文章,欢迎投稿


END







文章转载自开源之夏,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论