feat: 钉钉应用内免登录功能

This commit is contained in:
Your Name 2026-01-07 10:14:04 +08:00
parent d695d81ed4
commit 5526c7ad4e
4 changed files with 169 additions and 3 deletions

View File

@ -28,6 +28,7 @@ public class WebConfig implements WebMvcConfigurer {
"/api/system-config/key/site.name", // 排除公开的网站设置API(按key获取)
"/api/system-config/key/site.logo", // 排除公开的网站设置API(按key获取)
"/api/system-config/map/group/site", // 排除公开的网站设置API(Map格式)
"/api/dingtalk/config/public", // 排除钉钉公开配置API(免登需要)
"/doc.html", // 排除Swagger文档
"/swagger-ui/**",
"/v3/api-docs/**",

View File

@ -32,6 +32,20 @@ public class DingTalkController {
private final ObjectMapper objectMapper;
private final UserMapper userMapper;
@Operation(summary = "获取钉钉公开配置(用于免登)")
@GetMapping("/config/public")
public Result<Map<String, String>> getPublicConfig() {
DingTalkConfig config = dingTalkService.getConfig();
Map<String, String> publicConfig = new HashMap<>();
if (config != null && config.getEnabled() == 1) {
publicConfig.put("corpId", config.getCorpId());
publicConfig.put("agentId", config.getAgentId());
}
return Result.success(publicConfig);
}
@Operation(summary = "获取钉钉配置")
@GetMapping("/config")
public Result<DingTalkConfig> getConfig() {

View File

@ -152,6 +152,52 @@ export const useAuthStore = defineStore('auth', () => {
}
}
/**
* 钉钉免登录(使用授权码)
*/
const loginWithDingTalk = async (authCode: string) => {
try {
const response = await authApi.login({
authCode,
loginType: 'dingtalk'
})
const loginData = response.data
// 保存 token
const accessToken = loginData.token || loginData.accessToken
token.value = accessToken
localStorage.setItem('token', accessToken)
// 登录成功后获取完整用户信息
try {
await fetchUserInfo()
} catch (error) {
console.error('获取用户信息失败:', error)
// 使用基本信息
const userInfoData = {
id: loginData.userId,
username: loginData.username,
realName: loginData.realName,
avatar: loginData.avatar,
email: '',
phone: '',
deptId: null,
deptName: '',
roles: loginData.roles || [],
permissions: []
}
userInfo.value = userInfoData
localStorage.setItem('userInfo', JSON.stringify(userInfoData))
}
return { success: true }
} catch (error: any) {
console.error('钉钉免登录失败:', error)
return { success: false, message: error.message || '钉钉免登录失败' }
}
}
return {
token,
userInfo,
@ -159,6 +205,7 @@ export const useAuthStore = defineStore('auth', () => {
userName,
userRoles,
login,
loginWithDingTalk,
logout,
fetchUserInfo,
refreshToken

View File

@ -258,6 +258,13 @@ const registerForm = reactive({
// 钉钉登录
const dingTalkLoginEnabled = ref(false)
const dingTalkLoginLoading = ref(false)
const dingTalkAutoLoginInProgress = ref(false)
// 检测是否在钉钉环境内
const isDingTalkEnv = () => {
const ua = navigator.userAgent.toLowerCase()
return ua.includes('dingtalk')
}
// 部门列表
const departments = ref<any[]>([])
@ -411,7 +418,7 @@ const checkRegisterEnabled = async () => {
}
}
// 处理钉钉登录
// 处理钉钉登录(OAuth方式,用于非钉钉环境)
const handleDingTalkLogin = async () => {
dingTalkLoginLoading.value = true
try {
@ -438,7 +445,98 @@ const handleDingTalkLogin = async () => {
}
}
onMounted(() => {
// 钉钉应用内免登录
const handleDingTalkAutoLogin = async () => {
if (dingTalkAutoLoginInProgress.value) return
dingTalkAutoLoginInProgress.value = true
console.log('检测到钉钉环境,尝试免登录...')
try {
// 1. 先获取钉钉配置(corpId)
const configResponse = await request.get<Result<{ corpId: string; agentId: string }>>('/dingtalk/config/public')
if (configResponse.code !== 200 || !configResponse.data?.corpId) {
console.log('钉钉配置未完成,跳过免登录')
return
}
const corpId = configResponse.data.corpId
console.log('获取到corpId:', corpId)
// 2. 动态加载钉钉JSAPI
if (!(window as any).dd) {
await loadDingTalkJSAPI()
}
const dd = (window as any).dd
if (!dd) {
console.error('钉钉JSAPI加载失败')
return
}
// 3. 使用钉钉JSAPI获取免登授权码
dd.ready(() => {
dd.runtime.permission.requestAuthCode({
corpId: corpId,
onSuccess: async (result: { code: string }) => {
console.log('获取到免登授权码:', result.code)
try {
// 4. 使用授权码登录
const loginResult = await authStore.loginWithDingTalk(result.code)
if (loginResult.success) {
message.success('钉钉免登录成功')
router.push('/')
} else {
console.log('钉钉免登录失败:', loginResult.message)
// 免登失败不显示错误,让用户手动登录
}
} catch (error) {
console.error('钉钉免登录请求失败:', error)
} finally {
dingTalkAutoLoginInProgress.value = false
}
},
onFail: (err: any) => {
console.log('获取免登授权码失败:', err)
dingTalkAutoLoginInProgress.value = false
}
})
})
dd.error((err: any) => {
console.error('钉钉JSAPI错误:', err)
dingTalkAutoLoginInProgress.value = false
})
} catch (error) {
console.error('钉钉免登录异常:', error)
dingTalkAutoLoginInProgress.value = false
}
}
// 动态加载钉钉JSAPI
const loadDingTalkJSAPI = (): Promise<void> => {
return new Promise((resolve, reject) => {
if ((window as any).dd) {
resolve()
return
}
const script = document.createElement('script')
script.src = 'https://g.alicdn.com/dingding/dingtalk-jsapi/3.0.12/dingtalk.open.js'
script.onload = () => {
console.log('钉钉JSAPI加载成功')
resolve()
}
script.onerror = () => {
console.error('钉钉JSAPI加载失败')
reject(new Error('Failed to load DingTalk JSAPI'))
}
document.head.appendChild(script)
})
}
onMounted(async () => {
// 加载网站设置
siteSettingsStore.loadSiteSettings()
@ -446,11 +544,17 @@ onMounted(() => {
loadDepartments()
// 检查钉钉登录是否启用
checkDingTalkLogin()
await checkDingTalkLogin()
// 检查注册功能是否启用
checkRegisterEnabled()
// 如果在钉钉环境内且启用了钉钉登录,尝试自动免登
if (isDingTalkEnv() && dingTalkLoginEnabled.value) {
console.log('检测到钉钉环境,启动免登录流程')
handleDingTalkAutoLogin()
}
// 监听网站设置更新事件(在加载后监听,确保能接收到事件)
window.addEventListener('siteSettingsUpdated', handleSiteSettingsUpdate)