JustAuth,如你所见,它仅仅是一个第三方授权登录的工具类库,它可以让我们脱离繁琐的第三方登录SDK,让登录变得So easy!
本专栏将会由浅入深,详细介绍如何使用JustAuth实现第三方登录,以及如何使用JustAuth的高级特性。

使用SpringBoot初始化项目

在教程正式开始前,我们要先准备好相应的软件环境。JustAuth

在IDEA下使用以下方式创建项目:依次点击File-New-Project然后选择Spring Initializr,根据提示进行操作,配置依赖时勾选spring-boot-starter-weblombok两个依赖,为了方便开发测试,我这儿多选了一个spring-boot-devtools

按照提示创建完成后,得到POM如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.6.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>me.zhyd.justauth</groupId><artifactId>justauth-tutorial</artifactId><version>0.0.1-SNAPSHOT</version><name>justauth-tutorial</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

等待项目编译完成后,我们开始正式集成JustAuth。

添加JustAuth依赖

正式开始前,建议朋友们先看一下JustAuth的用户文档:https://docs.justauth.whnb.wang, 重点关注快速开始这个章节。

这个章节中包含了关于OAuth和JustAuth相关的重要信息,再次建议优先查看该章节内容。

重申一遍,使用JustAuth总共分三步(这三步也适合于JustAuth支持的任何一个平台):

  1. 申请注册第三方平台的开发者账号
  2. 创建第三方平台的应用,获取配置信息(accessKey, secretKey, redirectUri)
  3. 使用该工具实现授权登陆

当然, 第三步和前两步谁先谁后都无所谓,您可以先实现代码再申请第三方应用,也可以先申请第三方应用,再集成代码。

接下来我们按照文档上的第一步指示,先添加pom依赖。

// ...<dependency><groupId>me.zhyd.oauth</groupId><artifactId>JustAuth</artifactId><version>1.15.1</version></dependency>// ...

依赖添加完成后,等待项目编译完成,接下来我们就开始正式接入JustAuth。

集成JustAuth的API

注意,以下代码中,我们的请求链接中是通过动态参数{source}去取的,这样可以方便的让我们集成任意平台,比如集成gitee时, 我们的请求地址就是:http://localhost:8080/oauth/render/gitee, 而回调地址就是http://localhost:8080/oauth/callback/gitee。

当然,例子中只是举例告诉大家可以这么用,但如果各位只需要集成单一平台的话, 可以直接将{souce}改为平台名,如gitee

package me.zhyd.justauth;import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.request.AuthGiteeRequest;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 实战演示如何使用JustAuth实现第三方登录** @author yadong.zhang (yadong.zhang0415(a)gmail.com)* @version 1.0.0* @since 1.0.0*/
@RestController
@RequestMapping("/oauth")
public class JustAuthController {/*** 获取授权链接并跳转到第三方授权页面** @param response response* @throws IOException response可能存在的异常*/@RequestMapping("/render/{source}")public void renderAuth(HttpServletResponse response) throws IOException {AuthRequest authRequest = getAuthRequest();String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());response.sendRedirect(authorizeUrl);}/*** 用户在确认第三方平台授权(登录)后, 第三方平台会重定向到该地址,并携带code、state等参数** @param callback 第三方回调时的入参* @return 第三方平台的用户信息*/@RequestMapping("/callback/{source}")public Object login(AuthCallback callback) {AuthRequest authRequest = getAuthRequest();return authRequest.login(callback);}/*** 获取授权Request** @return AuthRequest*/private AuthRequest getAuthRequest() {return new AuthGiteeRequest(AuthConfig.builder().clientId("clientId").clientSecret("clientSecret").redirectUri("redirectUri").build());}}

接下来我们就需要去gitee上创建我们的OAuth应用。登录gitee后,我们点击右上角用户头像,选择设置,然后点击第三方应用进入第三方应用管理页面,点击右上角的创建应用按钮,进入应用创建页面

我们按照提示填入我们的应用信息即可。

应用名称: 一般填写自己的网站名称即可

应用描述: 一般填写自己的应用描述即可

应用主页: 填写自己的网站首页地址

应用回调地址: 重点,该地址为用户授权后需要跳转到的自己网站的地址,默认携带一个code参数

权限: 根据页面提示操作,默认勾选第一个就行,因为我们只需要获取用户信息即可

