前言
公共接口,定义了对第三方平台进行授权、登录、撤销授权和刷新 token 的操作
1. 基本知识
先看源码基本API接口:
import me.zhyd.oauth.enums.AuthResponseStatus;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
/**
* JustAuth {@code Request}公共接口,所有平台的{@code Request}都需要实现该接口
* <p>
* {@link AuthRequest#authorize()}
* {@link AuthRequest#authorize(String)}
* {@link AuthRequest#login(AuthCallback)}
* {@link AuthRequest#revoke(AuthToken)}
* {@link AuthRequest#refresh(AuthToken)}
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @since 1.8
*/
public interface AuthRequest {
/**
* 返回授权url,可自行跳转页面
* <p>
* 不建议使用该方式获取授权地址,不带{@code state}的授权地址,容易受到csrf攻击。
* 建议使用{@link AuthDefaultRequest#authorize(String)}方法生成授权地址,在回调方法中对{@code state}进行校验
*
* @return 返回授权地址
*/
@Deprecated
default String authorize() {
throw new AuthException(AuthResponseStatus.NOT_IMPLEMENTED);
}
/**
* 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
*
* @param state state 验证授权流程的参数,可以防止csrf
* @return 返回授权地址
*/
default String authorize(String state) {
throw new AuthException(AuthResponseStatus.NOT_IMPLEMENTED);
}
/**
* 第三方登录
*
* @param authCallback 用于接收回调参数的实体
* @return 返回登录成功后的用户信息
*/
default AuthResponse login(AuthCallback authCallback) {
throw new AuthException(AuthResponseStatus.NOT_IMPLEMENTED);
}
/**
* 撤销授权
*
* @param authToken 登录成功后返回的Token信息
* @return AuthResponse
*/
default AuthResponse revoke(AuthToken authToken) {
throw new AuthException(AuthResponseStatus.NOT_IMPLEMENTED);
}
/**
* 刷新access token (续期)
*
* @param authToken 登录成功后返回的Token信息
* @return AuthResponse
*/
default AuthResponse refresh(AuthToken authToken) {
throw new AuthException(AuthResponseStatus.NOT_IMPLEMENTED);
}
}
大致方法如下:
authorize()
:返回授权 URL,但已被标记为过时 (@Deprecated)。不建议使用此方法获取授权地址,因为不带 state 的授权地址容易受到 CSRF 攻击authorize(String state)
:返回带 state 参数的授权 URL,授权回调时会带上这个 state,用于验证授权流程的参数,防止 CSRF 攻击login(AuthCallback authCallback)
:第三方登录方法,用于接收回调参数的实体,并返回登录成功后的用户信息revoke(AuthToken authToken)
:撤销授权方法,用于撤销登录成功后返回的 Token 信息refresh(AuthToken authToken)
:刷新 Access Token 方法,用于续期登录成功后返回的 Token 信息
2. Demo
根据上述接口,简单测试下接口功能:
制造一个第三方的类:
package com.example.test;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.request.AuthRequest;
public class GitHubAuthProvider implements AuthRequest {
@Override
public String authorize(String state) {
// 返回 GitHub 授权 URL,并将 state 参数添加到 URL 中
return "https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&state=" + state;
}
@Override
public AuthResponse login(AuthCallback authCallback) {
// 模拟 GitHub 登录成功后返回的用户信息
return new AuthResponse(200, "Success", "GitHubUser,github@example.com");
}
@Override
public AuthResponse revoke(AuthToken authToken) {
// 撤销授权的具体实现
return new AuthResponse(200, "Success", "Authorization revoked successfully");
}
@Override
public AuthResponse refresh(AuthToken authToken) {
// 刷新 Access Token 的具体实现
return new AuthResponse(200, "Success", "Access Token refreshed successfully");
}
}
对应的接口测试类如下:
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.request.AuthRequest;
public class test {
public static void main(String[] args) {
// 创建一个第三方平台的具体实现对象
AuthRequest authRequest = new GitHubAuthProvider();
// 获取授权 URL,传入 state 参数
String authorizeUrl = authRequest.authorize("random_state_parameter");
System.out.println("Authorize URL: " + authorizeUrl);
// 模拟第三方登录操作,传入 AuthCallback 实体
AuthResponse authResponse = authRequest.login(new AuthCallback());
System.out.println("Login response: " + authResponse);
// 模拟撤销授权操作,传入登录成功后返回的 Token 信息
AuthToken authToken = new AuthToken();
AuthResponse revokeResponse = authRequest.revoke(authToken);
System.out.println("Revoke response: " + revokeResponse);
// 模拟刷新 Access Token 操作,传入登录成功后返回的 Token 信息
AuthResponse refreshResponse = authRequest.refresh(authToken);
System.out.println("Refresh response: " + refreshResponse);
}
}
截图如下:(默认的 toString() 方法返回的对象字符串表示形式)
3. 实战
上述Demo只是一个简易版
在实战中应对各个不同的应用,可以写个模板类
import java.util.Objects;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.request.AuthAlipayRequest;
import me.zhyd.oauth.request.AuthBaiduRequest;
import me.zhyd.oauth.request.AuthCodingRequest;
import me.zhyd.oauth.request.AuthCsdnRequest;
import me.zhyd.oauth.request.AuthDingTalkRequest;
import me.zhyd.oauth.request.AuthDouyinRequest;
import me.zhyd.oauth.request.AuthElemeRequest;
import me.zhyd.oauth.request.AuthFacebookRequest;
import me.zhyd.oauth.request.AuthGiteeRequest;
import me.zhyd.oauth.request.AuthGithubRequest;
import me.zhyd.oauth.request.AuthGitlabRequest;
import me.zhyd.oauth.request.AuthGoogleRequest;
import me.zhyd.oauth.request.AuthHuaweiRequest;
import me.zhyd.oauth.request.AuthKujialeRequest;
import me.zhyd.oauth.request.AuthLinkedinRequest;
import me.zhyd.oauth.request.AuthMeituanRequest;
import me.zhyd.oauth.request.AuthMiRequest;
import me.zhyd.oauth.request.AuthMicrosoftRequest;
import me.zhyd.oauth.request.AuthOschinaRequest;
import me.zhyd.oauth.request.AuthPinterestRequest;
import me.zhyd.oauth.request.AuthQqRequest;
import me.zhyd.oauth.request.AuthRenrenRequest;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.request.AuthStackOverflowRequest;
import me.zhyd.oauth.request.AuthTaobaoRequest;
import me.zhyd.oauth.request.AuthTeambitionRequest;
import me.zhyd.oauth.request.AuthToutiaoRequest;
import me.zhyd.oauth.request.AuthTwitterRequest;
import me.zhyd.oauth.request.AuthWeChatEnterpriseRequest;
import me.zhyd.oauth.request.AuthWeChatMpRequest;
import me.zhyd.oauth.request.AuthWeChatOpenRequest;
import me.zhyd.oauth.request.AuthWeiboRequest;
import org.springblade.core.social.props.SocialProperties;
public class SocialUtil {
public SocialUtil() {
}
public static AuthRequest getAuthRequest(String source, SocialProperties socialProperties) {
AuthDefaultSource authSource = (AuthDefaultSource)Objects.requireNonNull(AuthDefaultSource.valueOf(source.toUpperCase()));
AuthConfig authConfig = (AuthConfig)socialProperties.getOauth().get(authSource);
if (authConfig == null) {
throw new AuthException("未获取到有效的Auth配置");
} else {
AuthRequest authRequest = null;
switch (authSource) {
case GITHUB:
authRequest = new AuthGithubRequest(authConfig);
break;
case GITEE:
authRequest = new AuthGiteeRequest(authConfig);
break;
case OSCHINA:
authRequest = new AuthOschinaRequest(authConfig);
break;
case QQ:
authRequest = new AuthQqRequest(authConfig);
break;
case WECHAT_OPEN:
authRequest = new AuthWeChatOpenRequest(authConfig);
break;
case WECHAT_ENTERPRISE:
authRequest = new AuthWeChatEnterpriseRequest(authConfig);
break;
case WECHAT_MP:
authRequest = new AuthWeChatMpRequest(authConfig);
break;
case DINGTALK:
authRequest = new AuthDingTalkRequest(authConfig);
break;
case ALIPAY:
authRequest = new AuthAlipayRequest(authConfig);
break;
case BAIDU:
authRequest = new AuthBaiduRequest(authConfig);
break;
case WEIBO:
authRequest = new AuthWeiboRequest(authConfig);
break;
case CODING:
authRequest = new AuthCodingRequest(authConfig);
break;
case CSDN:
authRequest = new AuthCsdnRequest(authConfig);
break;
case TAOBAO:
authRequest = new AuthTaobaoRequest(authConfig);
break;
case GOOGLE:
authRequest = new AuthGoogleRequest(authConfig);
break;
case FACEBOOK:
authRequest = new AuthFacebookRequest(authConfig);
break;
case DOUYIN:
authRequest = new AuthDouyinRequest(authConfig);
break;
case LINKEDIN:
authRequest = new AuthLinkedinRequest(authConfig);
break;
case MICROSOFT:
authRequest = new AuthMicrosoftRequest(authConfig);
break;
case MI:
authRequest = new AuthMiRequest(authConfig);
break;
case TOUTIAO:
authRequest = new AuthToutiaoRequest(authConfig);
break;
case TEAMBITION:
authRequest = new AuthTeambitionRequest(authConfig);
break;
case PINTEREST:
authRequest = new AuthPinterestRequest(authConfig);
break;
case RENREN:
authRequest = new AuthRenrenRequest(authConfig);
break;
case STACK_OVERFLOW:
authRequest = new AuthStackOverflowRequest(authConfig);
break;
case HUAWEI:
authRequest = new AuthHuaweiRequest(authConfig);
break;
case KUJIALE:
authRequest = new AuthKujialeRequest(authConfig);
break;
case GITLAB:
authRequest = new AuthGitlabRequest(authConfig);
break;
case MEITUAN:
authRequest = new AuthMeituanRequest(authConfig);
break;
case ELEME:
authRequest = new AuthElemeRequest(authConfig);
break;
case TWITTER:
authRequest = new AuthTwitterRequest(authConfig);
}
if (null == authRequest) {
throw new AuthException("未获取到有效的Auth配置");
} else {
return (AuthRequest)authRequest;
}
}
}
}
类似的Github第三方平台如下:(以下类为API自带的)
import com.alibaba.fastjson.JSONObject;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
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.utils.GlobalAuthUtils;
import java.util.Map;
/**
* Github登录
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @since 1.0.0
*/
public class AuthGithubRequest extends AuthDefaultRequest {
public AuthGithubRequest(AuthConfig config) {
super(config, AuthDefaultSource.GITHUB);
}
public AuthGithubRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthDefaultSource.GITHUB, authStateCache);
}
@Override
protected AuthToken getAccessToken(AuthCallback authCallback) {
String response = doPostAuthorizationCode(authCallback.getCode());
Map<String, String> res = GlobalAuthUtils.parseStringToMap(response);
this.checkResponse(res.containsKey("error"), res.get("error_description"));
return AuthToken.builder()
.accessToken(res.get("access_token"))
.scope(res.get("scope"))
.tokenType(res.get("token_type"))
.build();
}
@Override
protected AuthUser getUserInfo(AuthToken authToken) {
String response = doGetUserInfo(authToken);
JSONObject object = JSONObject.parseObject(response);
this.checkResponse(object.containsKey("error"), object.getString("error_description"));
return AuthUser.builder()
.rawUserInfo(object)
.uuid(object.getString("id"))
.username(object.getString("login"))
.avatar(object.getString("avatar_url"))
.blog(object.getString("blog"))
.nickname(object.getString("name"))
.company(object.getString("company"))
.location(object.getString("location"))
.email(object.getString("email"))
.remark(object.getString("bio"))
.gender(AuthUserGender.UNKNOWN)
.token(authToken)
.source(source.toString())
.build();
}
private void checkResponse(boolean error, String error_description) {
if (error) {
throw new AuthException(error_description);
}
}
}
后续在使用过程的代码如下:
@NonDS
@Slf4j
@RestController
@AllArgsConstructor
@ConditionalOnProperty(value = "social.enabled", havingValue = "true")
public class BladeSocialEndpoint {
private final SocialProperties socialProperties;
/**
* 授权完毕跳转
*/
@RequestMapping("/oauth/render/{source}")
public void renderAuth(@PathVariable("source") String source, HttpServletResponse response) throws IOException {
AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties);
String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
response.sendRedirect(authorizeUrl);
}
/**
* 获取认证信息
*/
@RequestMapping("/oauth/callback/{source}")
public Object login(@PathVariable("source") String source, AuthCallback callback) {
AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties);
return authRequest.login(callback);
}
/**
* 撤销授权
*/
@RequestMapping("/oauth/revoke/{source}/{token}")
public Object revokeAuth(@PathVariable("source") String source, @PathVariable("token") String token) {
AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties);
return authRequest.revoke(AuthToken.builder().accessToken(token).build());
}
/**
* 续期令牌
*/
@RequestMapping("/oauth/refresh/{source}")
public Object refreshAuth(@PathVariable("source") String source, String token) {
AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties);
return authRequest.refresh(AuthToken.builder().refreshToken(token).build());
}
}