# justauth-tutorial **Repository Path**: justauth/justauth-tutorial ## Basic Information - **Project Name**: justauth-tutorial - **Description**: JustAuth实战教程 - **Primary Language**: Java - **License**: MulanPSL-1.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 3 - **Created**: 2020-04-18 - **Last Updated**: 2024-01-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ![](https://imgkr.cn-bj.ufileos.com/eb747293-43ed-4f74-ad07-a9c37c9c6004.png) # JustAuth实战文档 JustAuth,如你所见,它仅仅是一个第三方授权登录的工具类库,它可以让我们脱离繁琐的第三方登录SDK,让登录变得So easy! 本专栏将会由浅入深,详细介绍如何使用JustAuth实现第三方登录,以及如何使用JustAuth的高级特性。 ## :tada: 使用SpringBoot初始化项目 在教程正式开始前,我们要先准备好相应的软件环境。[JustAuth](https://gitee.com/yadong.zhang/JustAuth) 在IDEA下使用以下方式创建项目:依次点击`File-New-Project`然后选择Spring Initializr,根据提示进行操作,配置依赖时勾选`spring-boot-starter-web`和`lombok`两个依赖,为了方便开发测试,我这儿多选了一个`spring-boot-devtools`。 按照提示创建完成后,得到POM如下 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.6.RELEASE me.zhyd.justauth justauth-tutorial 0.0.1-SNAPSHOT justauth-tutorial Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.boot spring-boot-maven-plugin ``` 等待项目编译完成后,我们开始正式集成JustAuth。 ## :heavy_plus_sign: 添加JustAuth依赖 正式开始前,建议朋友们先看一下JustAuth的用户文档:[https://docs.justauth.whnb.wang](https://docs.justauth.whnb.wang), 重点关注快速开始这个章节。 ![](https://imgkr.cn-bj.ufileos.com/f6275b07-1b47-4445-b27f-b962cedbea0e.png) 这个章节中包含了关于OAuth和JustAuth相关的**重要**信息,再次建议优先查看该章节内容。 重申一遍,使用JustAuth总共分三步(**这三步也适合于JustAuth支持的任何一个平台**): 1. 申请注册第三方平台的开发者账号 2. 创建第三方平台的应用,获取配置信息(`accessKey`, `secretKey`, `redirectUri`) 3. 使用该工具实现授权登陆 当然, 第三步和前两步谁先谁后都无所谓,您可以先实现代码再申请第三方应用,也可以先申请第三方应用,再集成代码。 接下来我们按照[文档](https://docs.justauth.whnb.wang/#/how-to-use?id=%e4%bd%bf%e7%94%a8%e6%96%b9%e5%bc%8f)上的第一步指示,先添加pom依赖。 ```xml // ... me.zhyd.oauth JustAuth 1.15.1 // ... ``` 依赖添加完成后,等待项目编译完成,接下来我们就开始正式接入JustAuth。 ## :heavy_plus_sign: 集成JustAuth的API 注意,以下代码中,我们的请求链接中是通过动态参数`{source}`去取的,这样可以方便的让我们集成任意平台,比如集成gitee时, 我们的请求地址就是:http://localhost:8080/oauth/render/gitee, 而回调地址就是http://localhost:8080/oauth/callback/gitee。 当然,例子中只是举例告诉大家可以这么用,但如果各位只需要集成单一平台的话, 可以直接将`{souce}`改为平台名,如gitee ```java 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后,我们点击右上角用户头像,选择设置,然后点击第三方应用进入第三方应用管理页面,点击右上角的创建应用按钮,进入应用创建页面 ![](https://imgkr.cn-bj.ufileos.com/c5ceab99-c5c6-4139-8e01-7fd69723007b.png) ![](https://imgkr.cn-bj.ufileos.com/4d7078b5-84ee-4f07-9d0d-7f98ba0c00a6.png) 我们按照提示填入我们的应用信息即可。 **应用名称:** 一般填写自己的网站名称即可 **应用描述:** 一般填写自己的应用描述即可 **应用主页:** 填写自己的网站首页地址 **应用回调地址:** **重点**,该地址为用户授权后需要跳转到的自己网站的地址,默认携带一个code参数 **权限:** 根据页面提示操作,默认勾选第一个就行,因为我们只需要获取用户信息即可 以上信息输入完成后,点击确定按钮创建应用。创建完成后,点击进入应用详情页,可以看到应用的密钥等信息 ![](https://imgkr.cn-bj.ufileos.com/263eb7b8-a20e-452d-803e-4a071fb7b4f4.png) 复制以下三个信息:**Client ID**、**Client Secret**和**应用回调地址**。 ## :heavy_plus_sign: 自定义HTTP工具 将上一步获取到配置信息配置`AuthConfig`中,如下: ```java // ... */ 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](http://localhost:8080/oauth/render/gitee),当出现以下页面时,表示我们已经集成成功,并且已跳转到了第三方的授权页面。 ![](https://imgkr.cn-bj.ufileos.com/aa7514fd-267e-418c-88ab-414611e01319.png) 注意,如果您当前没有在浏览器中登录过您的账号,您将会看到如下页面: ![](https://imgkr.cn-bj.ufileos.com/e8bdd8e2-16f7-46bb-9c46-411ebcceb52f.png) 我们点击“同意授权”后,第三方应用(Gitee)将会生成一个**code授权码**,连带着我们先前传过去的**state**一并回调到我们配置的**redirectUri**接口中。 ![](https://imgkr.cn-bj.ufileos.com/189e079f-836b-45df-9feb-690372ed8f66.png) 如上图,进入回调接口后,我们可以断点跟踪到第三方平台传回的信息:state和code。 如果您是新项目,这儿可能会出现一个小小的问题: ```java 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](https://gitee.com/yadong.zhang/JustAuth/releases/v1.14.0)开始默认集成了的[simple-http](https://github.com/xkcoding/simple-http)作为HTTP通用接口(更新说明见[JustAuth 1.14.0版本正式发布!完美解耦HTTP工具](https://mp.weixin.qq.com/s?__biz=MzA3NDk3OTIwMg==&mid=2450633197&idx=1&sn=11e625b307db62b2f1c4e82f7744b2a2&chksm=88929300bfe51a16562b45592a264482ae2c74c6dbfa4a3aa9611ad4fea4a9be5b1f0545527d&token=1093833287&lang=zh_CN#rd)),鉴于一般项目中都已经集成了HTTP工具,比如OkHttp3、apache HttpClient、hutool-http,因此为了减少不必要的依赖,从[v1.14.0](https://gitee.com/yadong.zhang/JustAuth/releases/v1.14.0)开始JustAuth将不会默认集成hutool-http,如果开发者的项目是全新的或者项目内没有集成HTTP实现工具,还需要添加对应的HTTP实现类,JustAuth提供了三种备选的pom依赖: **hutool-http** ```xml cn.hutool hutool-http 5.2.5 ``` **httpclient** ```xml org.apache.httpcomponents httpclient 4.5.12 ``` **okhttp** ```xml com.squareup.okhttp3 okhttp 4.4.1 ``` 添加完HTTP工具依赖后,重启项目,重新访问[http://localhost:8080/oauth/render/gitee](http://localhost:8080/oauth/render/gitee)链接,然后进行授权。 授权完成后,您将会看到如下页面: ![](https://imgkr.cn-bj.ufileos.com/6834e097-407d-4b5a-963a-b1283e65db9a.png) ## :heavy_plus_sign: 自定义缓存 在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](https://tools.ietf.org/html/rfc6749#section-10.12). ——以上内容节选自《[The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749)》4.1.1 简单翻译就是说:state是用于维护请求和回调之间状态的不透明值。这儿的“不透明”理解为“不可预测”更好些。在没有使用state时,已集成OAuth登录的网站极易受到CSRF攻击,关于实现CSRF攻击的细节和流程已经危害,这儿不作赘述,感兴趣的朋友可以参考:[Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12)和[移花接木:针对OAuth2的CSRF攻击](https://www.jianshu.com/p/c7c8f51713b6) 两篇文章。 由此可见,state贯穿整个OAuth授权流程,能够确保流程之间的连续性和安全性。但因其是个非必选的参数,所以大多时候开发者都会忘记使用该参数,并且多个第三方平台的OAuth API中也是非必选state。 在OAuth流程中,code参数都是有时效性的,一般为10分钟有效期,如果超过10分钟未使用,则需要重新申请code信息。而对于state来说,OAuth官网文档中并未给出时效性的说明,也就是说只能客户端本身去对state做时效性和有效性作校验。 JustAuth考虑到这一点,在所有OAuth流程中一方面都会传递state参数用于确保流程的完整性,如果客户端没有手动指定,则会使用默认的唯一值;另一方面也会对state做缓存处理,确保state也有其时效性,防止state被重复利用。 JustAuth中默认使用了本地map的方式,实现对state的缓存,详情可参考 [AuthDefaultCache](https://github.com/justauth/JustAuth/blob/master/src/main/java/me/zhyd/oauth/cache/AuthDefaultCache.java) 类 ```java 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 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 缓存内容 */ @Override public void set(String key, String value) { set(key, value, AuthCacheConfig.timeout); } /** * 设置缓存 * * @param key 缓存KEY * @param value 缓存内容 * @param timeout 指定缓存过期时间(毫秒) */ @Override public 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 缓存内容 */ @Override public 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不存在或者已过期 */ @Override public boolean containsKey(String key) { readLock.lock(); try { CacheState cacheState = stateCache.get(key); return null != cacheState && !cacheState.isExpired(); } finally { readLock.unlock(); } } /** * 清理过期的缓存 */ @Override public void pruneCache() { Iterator 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 @Setter private 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依赖 ```xml // ... org.springframework.boot spring-boot-starter-data-redis // ... ``` 然后在配置文件中添加redis的基本配置 ```properties // ... spring.redis.database=0 spring.redis.host=localhost spring.redis.port=6379 spring.redis.password= ``` 接下来实现JustAuth对外提供的缓存接口AuthStateCache,我们使用RedisTemplate实现对state的缓存。 ```java 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 { @Autowired private RedisTemplate redisTemplate; private ValueOperations valueOperations; @PostConstruct public void init() { valueOperations = redisTemplate.opsForValue(); } /** * 存入缓存,默认3分钟 * * @param key 缓存key * @param value 缓存内容 */ @Override public void cache(String key, String value) { valueOperations.set(key, value, AuthCacheConfig.timeout, TimeUnit.MILLISECONDS); } /** * 存入缓存 * * @param key 缓存key * @param value 缓存内容 * @param timeout 指定缓存过期时间(毫秒) */ @Override public void cache(String key, String value, long timeout) { valueOperations.set(key, value, timeout, TimeUnit.MILLISECONDS); } /** * 获取缓存内容 * * @param key 缓存key * @return 缓存内容 */ @Override public String get(String key) { return valueOperations.get(key); } /** * 是否存在key,如果对应key的value值已过期,也返回false * * @param key 缓存key * @return true:存在key,并且value没过期;false:key不存在或者已过期 */ @Override public boolean containsKey(String key) { return redisTemplate.hasKey(key); } } ``` > **注意** > > AuthCacheConfig为JustAuth默认的缓存配置类,AuthCacheConfig.timeout为内置的缓存过期时间,默认3分钟有效期 JustAuth对外提供的Request类,默认都会支持两种构造参数,一种是只需传入AuthConfig,JustAuth默认使用内置缓存;另外一种则是支持传入AuthConfig和AuthStateCache ![](https://imgkr.cn-bj.ufileos.com/01fc7ff8-de00-4800-aa5b-56dd6ec43c7f.png) 接下来我们需要将上一步创建的stateCache实现类注入到JustAuth的Request中。 ```java // ... /** * 注入自定义的缓存实现类 */ @Autowired private AuthStateRedisCache stateRedisCache; // ... private AuthRequest getAuthRequest() { return new AuthGiteeRequest(AuthConfig.builder() .clientId("4c504cd2e1b1dbaba8dc1187d8070adf679acab17b2bc9cf6dfa76b9ae06aadc") .clientSecret("fa5857175723475e4675e36af9eafde338545c1a0dfa49d1e0cc78f9c3ce5ebe") .redirectUri("http://localhost:8080/oauth/callback/gitee") // ... .build(), stateRedisCache); } // ... ``` 我们通过断点看一下是否已启用我们自定义的缓存实现。 ![](https://imgkr.cn-bj.ufileos.com/da94f778-2619-4c2a-99b7-4ef99fd8ec38.png) 可以看到,当前使用的缓存实现就是我们自定义的缓存。 > **注意** > > AuthCacheConfig.timeout为内置的缓存过期时间,默认3分钟有效期。主要基于正常流程考虑,一个授权流程的时间一般不会太长,因此综合考虑下,为了保证OAuth流程能够正常走完,且保证state的有效性,系统默认3分钟有效期。可以通过重新赋值 AuthCacheConfig.timeout实现缓存时间自定义或者在实现`public void cache(String key, String value)`方法时自定义缓存过期时间。 ## :heavy_plus_sign: 集成自有的Gitlab私服登录 [JustAuth](https://github.com/justauth/JustAuth)发展到现在,基本上已经涵盖了国内外大多数知名的网站。[JustAuth](https://github.com/justauth/JustAuth)也一直以它的**全**和**简**,备受各位开发者的喜爱和支持。 但现在OAuth技术越来越成熟,越来越多的个人站长或者企业都开始搭建自己的OAuth授权平台,那么针对这种情况,[JustAuth](https://github.com/justauth/JustAuth)并不能做到面面俱到,也无法集成所有支持OAuth的网站(这也是不现实的)。 既然考虑到有这种需求,那么就要想办法解决、想办法填补漏洞,不为了自己,也为了陪伴[JustAuth](https://github.com/justauth/JustAuth)一路走来的所有朋友们。 [ JustAuth](https://github.com/justauth/JustAuth)开发团队也在[v1.12.0](https://github.com/justauth/JustAuth/releases/tag/v1.12.0)版本中新加入了一大特性,就是可以支持**任意**支持OAuth的网站通过JustAuth实现便捷的OAuth登录! 本节内容,将会演示**如何集成自有的Gitlab私服实现第三方登录。** **** ## 准备Gitlab私服 首先我们要有一个可用的Gitlab服私服,如果没有请自行解决。 ## 创建OAuth应用 ![](https://imgkr.cn-bj.ufileos.com/a5789c0b-d0ba-47ad-bc7c-b9238092afcb.png) 创建完成后将会得到如下内容: ![](https://imgkr.cn-bj.ufileos.com/b927c963-54fe-469b-9029-932b1243fda5.png) 复制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 ```java 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 */ @Override public String authorize() { return "http://gitlab.demo.dev/oauth/authorize"; } /** * 获取accessToken的api * * @return url */ @Override public String accessToken() { return "http://gitlab.demo.dev/oauth/token"; } /** * 获取用户信息的api * * @return url */ @Override public 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](https://github.com/justauth/JustAuth/blob/master/src/main/java/me/zhyd/oauth/request/AuthGitlabRequest.java)即可,完整代码如下: ```java 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); } @Override protected 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(); } @Override protected 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 */ @Override public String authorize(String state) { return UrlBuilder.fromBaseUrl(super.authorize(state)) .queryParam("scope", "read_user+openid") .build(); } } ``` ## 修改JustAuthController 这儿我们对JustAuthController最一下修改,让其支持多平台,完整代码如下: ```java 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 { /** * 注入自定义的缓存实现类 */ @Autowired private 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将会看到如下页面: ![](https://imgkr.cn-bj.ufileos.com/a9cdde42-c922-424e-a377-e2a9f8324c63.png) 点击**Authorize**后就完成了gitlab私服的登录 ![](https://imgkr.cn-bj.ufileos.com/187a5ec6-a174-4f54-8846-60a894384b3a.png) 至此,我们就实现了**集成自有Gitlab私服登录的功能**。