以上信息输入完成后,点击确定按钮创建应用。创建完成后,点击进入应用详情页,可以看到应用的密钥等信息

复制以下三个信息:Client IDClient Secret应用回调地址

自定义HTTP工具

将上一步获取到配置信息配置AuthConfig中,如下:

// ...*/private AuthRequest getAuthRequest() {return new AuthGiteeRequest(AuthConfig.builder().clientId("4c504cd2e1b1dbaba8dc1187d8070adf679acab17b2bc9cf6dfa76b9ae06aadc").clientSecret("fa5857175723475e4675e36af9eafde338545c1a0dfa49d1e0cc78f9c3ce5ebe").redirectUri("http://localhost:8080/oauth/callback/gitee").build());}}

以上工作完成后,我们直接启动项目,然后在浏览器中访问http://localhost:8080/oauth/render/gitee,当出现以下页面时,表示我们已经集成成功,并且已跳转到了第三方的授权页面。

注意,如果您当前没有在浏览器中登录过您的账号,您将会看到如下页面:

我们点击“同意授权”后,第三方应用(Gitee)将会生成一个code授权码,连带着我们先前传过去的state一并回调到我们配置的redirectUri接口中。

如上图,进入回调接口后,我们可以断点跟踪到第三方平台传回的信息:state和code。

如果您是新项目,这儿可能会出现一个小小的问题:

2020-04-21 23:50:02 http-nio-8080-exec-4 me.zhyd.oauth.log.Log(error:45) [ERROR] - Failed to login with oauth authorization.
com.xkcoding.http.exception.SimpleHttpException: HTTP 实现类未指定!at com.xkcoding.http.HttpUtil.checkHttpNotNull(HttpUtil.java:70)at com.xkcoding.http.HttpUtil.post(HttpUtil.java:119)at me.zhyd.oauth.request.AuthDefaultRequest.doPostAuthorizationCode(AuthDefaultRequest.java:213)at me.zhyd.oauth.request.AuthGiteeRequest.getAccessToken(AuthGiteeRequest.java:31)at me.zhyd.oauth.request.AuthDefaultRequest.login(AuthDefaultRequest.java:79)at me.zhyd.justauth.JustAuthController.login(JustAuthController.java:47)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879)at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793)at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1594)at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)at java.lang.Thread.run(Thread.java:748)

