feat(auth): 实现飞书OAuth认证及相关配置
- 新增飞书OAuth认证控制器,支持获取授权URL、登录和登录状态检查 - 实现飞书认证服务接口及实现类,完成授权码换取用户信息及用户创建或更新 - 配置Sa-Token拦截器和全局过滤器,实现登录鉴权和跨域支持 - 添加系统用户Mapper及对应XML,支持用户和角色查询及分页筛选 - 新增飞书开放平台配置类,支持应用ID、密钥和回调地址配置 - 添加开发环境配置文件,配置数据库、MinIO、MyBatis Plus、飞书和日志等相关参数 - 新增Maven项目依赖,包含Spring Boot、Sa-Token、MyBatis Plus、MinIO客户端、文档解析和飞书SDK等依赖
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package cn.yinlihupo.service.system;
|
||||
|
||||
import cn.yinlihupo.domain.entity.SysUser;
|
||||
|
||||
/**
|
||||
* 飞书认证服务接口
|
||||
*/
|
||||
public interface FeishuAuthService {
|
||||
|
||||
/**
|
||||
* 构建飞书OAuth授权URL
|
||||
*
|
||||
* @param state 状态参数(防CSRF攻击)
|
||||
* @return 授权URL
|
||||
*/
|
||||
String buildAuthUrl(String state);
|
||||
|
||||
/**
|
||||
* 通过授权码登录
|
||||
* 使用code换取accessToken,获取用户信息,完成登录
|
||||
*
|
||||
* @param code 飞书授权码
|
||||
* @return 登录后的用户信息
|
||||
*/
|
||||
SysUser loginByCode(String code);
|
||||
|
||||
/**
|
||||
* 根据手机号获取或创建用户
|
||||
* 如果手机号存在则更新用户信息,不存在则创建新用户
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @param realName 真实姓名
|
||||
* @param avatar 头像URL
|
||||
* @param email 邮箱
|
||||
* @param openId 飞书OpenID
|
||||
* @return 用户实体
|
||||
*/
|
||||
SysUser getOrCreateUserByPhone(String phone, String realName, String avatar, String email, String openId);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package cn.yinlihupo.service.system.impl;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.yinlihupo.common.config.FeishuConfig;
|
||||
import cn.yinlihupo.domain.entity.SysUser;
|
||||
import cn.yinlihupo.mapper.SysUserMapper;
|
||||
import cn.yinlihupo.service.system.FeishuAuthService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 飞书认证服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FeishuAuthServiceImpl implements FeishuAuthService {
|
||||
|
||||
private final FeishuConfig feishuConfig;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
/**
|
||||
* 飞书OAuth授权端点
|
||||
*/
|
||||
private static final String FEISHU_OAUTH_URL = "https://open.feishu.cn/open-apis/authen/v1/authorize";
|
||||
|
||||
/**
|
||||
* 获取访问令牌端点
|
||||
*/
|
||||
private static final String FEISHU_TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token";
|
||||
|
||||
/**
|
||||
* 获取用户信息端点
|
||||
*/
|
||||
private static final String FEISHU_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";
|
||||
|
||||
/**
|
||||
* 应用访问令牌端点(用于获取应用级token)
|
||||
*/
|
||||
private static final String FEISHU_APP_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal";
|
||||
|
||||
@Override
|
||||
public String buildAuthUrl(String state) {
|
||||
String encodedRedirectUri = URLEncoder.encode(feishuConfig.getRedirectUri(), StandardCharsets.UTF_8);
|
||||
return FEISHU_OAUTH_URL +
|
||||
"?app_id=" + feishuConfig.getAppId() +
|
||||
"&redirect_uri=" + encodedRedirectUri +
|
||||
"&state=" + (state != null ? state : "") +
|
||||
"&scope=contact:user.base contact:user.phone:readonly";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysUser loginByCode(String code) {
|
||||
log.info("处理飞书登录, code: {}", code);
|
||||
|
||||
// 1. 获取应用访问令牌
|
||||
String appAccessToken = getAppAccessToken();
|
||||
|
||||
// 2. 使用授权码获取用户访问令牌
|
||||
String userAccessToken = getUserAccessToken(code, appAccessToken);
|
||||
|
||||
// 3. 获取用户信息
|
||||
JSONObject userInfo = getUserInfo(userAccessToken);
|
||||
|
||||
// 4. 提取用户信息
|
||||
String phone = userInfo.getStr("mobile");
|
||||
String realName = userInfo.getStr("name");
|
||||
String avatar = userInfo.getStr("avatar_url");
|
||||
String email = userInfo.getStr("email");
|
||||
String openId = userInfo.getStr("open_id");
|
||||
|
||||
log.info("飞书用户信息: phone={}, name={}, openId={}", phone, realName, openId);
|
||||
|
||||
// 5. 根据手机号获取或创建用户
|
||||
return getOrCreateUserByPhone(phone, realName, avatar, email, openId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用访问令牌
|
||||
*/
|
||||
private String getAppAccessToken() {
|
||||
JSONObject requestBody = new JSONObject();
|
||||
requestBody.set("app_id", feishuConfig.getAppId());
|
||||
requestBody.set("app_secret", feishuConfig.getAppSecret());
|
||||
|
||||
try (HttpResponse response = HttpRequest.post(FEISHU_APP_TOKEN_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(requestBody.toString())
|
||||
.execute()) {
|
||||
|
||||
String body = response.body();
|
||||
log.debug("获取应用访问令牌响应: {}", body);
|
||||
|
||||
JSONObject jsonObject = JSONUtil.parseObj(body);
|
||||
if (jsonObject.getInt("code") != 0) {
|
||||
throw new RuntimeException("获取应用访问令牌失败: " + jsonObject.getStr("msg"));
|
||||
}
|
||||
|
||||
return jsonObject.getJSONObject("app_access_token").getStr("app_access_token");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用授权码获取用户访问令牌
|
||||
*/
|
||||
private String getUserAccessToken(String code, String appAccessToken) {
|
||||
JSONObject requestBody = new JSONObject();
|
||||
requestBody.set("grant_type", "authorization_code");
|
||||
requestBody.set("code", code);
|
||||
|
||||
try (HttpResponse response = HttpRequest.post(FEISHU_TOKEN_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer " + appAccessToken)
|
||||
.body(requestBody.toString())
|
||||
.execute()) {
|
||||
|
||||
String body = response.body();
|
||||
log.debug("获取用户访问令牌响应: {}", body);
|
||||
|
||||
JSONObject jsonObject = JSONUtil.parseObj(body);
|
||||
if (jsonObject.getInt("code") != 0) {
|
||||
throw new RuntimeException("获取用户访问令牌失败: " + jsonObject.getStr("msg"));
|
||||
}
|
||||
|
||||
return jsonObject.getJSONObject("data").getStr("access_token");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
private JSONObject getUserInfo(String userAccessToken) {
|
||||
try (HttpResponse response = HttpRequest.get(FEISHU_USER_INFO_URL)
|
||||
.header("Authorization", "Bearer " + userAccessToken)
|
||||
.execute()) {
|
||||
|
||||
String body = response.body();
|
||||
log.debug("获取用户信息响应: {}", body);
|
||||
|
||||
JSONObject jsonObject = JSONUtil.parseObj(body);
|
||||
if (jsonObject.getInt("code") != 0) {
|
||||
throw new RuntimeException("获取用户信息失败: " + jsonObject.getStr("msg"));
|
||||
}
|
||||
|
||||
return jsonObject.getJSONObject("data");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysUser getOrCreateUserByPhone(String phone, String realName, String avatar, String email, String openId) {
|
||||
// 根据手机号查询用户
|
||||
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SysUser::getPhone, phone);
|
||||
SysUser user = sysUserMapper.selectOne(queryWrapper);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (user != null) {
|
||||
// 用户已存在,更新信息
|
||||
log.info("用户已存在,更新用户信息, phone: {}", phone);
|
||||
user.setRealName(realName);
|
||||
user.setAvatar(avatar);
|
||||
user.setEmail(email);
|
||||
user.setNickname(realName);
|
||||
user.setLastLoginTime(now);
|
||||
user.setUpdateTime(now);
|
||||
// 使用openId作为用户名(如果没有设置过)
|
||||
if (user.getUsername() == null || user.getUsername().isEmpty()) {
|
||||
user.setUsername(openId);
|
||||
}
|
||||
sysUserMapper.updateById(user);
|
||||
} else {
|
||||
// 用户不存在,创建新用户
|
||||
log.info("用户不存在,创建新用户, phone: {}", phone);
|
||||
user = new SysUser();
|
||||
user.setUsername(openId);
|
||||
user.setRealName(realName);
|
||||
user.setNickname(realName);
|
||||
user.setAvatar(avatar);
|
||||
user.setPhone(phone);
|
||||
user.setEmail(email);
|
||||
user.setStatus(1); // 正常状态
|
||||
user.setLastLoginTime(now);
|
||||
user.setCreateTime(now);
|
||||
user.setUpdateTime(now);
|
||||
user.setDeleted(0);
|
||||
// 飞书登录用户不需要密码,设置一个随机密码
|
||||
user.setPassword("FEISHU_OAUTH_USER");
|
||||
sysUserMapper.insert(user);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user