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:
2026-03-27 16:36:55 +08:00
parent 0b5af874ca
commit b9aacf0f10
9 changed files with 401 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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;
}
}