这是因为JustAuth从v1.14.0开始默认集成了的simple-http作为HTTP通用接口(更新说明见JustAuth 1.14.0版本正式发布!完美解耦HTTP工具开始JustAuth将不会默认集成hutool-http,如果开发者的项目是全新的或者项目内没有集成HTTP实现工具,还需要添加对应的HTTP实现类,JustAuth提供了三种备选的pom依赖:

hutool-http

<dependency><groupId>cn.hutool</groupId><artifactId>hutool-http</artifactId><version>5.2.5</version>
</dependency>

httpclient

<dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.12</version>
</dependency>

okhttp

<dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.4.1</version>
</dependency>

添加完HTTP工具依赖后,重启项目,重新访问http://localhost:8080/oauth/render/gitee链接,然后进行授权。

授权完成后,您将会看到如下页面:

自定义缓存

在OAuth授权流程中,有一个存在感极弱但又非常重要的参数state,在相关文档中是这么对State参数解释的:

RECOMMENDED. An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back
to the client. The parameter SHOULD be used for preventing
cross-site request forgery as described in Section 10.12.

——以上内容节选自《The OAuth 2.0 Authorization Framework》4.1.1

简单翻译就是说:state是用于维护请求和回调之间状态的不透明值。这儿的“不透明”理解为“不可预测”更好些。在没有使用state时,已集成OAuth登录的网站极易受到CSRF攻击,关于实现CSRF攻击的细节和流程已经危害,这儿不作赘述,感兴趣的朋友可以参考:Cross-Site Request Forgery和移花接木:针对OAuth2的CSRF攻击 两篇文章。

由此可见,state贯穿整个OAuth授权流程,能够确保流程之间的连续性和安全性。但因其是个非必选的参数,所以大多时候开发者都会忘记使用该参数,并且多个第三方平台的OAuth API中也是非必选state。

在OAuth流程中,code参数都是有时效性的,一般为10分钟有效期,如果超过10分钟未使用,则需要重新申请code信息。而对于state来说,OAuth官网文档中并未给出时效性的说明,也就是说只能客户端本身去对state做时效性和有效性作校验。

JustAuth考虑到这一点,在所有OAuth流程中一方面都会传递state参数用于确保流程的完整性,如果客户端没有手动指定,则会使用默认的唯一值;另一方面也会对state做缓存处理,确保state也有其时效性,防止state被重复利用。

JustAuth中默认使用了本地map的方式,实现对state的缓存,详情可参考 AuthDefaultCache 类

package me.zhyd.oauth.cache;import lombok.Getter;
import lombok.Setter;import java.io.Serializable;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;/*** 默认的缓存实现** @author yadong.zhang (yadong.zhang0415(a)gmail.com)* @since 1.9.3*/
public class AuthDefaultCache implements AuthCache {/*** state cache*/private static Map<String, CacheState> stateCache = new ConcurrentHashMap<>();private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock(true);private final Lock writeLock = cacheLock.writeLock();private final Lock readLock = cacheLock.readLock();public AuthDefaultCache() {if (AuthCacheConfig.schedulePrune) {this.schedulePrune(AuthCacheConfig.timeout);}}/*** 设置缓存** @param key   缓存KEY* @param value 缓存内容*/@Overridepublic void set(String key, String value) {set(key, value, AuthCacheConfig.timeout);}/*** 设置缓存** @param key     缓存KEY* @param value   缓存内容* @param timeout 指定缓存过期时间(毫秒)*/@Overridepublic void set(String key, String value, long timeout) {writeLock.lock();try {stateCache.put(key, new CacheState(value, timeout));} finally {writeLock.unlock();}}/*** 获取缓存** @param key 缓存KEY* @return 缓存内容*/@Overridepublic String get(String key) {readLock.lock();try {CacheState cacheState = stateCache.get(key);if (null == cacheState || cacheState.isExpired()) {return null;}return cacheState.getState();} finally {readLock.unlock();}}/*** 是否存在key,如果对应key的value值已过期,也返回false** @param key 缓存KEY* @return true:存在key,并且value没过期;false:key不存在或者已过期*/@Overridepublic boolean containsKey(String key) {readLock.lock();try {CacheState cacheState = stateCache.get(key);return null != cacheState && !cacheState.isExpired();} finally {readLock.unlock();}}/*** 清理过期的缓存*/@Overridepublic void pruneCache() {Iterator<CacheState> values = stateCache.values().iterator();CacheState cacheState;while (values.hasNext()) {cacheState = values.next();if (cacheState.isExpired()) {values.remove();}}}/*** 定时清理** @param delay 间隔时长,单位毫秒*/public void schedulePrune(long delay) {AuthCacheScheduler.INSTANCE.schedule(this::pruneCache, delay);}@Getter@Setterprivate class CacheState implements Serializable {private String state;private long expire;CacheState(String state, long expire) {this.state = state;// 实际过期时间等于当前时间加上有效期this.expire = System.currentTimeMillis() + expire;}boolean isExpired() {return System.currentTimeMillis() > this.expire;}}
}

提示

关于JustAuth校验state的流程,可以参考:https://segmentfault.com/a/1190000020712258。

另外,由于开发者可能本身并不想使用map的形式作为缓存工具,或者说开发者现有项目中已经用到了其他缓存组件,比如Redis,想直接使用项目里已有的缓存组件实现state缓存,因此JustAuth也支持开发者自定义缓存实现。

本文以Redis为例,实现自定义的缓存。

首先在项目中添加Redis依赖

// ...<!-- 自定义缓存实现 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>// ...

然后在配置文件中添加redis的基本配置

 // ...
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=

接下来实现JustAuth对外提供的缓存接口AuthStateCache,我们使用RedisTemplate实现对state的缓存。

package me.zhyd.justauth.cache;import me.zhyd.oauth.cache.AuthCacheConfig;
import me.zhyd.oauth.cache.AuthStateCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;/*** 扩展Redis版的state缓存** @author yadong.zhang (yadong.zhang0415(a)gmail.com)* @version 1.0.0* @date 2020/4/25 14:21* @since 1.0.0*/
@Component
public class AuthStateRedisCache implements AuthStateCache {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private ValueOperations<String, String> valueOperations;@PostConstructpublic void init() {valueOperations = redisTemplate.opsForValue();}/*** 存入缓存,默认3分钟** @param key   缓存key* @param value 缓存内容*/@Overridepublic void cache(String key, String value) {valueOperations.set(key, value, AuthCacheConfig.timeout, TimeUnit.MILLISECONDS);}/*** 存入缓存** @param key     缓存key* @param value   缓存内容* @param timeout 指定缓存过期时间(毫秒)*/@Overridepublic void cache(String key, String value, long timeout) {valueOperations.set(key, value, timeout, TimeUnit.MILLISECONDS);}/*** 获取缓存内容** @param key 缓存key* @return 缓存内容*/@Overridepublic String get(String key) {return valueOperations.get(key);}/*** 是否存在key,如果对应key的value值已过期,也返回false** @param key 缓存key* @return true:存在key,并且value没过期;false:key不存在或者已过期*/@Overridepublic boolean containsKey(String key) {return redisTemplate.hasKey(key);}
}

注意

AuthCacheConfig为JustAuth默认的缓存配置类,AuthCacheConfig.timeout为内置的缓存过期时间,默认3分钟有效期

JustAuth对外提供的Request类,默认都会支持两种构造参数,一种是只需传入AuthConfig,JustAuth默认使用内置缓存;另外一种则是支持传入AuthConfig和AuthStateCache

接下来我们需要将上一步创建的stateCache实现类注入到JustAuth的Request中。

// .../*** 注入自定义的缓存实现类*/@Autowiredprivate AuthStateRedisCache stateRedisCache;// ...private AuthRequest getAuthRequest() {return new AuthGiteeRequest(AuthConfig.builder().clientId("4c504cd2e1b1dbaba8dc1187d8070adf679acab17b2bc9cf6dfa76b9ae06aadc").clientSecret("fa5857175723475e4675e36af9eafde338545c1a0dfa49d1e0cc78f9c3ce5ebe").redirectUri("http://localhost:8080/oauth/callback/gitee")// ....build(), stateRedisCache);}// ...

我们通过断点看一下是否已启用我们自定义的缓存实现。

可以看到,当前使用的缓存实现就是我们自定义的缓存。

注意

AuthCacheConfig.timeout为内置的缓存过期时间,默认3分钟有效期。主要基于正常流程考虑,一个授权流程的时间一般不会太长,因此综合考虑下,为了保证OAuth流程能够正常走完,且保证state的有效性,系统默认3分钟有效期。可以通过重新赋值 AuthCacheConfig.timeout实现缓存时间自定义或者在实现public void cache(String key, String value)方法时自定义缓存过期时间。

集成自有的Gitlab私服登录

JustAuth发展到现在,基本上已经涵盖了国内外大多数知名的网站。JustAuth也一直以它的,备受各位开发者的喜爱和支持。

但现在OAuth技术越来越成熟,越来越多的个人站长或者企业都开始搭建自己的OAuth授权平台,那么针对这种情况,JustAuth并不能做到面面俱到,也无法集成所有支持OAuth的网站(这也是不现实的)。

既然考虑到有这种需求,那么就要想办法解决、想办法填补漏洞,不为了自己,也为了陪伴JustAuth一路走来的所有朋友们。

JustAuth开发团队也在v1.12.0版本中新加入了一大特性,就是可以支持任意支持OAuth的网站通过JustAuth实现便捷的OAuth登录!

本节内容,将会演示如何集成自有的Gitlab私服实现第三方登录。


准备Gitlab私服

首先我们要有一个可用的Gitlab服私服,如果没有请自行解决。

创建OAuth应用

创建完成后将会得到如下内容:

复制Application Id、Secret和Callback url备用

实现AuthSource接口

AuthSource.java是提供OAuth平台的API地址的统一接口,提供以下接口:

AuthSource#authorize(): 获取授权url. 必须实现

  • AuthSource#accessToken(): 获取accessToken的url. 必须实现

  • AuthSource#userInfo(): 获取用户信息的url. 必须实现

  • AuthSource#revoke(): 获取取消授权的url. 非必须实现接口(部分平台不支持)

  • AuthSource#refresh(): 获取刷新授权的url. 非必须实现接口(部分平台不支持)

注:

注意

  1. 当通过JustAuth扩展实现第三方授权时,请参考AuthDefaultSource自行创建对应的枚举类并实现AuthSource接口
  2. 如果不是使用的枚举类,那么在授权成功后获取用户信息时,需要单独处理source字段的赋值
  3. 如果扩展了对应枚举类时,在me.zhyd.oauth.request.AuthRequest#login(AuthCallback)中可以通过xx.toString()获取对应的source
package me.zhyd.justauth.ext;import me.zhyd.oauth.config.AuthSource;/*** 自定义的AuthSource,用来集成自有的OAUTH系统** @author yadong.zhang (yadong.zhang0415(a)gmail.com)* @version 1.0.0* @date 2020/4/25 17:37* @since 1.0.0*/
public enum AuthCustomSource implements AuthSource {/*** 自己搭建的gitlab私服*/MYGITLAB {/*** 授权的api** @return url*/@Overridepublic String authorize() {return "http://gitlab.demo.dev/oauth/authorize";}/*** 获取accessToken的api** @return url*/@Overridepublic String accessToken() {return "http://gitlab.demo.dev/oauth/token";}/*** 获取用户信息的api** @return url*/@Overridepublic String userInfo() {return "http://gitlab.demo.dev/api/v4/user";}}
}

注意,文中的gitlab服务url已被我脱敏,请使用者换成自己的gitlab服务域名,比如:你的私服域名是https://gitlab.zhyd.me,那就将上文中的http://gitlab.demo.dev全部替换成https://gitlab.zhyd.me即可。

创建自定义的Request

这儿直接参考AuthGitlabRequest即可,完整代码如下:

package me.zhyd.justauth.ext;import com.alibaba.fastjson.JSONObject;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.enums.AuthUserGender;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import me.zhyd.oauth.utils.UrlBuilder;/*** 自定义的OAuth平台的Request** @author yadong.zhang (yadong.zhang0415(a)gmail.com)* @version 1.0.0* @date 2020/4/25 17:39* @since 1.0.0*/
public class AuthMyGitlabRequest extends AuthDefaultRequest {public AuthMyGitlabRequest(AuthConfig config) {super(config, AuthCustomSource.MYGITLAB);}public AuthMyGitlabRequest(AuthConfig config, AuthStateCache authStateCache) {super(config, AuthCustomSource.MYGITLAB, authStateCache);}@Overrideprotected AuthToken getAccessToken(AuthCallback authCallback) {String responseBody = doPostAuthorizationCode(authCallback.getCode());JSONObject object = JSONObject.parseObject(responseBody);this.checkResponse(object);return AuthToken.builder().accessToken(object.getString("access_token")).refreshToken(object.getString("refresh_token")).idToken(object.getString("id_token")).tokenType(object.getString("token_type")).scope(object.getString("scope")).build();}@Overrideprotected AuthUser getUserInfo(AuthToken authToken) {String responseBody = doGetUserInfo(authToken);JSONObject object = JSONObject.parseObject(responseBody);this.checkResponse(object);return AuthUser.builder().uuid(object.getString("id")).username(object.getString("username")).nickname(object.getString("name")).avatar(object.getString("avatar_url")).blog(object.getString("web_url")).company(object.getString("organization")).location(object.getString("location")).email(object.getString("email")).remark(object.getString("bio")).gender(AuthUserGender.UNKNOWN).token(authToken).source(source.toString()).build();}private void checkResponse(JSONObject object) {// oauth/token 验证异常if (object.containsKey("error")) {throw new AuthException(object.getString("error_description"));}// user 验证异常if (object.containsKey("message")) {throw new AuthException(object.getString("message"));}}/*** 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}** @param state state 验证授权流程的参数,可以防止csrf* @return 返回授权地址* @since 1.11.0*/@Overridepublic String authorize(String state) {return UrlBuilder.fromBaseUrl(super.authorize(state)).queryParam("scope", "read_user+openid").build();}
}

修改JustAuthController

这儿我们对JustAuthController最一下修改,让其支持多平台,完整代码如下:

package me.zhyd.justauth;import me.zhyd.justauth.cache.AuthStateRedisCache;
import me.zhyd.justauth.ext.AuthMyGitlabRequest;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.request.AuthDingTalkRequest;
import me.zhyd.oauth.request.AuthGiteeRequest;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 实战演示如何使用JustAuth实现第三方登录** @author yadong.zhang (yadong.zhang0415(a)gmail.com)* @version 1.0.0* @since 1.0.0*/
@RestController
@RequestMapping("/oauth")
public class JustAuthController {/*** 注入自定义的缓存实现类*/@Autowiredprivate AuthStateRedisCache stateRedisCache;/*** 获取授权链接并跳转到第三方授权页面** @param response response* @throws IOException response可能存在的异常*/@RequestMapping("/render/{source}")// ...public void renderAuth(@PathVariable("source") String source, HttpServletResponse response) throws IOException {AuthRequest authRequest = getAuthRequest(source);String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());response.sendRedirect(authorizeUrl);}/*** 用户在确认第三方平台授权(登录)后, 第三方平台会重定向到该地址,并携带code、state等参数** @param callback 第三方回调时的入参* @return 第三方平台的用户信息*/@RequestMapping("/callback/{source}")// ...public Object login(@PathVariable("source") String source, AuthCallback callback) {AuthRequest authRequest = getAuthRequest(source);return authRequest.login(callback);}/*** 获取授权Request** @return AuthRequest*/// ...private AuthRequest getAuthRequest(String source) {AuthRequest authRequest = null;switch (source) {case "gitee":authRequest = new AuthGiteeRequest(AuthConfig.builder().clientId("4c504cd2e1b1dbaba8dc1187d8070adf679acab17b2bc9cf6dfa76b9ae06aadc").clientSecret("fa5857175723475e4675e36af9eafde338545c1a0dfa49d1e0cc78f9c3ce5ebe").redirectUri("http://localhost:8080/oauth/callback/gitee").build(), stateRedisCache);break;case "mygitlab":authRequest = new AuthMyGitlabRequest(AuthConfig.builder().clientId("6ff1e2ccc356a4c193b663a2fbd4be34807e97a630e6e225d8d980ee9406d4a1").clientSecret("d935d24579e689c7cc8b41407a1b7886e45e8da3cd40fb2694bc8a00c0430c4e").redirectUri("http://localhost:8080/oauth/callback/mygitlab").build());break;default:break;}if (null == authRequest) {throw new AuthException("未获取到有效的Auth配置");}return authRequest;}}

重启项目,浏览器端访问http://localhost:8080/oauth/render/mygitlab将会看到如下页面:

点击Authorize后就完成了gitlab私服的登录

至此,我们就实现了集成自有Gitlab私服登录的功能

新计划:支持更多语言SDK

最后,发布一条启示,招募更多 NodeJS、Python、PHP、Go 社区开发者:

JustAuth组织:https://github.com/justauth

  • Java(已完成:https://github.com/justauth/JustAuth)

  • .Net(已完成:https://github.com/justauth/CollectiveOAuth,此项目为参考 JustAuth 开发,现已收录到 JustAuth 组织中)

JustAuth 计划开源 以下版本的 SDK:

  • NodeJS
  • Python
  • PHP
  • Go

最近的一版,将优先考虑推出 NodeJS 版的 JustAuth,如果感兴趣的小伙伴,欢迎报名,欢迎一起来做。还是那句话:和一群志同道合的朋友一起做一件喜欢的事情,是一种幸福。

如有兴趣,请加wx:rz04151123(添加备注: JustAuth开发),拉你进专属开发者交流群。

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

一杯茶的时间,上手第三方登录类库 JustAuth相关推荐

  1. 容我喝一杯 java_花一杯茶的时间,学会Java反射(实用篇)

    前言 之前,我写了一篇关于java反射的使用--花一杯茶的时间,学会Java反射(基础篇),今天就拿一些实例和应用给大家看看如何在项目中用反射. 实例 反射可以提高代码的扩展性,低耦合,高内聚.就拿工 ...

  2. 一杯茶的时间,上手Zabbix

    " IT 运维离不开系统监控,就好像鱼儿离不开水一样.一款强大的监控系统可以有力保证设备和业务的稳定. 来自:51cto技术栈 在监控系统层出不穷的今天,作为老牌监控系统的 Zabbix 依 ...

  3. zabbix 5.0所有依赖包_一杯茶的时间,上手Zabbix

    [51CTO.com原创稿件] IT 运维离不开系统监控,就好像鱼儿离不开水一样.一款强大的监控系统可以有力保证设备和业务的稳定. 图片来自 Pexels 在监控系统层出不穷的今天,作为老牌监控系统的 ...

  4. 一杯茶的时间,上手 Docker

    在正式阅读这篇文章之前,我们希望你已经具备以下条件: •最基本的命令行操作经验•对计算机网络有一定的了解,特别是应用层中的端口这一概念•最好经历过配环境.部署项目的痛苦挣扎??? 我们将实现什么 现在 ...

  5. 一杯茶的时间,上手 Taro 京东小程序开发

    我们研发开源了一款基于 Git 进行技术实战教程写作的工具,我们图雀社区的所有教程都是用这款工具写作而成,欢迎 Star 哦 如果你想快速了解如何使用,欢迎阅读我们的 教程文档 哦 本文由图雀社区成员 ...

  6. 一杯茶的时间,搞定JavaScript提升

    ✍️ 前言 我们都知道在JavaScript中同一个作用域内的变量都属于这个区域,在别的作用域是获取不到这个变量的. 但是在同一个作用域内,其中变量的声明的先后顺序和位置其实是有奇特的关系的,而这个奇 ...

  7. Spring Boot 快速集成第三方登录功能

    Spring Boot 快速集成第三方登录功能 前言 此 demo 主要演示 Spring Boot 项目如何使用 史上最全的第三方登录工具 - JustAuth 实现第三方登录,包括 QQ 登录.G ...

  8. 如何从零开始对接第三方登录(Java版):QQ登录和微博登录

    前言 个人网站最近增加了评论功能,为了方便用户不用注册就可以评论,对接了QQ和微博这2大常用软件的一键登录,总的来说其实都挺简单的,可能会有一点小坑,但不算多,完整记录下来方便后来人快速对接. 后台设 ...

  9. 一杯茶的功夫让你学会OA选型

    很多管理者都认为,选择管理软件是非常头痛的事,其实,看清本质,看透事实,一切都是如此的简单--那么,看看如何用一杯茶的时间来学会OA选型吧! 1.最重要的就是企业各部门都需要些什么功能?综合大家的需求 ...

  10. SpringBoot项目中集成第三方登录功能

    SpringBoot项目中集成第三方登录功能 引言 1 环境准备 2 代码实现 3 第三方平台认证申请 4 打包和部署项目 5 第三方平台登录认证测试 6 参考文章 引言 最近想把自己在公众号上介绍过 ...

最新文章

  1. SAP MCH1表和MCHA表更新逻辑
  2. 记录奥林比克/课程录制 洛谷P2255 [USACO14JAN]
  3. BZOJ 3101: N皇后 构造
  4. springboot4.1.1的log4j2配置
  5. 【java】map的几种遍历方式
  6. python综合管理系统_学生综合信息管理系统
  7. keras构建卷积神经网络_在Keras中构建,加载和保存卷积神经网络
  8. python怎么绘制渐变图_用Python画colorbar渐变图+修改刻度大小+修改渐变颜色
  9. ASP.NET MVC5 与EF6学习系列
  10. eclipse中使用Lombok(转)
  11. 最大公约数GCD的三种算法程序
  12. 快捷键截屏_win7截图快捷键是什么 win7截图快捷键怎么按
  13. java里美元符_Java语言标识符中可以使用美元符
  14. OC语言--OC语言基础、类
  15. 图片复印如何去除黑底_如何将扫描后的图片底色去掉
  16. native react 图片裁剪_React Native图片选择裁剪组件
  17. B. Shashlik Cooking
  18. eas表单分录带出自定义核算项目
  19. android ip v6 teredo,Win7系统通过teredo连接IPv6的方法
  20. 王煜全分析:四大类手机游戏的未来机会

热门文章

  1. 蛋白组学搜库分析软件 MaxQuant使用教程
  2. Excel里怎么设置输入可以打钩的选择框?
  3. SpringBoot+JWT+SpringSecurity对api进行授权保护
  4. kindle不能接收qq邮箱超大附件
  5. 2022吴恩达机器学习课程学习笔记(第二课第一周)
  6. 【微信小游戏】微信小游戏开发设置竖屏
  7. Win10怎么搜索文件内容?Win10通过文件内容查找文件的方法
  8. 给各位“老划水员”分享10款提高幸福指数的VSCode“摸鱼神器“
  9. APICloud 上传文件到云数据库2.0的代码实现
  10. Source(拉电流) Sink(灌电流)意思的歪记方法