JustAuth,如你所见,它仅仅是一个第三方授权登录的工具类库,它可以让我们脱离繁琐的第三方登录SDK,让登录变得So easy!
github地址:https://github.com/justauth/JustAuth
对于spirng boot+Vue前后端分离的项目实现第三方登录比单纯spring boot相对比较麻烦,所以在此做个记录。
首先准备一台有公网 IP 的服务器,可以选用阿里云或者腾讯云,百度云也行,好像百度云最近有优惠。
frp 安装程序:https://github.com/fatedier/frp/releases
1.2.1. frp 服务端搭建
服务端搭建在上一步准备的公网服务器上,因为服务器是 centos7 x64 的系统,因此,这里下载安装包版本为 linux_amd64 的 frp_0.29.1_linux_amd64.tar.gz 。
用shell远程连接服务器,命令行输入
下载安装包(下载速度可能会比较慢)
$ wget https://github.com/fatedier/frp/releases/download/v0.29.1/frp_0.29.1_linux_amd64.tar.gz
$ tar -zxvf frp_0.29.1_linux_amd64.tar.gz
$ cd frp_0.29.1_linux_amd64
$ vim frps.ini
[common]
bind_port = 7100
vhost_http_port = 7200
$nohup /root/frp_0.29.1_linux_amd64/frps -c /root/frp_0.29.1_linux_amd64/frps.ini
nohup: ignoring input and appending output to ‘nohup.out’
运行后frps的运行日志将会在‘nohup.out’文件中输出
1.2.2. frp 客户端搭建
客户端搭建在本地的 Windows 上,因此下载安装包版本为 windows_amd64 的 frp_0.29.1_windows_amd64.zip 。
1.直接下载压缩安装包
https://github.com/fatedier/frp/releases/download/v0.29.1/frp_0.29.1_windows_amd64.zip
2.解压文件到自己的电脑中
3.修改配置文件frpc.ini
[common]
server_addr = 119.29.528.35
server_port = 7000
[web]
type = http
local_port = 8080
custom_domains = www.example.cn
server_addr = 你外网的服务器
Iplocal_port = 你本地spring boot运行端口
custom_domains 你外网服务器IP进行dns绑定的域名
4.启动frpc客户端(双击直接运行我好像运行不起来)使用CMD执行以下命令
//切换到解压文件目录
c:> cd frp_0.29.1_windows_amd64
//运行
c:> frpc.exe
2019/11/21 10:20:35 [I] [service.go:249] [01a75fadd6a3998d] login to server success, get run id [01a75fadd6a3998d], server udp port [0]
2019/11/21 10:20:35 [I] [proxy_manager.go:144] [01a75fadd6a3998d] proxy added: [web]
2019/11/21 10:20:35 [I] [control.go:164] [01a75fadd6a3998d] [web] start proxy success
:现在当我们在浏览器输入 http://
www.eample.com
:7200的时候,网络流量其实会经历以下几个步骤:
- 通过之前配的 DNS 域名解析会访问到我们的公网服务器
119.29.528.35
的 7200端口- 再经过 frp 穿透到我们的 windows电脑的 8080 端口
- 此时 8080 就是我们的应用程序端口
如果需要去掉7200端口可以自己配置nginx进行代理到本地的 7200 端口,需要的话自己百度
前往 https://connect.qq.com/
申请开发者
应用管理 -> 添加网站应用,等待审核通过即可
其他平台相类似;
思路:用户点击前端Vue第三方按钮,后端/render/{source}接口授权接收请求返回重定向的第三方登录地址,前端Vue接收到后可直接跳转到授权界面或者通过弹窗的方式打开第三方登录界面也行,登录成功后第三方返回回调地址到我们外网的域名,在经过我们的内网映射到本地到/callback/{source}接口,我们获取到token后进行数据库数据存储,并用Token去获取openid和其他信息,我们创建uuid产生Key用redis进行保存用户信息,并把key携带到地址中重定向到首页,首页解析地址是否包含我们携带的key有的话就可以到后台redis中请求到用户的信息了,返回前端后提示登录成功并跳转到用户中心界面。
me.zhyd.oauth
JustAuth
1.13.1
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-data-redis
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
com.alibaba
druid-spring-boot-starter
${druid.version}
server:
port: 8080
servlet:
context-path: /oauth
spring:
redis:
host: localhost
# 连接超时时间(记得添加单位,Duration)
timeout: 10000ms
# Redis默认情况下有16个分片,这里配置具体使用的分片
# database: 0
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1ms
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
cache:
# 一般来说是不用配置的,Spring Cache 会根据依赖的包自行装配
type: redis
datasource:
master:
url: jdbc:mysql://127.0.0.1:3306/oauth?characterEncoding=UTF-8&useUnicode=true&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
-- ----------------------------
-- Table structure for sys_user_social
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_social`;
CREATE TABLE `sys_user_social` (
`userId` varchar(255) NOT NULL COMMENT '用户Id',
`providerId` varchar(255) NOT NULL COMMENT '平台类型qq,wechat,weibo等',
`providerUserId` varchar(255) NOT NULL COMMENT '社交平台唯一ID',
`rank` int(11) NOT NULL DEFAULT '0' COMMENT '等级',
`displayName` varchar(255) DEFAULT NULL COMMENT '昵称',
`profileUrl` varchar(512) DEFAULT NULL COMMENT '配置文件地址',
`imageUrl` varchar(512) DEFAULT NULL COMMENT '头像地址',
`accessToken` varchar(512) NOT NULL COMMENT 'token',
`secret` varchar(512) DEFAULT NULL COMMENT '秘钥',
`refreshToken` varchar(512) DEFAULT NULL COMMENT '刷新token',
`expireTime` bigint(20) DEFAULT NULL COMMENT '过期时间',
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`userId`,`providerId`,`providerUserId`),
UNIQUE KEY `UserConnectionRank` (`userId`,`providerId`,`rank`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='社交登录表';
/**
* @author 淋汾
* @version 1.0
* @description 第三方授权登录
* @date 2019/11/19 9:28
*/
@RestController
@RequestMapping("/oauth")
public class LoginOauthController {
@Autowired
private RedisUtil redisUtil;
@Autowired
private ISysUserSocialService userSocialService;
@Autowired
private ISysDepartService sysDepartService;
@Autowired
private ISysUserService sysUserService;
@Autowired
private ISysBaseAPI sysBaseAPI;
//第三方登录回调地址
private String callBackBaseUrl = "http://www.example.cn:7200/oauth/oauth/callback";
//本地授权成功重定向地址
private String url = "http://127.0.0.1:8081";
/**
* 授权请求地址
*
* @param source
* @param response
* @return
* @throws IOException
*/
@RequestMapping("/render/{source}")
public Result renderAuth(@PathVariable("source") String source, HttpServletResponse response) throws IOException {
System.out.println("进入render:" + source);
AuthRequest authRequest = getAuthRequest(source);
String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
System.out.println(authorizeUrl);
Result result = new Result();
result.setSuccess(true);
result.setResult(authorizeUrl);
result.setMessage("正在进行跳转");
// response.sendRedirect(authorizeUrl);
return result;
}
/**
* oauth平台中配置的授权回调地址,以本项目为例,在创建github授权应用时的回调地址应为:http://127.0.0.1:8080/oauth/callback/github
*/
@RequestMapping("/callback/{source}")
public Object login(@PathVariable("source") String source, AuthCallback callback, HttpServletResponse response) throws IOException {
String uuid = "";
Result result = new Result();
System.out.println("进入callback:" + source + " callback params:" +
JSONObject.toJSONString(callback));
AuthRequest authRequest = getAuthRequest(source);
AuthResponse authResponse = authRequest.login(callback);
System.out.println(JSONObject.toJSONString(authResponse));
/* switch (source) {
case "weibo":
authResponse.getData().getToken().getOpenId();
break;
}
*/
//通过getProviderid和getProviderUserId查询用户
if (authResponse == null) {
result.error500("授权失败");
return result;
}
SysUserSocial sysUserSocial = userSocialService.lambdaQuery().eq(SysUserSocial::getProviderid, source)
.eq(SysUserSocial::getProvideruserid, authResponse.getData().getToken().getOpenId()).one();
if (sysUserSocial == null) {
AuthToken token = authResponse.getData().getToken();
sysUserSocial = new SysUserSocial();
sysUserSocial.setProviderid(source);
sysUserSocial.setProvideruserid(token.getOpenId());
sysUserSocial.setAccesstoken(token.getAccessToken());
sysUserSocial.setRefreshtoken(token.getRefreshToken());
sysUserSocial.setExpiretime(token.getExpireIn());
sysUserSocial.setDisplayname(authResponse.getData().getUsername());
sysUserSocial.setProfileurl(authResponse.getData().getBlog());
sysUserSocial.setImageurl(authResponse.getData().getAvatar());
sysUserSocial.setSecret(token.getOauthTokenSecret());
//TODO 社交账户未绑定用户数据库,进行绑定
uuid = UUID.randomUUID().toString();
redisUtil.set(uuid, sysUserSocial, 1000 * 60 * 10);
System.out.println("存数据的key"+uuid);
result.error500("社交账户未绑定用户");
response.sendRedirect(url + "/user/social-bind?key=" + uuid);
return result;
}
//通过用户ID超找用户,并进行登录
SysUser user = sysUserService.getById(sysUserSocial.getUserid());
if (user == null) {
result.error500("未找到用户,请绑定用户");
return result;
}
//用户登录信息
userInfo(user, result);
sysBaseAPI.addLog("用户名: " + user.getUsername() + ",第三方" + source + "登录成功!", CommonConstant.LOG_TYPE_1, null);
uuid = UUID.randomUUID().toString();
redisUtil.set(uuid, result.getResult(), 1000 * 60 * 10);
response.sendRedirect(url + "/user/login?key=" + uuid);
return result;
}
@PostMapping("/user_info")
public Result getSocialUserInfo(@RequestBody String key) {
Result result = new Result();
System.out.println("获取数据的key"+key);
if (key == null || key.equals("")) {
return result.error500("key不能为空");
}
key = JSON.parseObject(key).getString("key");
//从Redis读取数据
Object data = redisUtil.get(key);
//删除Redis中的数据
redisUtil.del(key);
if (data == null) {
return result.error500("数据请求失败");
}
result.setResult((JSONObject) data);
result.success("登录成功");
return result;
}
/**
* 社交登录绑定
* @param user 登录信息
* @return
*/
@PostMapping("/bind")
public Result register(@RequestBody SocialBindVO user) {
return sysUserService.doPostSignUp(user);
}
/**
* 取消授权登录
*
* @param source 平台
* @param token token
* @return
* @throws IOException
*/
@RequestMapping("/revoke/{source}/{token}")
public Object revokeAuth(@PathVariable("source") String source, @PathVariable("token") String token) throws IOException {
Result result = userSocialService.unBind(source, token);
AuthRequest authRequest = getAuthRequest(source);
try {
authRequest.revoke(AuthToken.builder().accessToken(token).build());
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 刷新Token
*
* @param source
* @param token
* @return
*/
@RequestMapping("/refresh/{source}")
public Object refreshAuth(@PathVariable("source") String source, String token) {
AuthRequest authRequest = getAuthRequest(source);
return authRequest.refresh(AuthToken.builder().refreshToken(token).build());
}
/**
* 用户信息
*
* @param sysUser
* @param result
* @return
*/
private Result userInfo(SysUser sysUser, Result result) {
String syspassword = sysUser.getPassword();
String username = sysUser.getUsername();
// 生成token
String token = JwtUtil.sign(username, syspassword);
// 设置token缓存有效时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
// 获取用户部门信息
JSONObject obj = new JSONObject();
List departs = sysDepartService.queryUserDeparts(sysUser.getId());
obj.put("departs", departs);
if (departs == null || departs.size() == 0) {
obj.put("multi_depart", 0);
} else if (departs.size() == 1) {
sysUserService.updateUserDepart(username, departs.get(0).getOrgCode());
obj.put("multi_depart", 1);
} else {
obj.put("multi_depart", 2);
}
obj.put("token", token);
obj.put("userInfo", sysUser);
result.setResult(obj);
result.success("登录成功");
return result;
}
/**
* 根据具体的授权来源,获取授权请求工具类
*
* @param source
* @return
*/
private AuthRequest getAuthRequest(String source) {
AuthRequest authRequest = null;
switch (source) {
case "dingtalk":
authRequest = new AuthDingTalkRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/dingtalk")
.build());
break;
case "baidu":
authRequest = new AuthBaiduRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/baidu")
.build());
break;
case "github":
authRequest = new AuthGithubRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/github")
.build());
break;
case "gitee":
authRequest = new AuthGiteeRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/gitee")
.build());
break;
case "weibo":
authRequest = new AuthWeiboRequest(AuthConfig.builder()
.clientId("微博的APP ID")
.clientSecret("微博的APP Key")
.redirectUri(callBackBaseUrl + "/weibo")
.build());
break;
case "coding":
authRequest = new AuthCodingRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/coding")
.build());
break;
case "tencentCloud":
authRequest = new AuthTencentCloudRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/tencentCloud")
.build());
break;
case "oschina":
authRequest = new AuthOschinaRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/oschina")
.build());
break;
case "alipay":
// 支付宝在创建回调地址时,不允许使用localhost或者127.0.0.1,所以这儿的回调地址使用的局域网内的ip
authRequest = new AuthAlipayRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.alipayPublicKey("")
.redirectUri(callBackBaseUrl + "/alipay")
.build());
break;
case "qq":
authRequest = new AuthQqRequest(AuthConfig.builder()
.clientId("填写QQ的APP ID")
.clientSecret("填写QQ的APP Key")
.redirectUri(callBackBaseUrl + "/qq")
.build());
break;
case "wechat":
authRequest = new AuthWeChatRequest(AuthConfig.builder()
.clientId("123")
.clientSecret("123")
.redirectUri(callBackBaseUrl + "/wechat")
.build());
break;
case "csdn":
authRequest = new AuthCsdnRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/csdn")
.build());
break;
case "taobao":
authRequest = new AuthTaobaoRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/taobao")
.build());
break;
case "google":
authRequest = new AuthGoogleRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/google")
.build());
break;
case "facebook":
authRequest = new AuthFacebookRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/facebook")
.build());
break;
case "douyin":
authRequest = new AuthDouyinRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/douyin")
.build());
break;
case "linkedin":
authRequest = new AuthLinkedinRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/linkedin")
.build());
break;
case "microsoft":
authRequest = new AuthMicrosoftRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/microsoft")
.build());
break;
case "mi":
authRequest = new AuthMiRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/mi")
.build());
break;
case "toutiao":
authRequest = new AuthToutiaoRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/toutiao")
.build());
break;
case "teambition":
authRequest = new AuthTeambitionRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/teambition")
.build());
break;
case "pinterest":
authRequest = new AuthPinterestRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/pinterest")
.build());
break;
case "renren":
authRequest = new AuthRenrenRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/teambition")
.build());
break;
case "stackoverflow":
authRequest = new AuthStackOverflowRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/login_success")
.stackOverflowKey("")
.build());
break;
case "huawei":
authRequest = new AuthHuaweiRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/huawei")
.build());
break;
case "wechatEnterprise":
authRequest = new AuthHuaweiRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/wechatEnterprise")
.agentId("")
.build());
break;
case "kujiale":
authRequest = new AuthKujialeRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/kujiale")
.build());
break;
case "gitlab":
authRequest = new AuthGitlabRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/gitlab")
.build());
break;
case "meituan":
authRequest = new AuthMeituanRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/meituan")
.build());
break;
case "eleme":
authRequest = new AuthElemeRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/eleme")
.build());
break;
case "twitter":
authRequest = new AuthTwitterRequest(AuthConfig.builder()
.clientId("")
.clientSecret("")
.redirectUri(callBackBaseUrl + "/twitter")
.build());
break;
default:
break;
}
if (null == authRequest) {
throw new AuthException("未获取到有效的Auth配置");
}
return authRequest;
}
}
第三方绑定账户
绑定账户
返回登录
注册账户
参考
1.Spring Boot 快速集成第三方登录功能:https://xkcoding.com/2019/05/22/spring-boot-login-with-oauth.html
2.JustAuth GitHub地址:https://github.com/justauth/JustAuth