: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 .
——以上内容节选自《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;public class AuthDefaultCache implements AuthCache { 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); } } @Override public void set (String key, String value) { set(key, value, AuthCacheConfig.timeout); } @Override public void set (String key, String value, long timeout) { writeLock.lock(); try { stateCache.put(key, new CacheState(value, timeout)); } finally { writeLock.unlock(); } } @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(); } } @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<CacheState> values = stateCache.values().iterator(); CacheState cacheState; while (values.hasNext()) { cacheState = values.next(); if (cacheState.isExpired()) { values.remove(); } } } 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依赖
pom.xml [tuture-add] [tuture-add] <dependency > [tuture-add] <groupId > org.springframework.boot</groupId > [tuture-add] <artifactId > spring-boot-starter-data-redis</artifactId > [tuture-add] </dependency >
然后在配置文件中添加redis的基本配置
src/main/resources/application.properties ... [tuture-add]spring.redis.database=0 [tuture-add]spring.redis.host=localhost [tuture-add]spring.redis.port=6379 [tuture-add]spring.redis.password=
接下来实现JustAuth对外提供的缓存接口AuthStateCache,我们使用RedisTemplate实现对state的缓存。
src/main/java/me/zhyd/justauth/cache/AuthStateRedisCache.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;@Component public class AuthStateRedisCache implements AuthStateCache { @Autowired private RedisTemplate<String, String> redisTemplate; private ValueOperations<String, String> valueOperations; @PostConstruct public void init () { valueOperations = redisTemplate.opsForValue(); } @Override public void cache (String key, String value) { valueOperations.set(key, value, AuthCacheConfig.timeout, TimeUnit.MILLISECONDS); } @Override public void cache (String key, String value, long timeout) { valueOperations.set(key, value, timeout, TimeUnit.MILLISECONDS); } @Override public String get (String key) { return valueOperations.get(key); } @Override public boolean containsKey (String key) { return redisTemplate.hasKey(key); } }
注意
AuthCacheConfig为JustAuth默认的缓存配置类,AuthCacheConfig.timeout为内置的缓存过期时间,默认3分钟有效期
JustAuth对外提供的Request类,默认都会支持两种构造参数,一种是只需传入AuthConfig,JustAuth默认使用内置缓存;另外一种则是支持传入AuthConfig和AuthStateCache
接下来我们需要将上一步创建的stateCache实现类注入到JustAuth的Request中。
src/main/java/me/zhyd/justauth/JustAuthController.java [tuture-add] [tuture-add] @Autowired [tuture-add] private AuthStateRedisCache stateRedisCache; private AuthRequest getAuthRequest () { return new AuthGiteeRequest(AuthConfig.builder() .clientId("4c504cd2e1b1dbaba8dc1187d8070adf679acab17b2bc9cf6dfa76b9ae06aadc" ) .clientSecret("fa5857175723475e4675e36af9eafde338545c1a0dfa49d1e0cc78f9c3ce5ebe" ) .redirectUri("http://localhost:8080/oauth/callback/gitee" ) [tuture-add] .build(), stateRedisCache); }
我们通过断点看一下是否已启用我们自定义的缓存实现。
可以看到,当前使用的缓存实现就是我们自定义的缓存。
注意
AuthCacheConfig.timeout为内置的缓存过期时间,默认3分钟有效期。主要基于正常流程考虑,一个授权流程的时间一般不会太长,因此综合考虑下,为了保证OAuth流程能够正常走完,且保证state的有效性,系统默认3分钟有效期。可以通过重新赋值 AuthCacheConfig.timeout实现缓存时间自定义或者在实现public void cache(String key, String value)
方法时自定义缓存过期时间。
:heavy_plus_sign: 集成自有的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. 非必须实现接口(部分平台不支持)
注:
注意
当通过JustAuth扩展实现第三方授权时,请参考AuthDefaultSource
自行创建对应的枚举类并实现AuthSource接口 如果不是使用的枚举类,那么在授权成功后获取用户信息时,需要单独处理source
字段的赋值 如果扩展了对应枚举类时,在me.zhyd.oauth.request.AuthRequest#login(AuthCallback)
中可以通过xx.toString()
获取对应的source
src/main/java/me/zhyd/justauth/ext/AuthCustomSource.java package me.zhyd.justauth.ext;import me.zhyd.oauth.config.AuthSource;public enum AuthCustomSource implements AuthSource { MYGITLAB { @Override public String authorize () { return "http://gitlab.demo.dev/oauth/authorize" ; } @Override public String accessToken () { return "http://gitlab.demo.dev/oauth/token" ; } @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 即可,完整代码如下:
src/main/java/me/zhyd/justauth/ext/AuthMyGitlabRequest.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;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) { if (object.containsKey("error" )) { throw new AuthException(object.getString("error_description" )); } if (object.containsKey("message" )) { throw new AuthException(object.getString("message" )); } } @Override public String authorize (String state) { return UrlBuilder.fromBaseUrl(super .authorize(state)) .queryParam("scope" , "read_user+openid" ) .build(); } }
修改JustAuthController 这儿我们对JustAuthController最一下修改,让其支持多平台,完整代码如下:
src/main/java/me/zhyd/justauth/JustAuthController.java package me.zhyd.justauth;import me.zhyd.justauth.cache.AuthStateRedisCache;[tuture-add]import me.zhyd.justauth.ext.AuthMyGitlabRequest; import me.zhyd.oauth.config.AuthConfig;[tuture-add]import me.zhyd.oauth.exception.AuthException; import me.zhyd.oauth.model.AuthCallback;[tuture-add]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;[tuture-add]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;@RestController @RequestMapping ("/oauth" )public class JustAuthController { @Autowired private AuthStateRedisCache stateRedisCache; @RequestMapping ("/render/{source}" ) [tuture-add] public void renderAuth (@PathVariable("source" ) String source, HttpServletResponse response) throws IOException { [tuture-add] AuthRequest authRequest = getAuthRequest(source); String authorizeUrl = authRequest.authorize(AuthStateUtils.createState()); response.sendRedirect(authorizeUrl); } @RequestMapping ("/callback/{source}" ) [tuture-add] public Object login (@PathVariable("source" ) String source, AuthCallback callback) { [tuture-add] AuthRequest authRequest = getAuthRequest(source); return authRequest.login(callback); } [tuture-add] private AuthRequest getAuthRequest (String source) { [tuture-add] AuthRequest authRequest = null ; [tuture-add] switch (source) { [tuture-add] case "gitee" : [tuture-add] authRequest = new AuthGiteeRequest(AuthConfig.builder() [tuture-add] .clientId("4c504cd2e1b1dbaba8dc1187d8070adf679acab17b2bc9cf6dfa76b9ae06aadc" ) [tuture-add] .clientSecret("fa5857175723475e4675e36af9eafde338545c1a0dfa49d1e0cc78f9c3ce5ebe" ) [tuture-add] .redirectUri("http://localhost:8080/oauth/callback/gitee" ) [tuture-add] .build(), stateRedisCache); [tuture-add] break ; [tuture-add] case "mygitlab" : [tuture-add] authRequest = new AuthMyGitlabRequest(AuthConfig.builder() [tuture-add] .clientId("6ff1e2ccc356a4c193b663a2fbd4be34807e97a630e6e225d8d980ee9406d4a1" ) [tuture-add] .clientSecret("d935d24579e689c7cc8b41407a1b7886e45e8da3cd40fb2694bc8a00c0430c4e" ) [tuture-add] .redirectUri("http://localhost:8080/oauth/callback/mygitlab" ) [tuture-add] .build()); [tuture-add] break ; [tuture-add] default : [tuture-add] break ; [tuture-add] } [tuture-add] if (null == authRequest) { [tuture-add] throw new AuthException("未获取到有效的Auth配置" ); [tuture-add] } [tuture-add] return authRequest; } }
重启项目,浏览器端访问http://localhost:8080/oauth/render/mygitlab将会看到如下页面:
点击Authorize 后就完成了gitlab私服的登录
至此,我们就实现了集成自有Gitlab私服登录的功能 。