Skip to main content

LangChat Pro 社交登录实施方案

一、方案概述

1.1 背景

LangChat Pro 需要支持多种社交登录方式,以满足不同用户群体的登录需求。本方案设计并实现以下社交登录功能:
  • 企业微信登录:适用于企业内部用户扫码登录
  • 微信开放平台登录:适用于 C 端用户扫码登录
  • 钉钉登录:适用于企业用户扫码登录
  • 邮箱登录:使用邮箱验证码登录
  • 手机验证码登录:使用阿里云短信服务

1.2 设计目标

  1. 统一抽象:定义统一的社交登录接口和策略模式
  2. 易于扩展:新增登录方式只需实现接口,无需修改核心逻辑
  3. 用户绑定:支持社交账号与本地账号的绑定
  4. 自动注册:新用户自动创建账号
  5. 安全可靠:所有登录方式均使用安全的认证流程

1.3 技术选型

登录方式实现方式版本
企业微信HTTP API + Hutool HttpUtil-
微信开放平台HTTP API + Hutool HttpUtil-
钉钉HTTP API + Hutool HttpUtil-
邮件javax.mail1.6.2
短信阿里云短信服务 SDK最新

二、架构设计

2.1 整体架构

┌─────────────────────────────────────────────────────────────────────┐
│                         社交登录架构                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────────────┐     ┌─────────────────┐     ┌────────────────┐│
│  │   前端页面       │     │  SocialEndpoint │     │  后端服务      ││
│  │  - 登录页组件    │     │  - 统一入口     │     │  - 策略工厂    ││
│  │  - 第三方按钮    │────▶│  - 参数校验     │────▶│  - 策略执行    ││
│  │  - 回调处理      │     │  - 结果处理     │     │  - 用户处理    ││
│  └─────────────────┘     └─────────────────┘     └────────────────┘│
│                              │                              │       │
│                              ▼                              ▼       │
│                     ┌─────────────────┐     ┌────────────────────┐ │
│                     │  SocialLogin    │     │  SocialLoginUser   │ │
│                     │  Strategy       │     │  Handler           │ │
│                     │  Factory        │     │                    │ │
│                     │                 │     │  - 用户创建        │ │
│                     │  策略工厂       │     │  - 账号绑定        │ │
│                     │  负责创建具体   │     │  - 会话管理        │ │
│                     │  登录策略       │     └────────────────────┘ │
│                     └─────────────────┘                           │
│                              │                                      │
│         ┌────────────────────┼────────────────────┐               │
│         │                    │                    │                │
│         ▼                    ▼                    ▼                │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐         │
│  │   WeChat    │     │  DingTalk   │     │EnterpriseWX │         │
│  │   Strategy  │     │  Strategy   │     │  Strategy   │         │
│  └─────────────┘     └─────────────┘     └─────────────┘         │
│         │                    │                    │                │
│         ▼                    ▼                    ▼                │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐         │
│  │EmailStrategy│     │SMS Strategy │     │  基础抽象    │         │
│  └─────────────┘     └─────────────┘     │  Strategy   │         │
│                                          └─────────────┘         │
└─────────────────────────────────────────────────────────────────────┘

2.2 核心组件

2.2.1 SocialLoginStrategy(社交登录策略接口)

所有登录方式需要实现的统一接口:
public interface SocialLoginStrategy {
    // 获取登录类型
    SocialType getType();

    // 是否启用
    boolean isEnabled();

    // 获取授权URL
    String getAuthorizeUrl(String redirectUri, String state);

    // 通过授权码获取用户信息
    SocialUserInfo getUserInfo(String code, String state);

    // 处理回调
    SocialLoginResult handleCallback(String code, String state);

    // 发送验证码(仅邮箱/手机使用)
    boolean sendCode(String target);
}

2.2.2 SocialLoginFactory(策略工厂)

根据登录类型创建对应的策略实例:
@Component
public class SocialLoginFactory {
    private final Map<SocialType, SocialLoginStrategy> strategies;

    public SocialLoginStrategy getStrategy(SocialType type) {
        return strategies.get(type);
    }
}

2.2.3 SocialLoginHandler(用户处理)

处理用户创建、绑定和会话管理:
@Component
public class SocialLoginHandler {
    // 社交登录
    SocialLoginResult login(SocialType type, String code, String state);

    // 邮箱/手机验证码登录
    SocialLoginResult loginWithUserInfo(SocialUserInfo socialUserInfo, SocialType type);

    // 绑定社交账号
    void bindAccount(String userId, SocialType type, String code, String state);

    // 解绑社交账号
    void unbindAccount(String userId, SocialType type);

    // 获取用户已绑定账号
    List<AigcUserSocial> getUserBindings(String userId);
}

2.3 数据模型设计

2.3.1 社交账号绑定表

CREATE TABLE `aigc_user_social` (
  `id` VARCHAR(64) PRIMARY KEY COMMENT '主键',
  `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
  `social_type` VARCHAR(20) NOT NULL COMMENT '社交类型: WECHAT/DINGTALK/ENTERPRISE_WECHAT/EMAIL/PHONE',
  `open_id` VARCHAR(128) COMMENT '第三方开放ID',
  `union_id` VARCHAR(128) COMMENT '微信unionId',
  `nickname` VARCHAR(64) COMMENT '第三方昵称',
  `avatar` VARCHAR(512) COMMENT '第三方头像',
  `access_token` VARCHAR(512) COMMENT '访问令牌',
  `refresh_token` VARCHAR(512) COMMENT '刷新令牌',
  `expires_in` BIGINT COMMENT '令牌过期时间',
  `status` TINYINT DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
  `create_time` BIGINT COMMENT '创建时间',
  `update_time` BIGINT COMMENT '更新时间',
  INDEX `idx_user_id` (`user_id`),
  INDEX `idx_social_type_open_id` (`social_type`, `open_id`)
) COMMENT '用户社交账号绑定表';

三、配置说明

3.1 配置文件结构

application.yml 中配置:
langchat:
  auth:
    # 社交登录配置
    social:
      # 是否启用社交登录
      enabled: true
      # 社交登录回调基础URL(用于扫码登录后重定向到前端页面)
      callback-url: ${SOCIAL_CALLBACK_URL}

      # 微信开放平台配置
      wechat:
        enabled: false
        app-id: ${WECHAT_APP_ID}
        app-secret: ${WECHAT_APP_SECRET}
        redirect-uri: ${WECHAT_REDIRECT_URI}

      # 企业微信配置
      enterprise-wechat:
        enabled: false
        corp-id: ${ENTERPRISE_WECHAT_CORP_ID}
        agent-id: ${ENTERPRISE_WECHAT_AGENT_ID}
        secret: ${ENTERPRISE_WECHAT_SECRET}
        redirect-uri: ${ENTERPRISE_WECHAT_REDIRECT_URI}

      # 钉钉配置
      dingtalk:
        enabled: false
        app-key: ${DINGTALK_APP_KEY}
        app-secret: ${DINGTALK_APP_SECRET}
        redirect-uri: ${DINGTALK_REDIRECT_URI}

      # 邮箱配置
      email:
        enabled: true
        host: smtp.example.com
        port: 465
        username: ${EMAIL_USERNAME}
        pass: ${EMAIL_PASSWORD}
        from: LangChat <${EMAIL_FROM}>
        ssl: true

      # 短信配置
      sms:
        enabled: false
        access-key-id: ${ALIYUN_SMS_ACCESS_KEY_ID}
        access-key-secret: ${ALIYUN_SMS_ACCESS_KEY_SECRET}
        sign-name: ${ALIYUN_SMS_SIGN_NAME}
        template-code: ${ALIYUN_SMS_TEMPLATE_CODE}

    # 默认菜单配置(当用户无权限时返回默认菜单)
    defaultMenu:
      enabled: true
      homeMenuId: acd27abc0fb323ef1a4f4c3581795690

3.2 配置属性类

@Data
@ConfigurationProperties(prefix = "langchat.auth.social")
public class SocialLoginProperties {
    private boolean enabled = true;
    private String callbackUrl;
    private WeChatConfig wechat = new WeChatConfig();
    private EnterpriseWeChatConfig enterpriseWechat = new EnterpriseWeChatConfig();
    private DingTalkConfig dingtalk = new DingTalkConfig();
    private EmailConfig email = new EmailConfig();
    private SmsConfig sms = new SmsConfig();

    @Data
    public static class WeChatConfig {
        private boolean enabled;
        private String appId;
        private String appSecret;
        private String redirectUri;
    }

    // ... 其他配置类
}

四、接口设计

4.1 API 端点

端点方法说明
/auth/social/typesGET获取可用的登录类型
/auth/social/authorize/{type}GET获取授权URL
/auth/social/callback/{type}GET社交平台回调(重定向到前端)
/auth/social/loginPOST社交登录
/auth/social/code/sendPOST发送验证码
/auth/social/code/verifyPOST验证验证码并登录
/auth/social/bindPOST绑定社交账号
/auth/social/unbind/{type}DELETE解绑社交账号
/auth/social/bindingsGET获取已绑定账号

4.2 登录类型枚举

@Getter
@JsonDeserialize(using = SocialTypeDeserializer.class)
public enum SocialType {
    WECHAT("wechat", "微信开放平台"),
    ENTERPRISE_WECHAT("enterprise_wechat", "企业微信"),
    DINGTALK("dingtalk", "钉钉"),
    EMAIL("email", "邮箱"),
    PHONE("phone", "手机");

    public static SocialType fromCode(String code) {
        for (SocialType type : values()) {
            if (type.getCode().equalsIgnoreCase(code)) {
                return type;
            }
        }
        throw new IllegalArgumentException("Unknown social type: " + code);
    }
}

4.3 自定义反序列化器

支持从 code(如 "enterprise_wechat")或枚举名(如 "ENTERPRISE_WECHAT")反序列化:
public class SocialTypeDeserializer extends JsonDeserializer<SocialType> {
    @Override
    public SocialType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = p.getValueAsString();
        // 优先使用 code 匹配
        for (SocialType type : SocialType.values()) {
            if (type.getCode().equalsIgnoreCase(value)) {
                return type;
            }
        }
        // 尝试使用枚举名匹配
        return SocialType.valueOf(value.toUpperCase());
    }
}

五、第三方平台配置

5.1 企业微信扫码登录

官方文档https://developer.work.weixin.qq.com/document/path/91039 授权 URL 格式
https://login.work.weixin.qq.com/wwlogin/sso/login?login_type=CorpApp&appid=CORP_ID&agentid=AGENT_ID&redirect_uri=ENCODED_URL&state=ENCODED_STATE
配置要求
  1. CorpID:企业 ID
  2. AgentID:自建应用 ID
  3. Secret:应用密钥
  4. OAuth可信域名:前端页面域名
  5. IP白名单:服务器出口 IP

5.2 微信开放平台扫码登录

授权 URL 格式
https://open.weixin.qq.com/connect/qrconnect?appid=APP_ID&redirect_uri=ENCODED_URL&response_type=code&scope=snsapi_login&state=STATE#wechat_redirect

5.3 钉钉扫码登录

授权 URL 格式
https://oapi.dingtalk.com/connect/qrconnect?appkey=APP_KEY&response_type=code&scope=snsapi_login&redirect_uri=ENCODED_URL&state=STATE

5.4 回调配置说明

回调配置需要特别注意:
  1. redirect_uri:企业微信/微信/钉钉后台配置的回调地址域名必须一致
  2. callback-url:配置为前端可访问的地址,用于扫码后重定向
┌─────────────────────────────────────────────────────────────────┐
│                        回调流程                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  用户扫码                                                        │
│     ↓                                                           │
│  企业微信扫码页                                                   │
│     ↓                                                           │
│  扫码成功,重定向到后端 /auth/social/callback/enterprise_wechat   │
│     ↓                                                           │
│  后端重定向到前端页面 + code + state                              │
│  https://your-domain.com/auth/social-login?type=xxx&code=xxx     │
│     ↓                                                           │
│  前端检测到 URL 参数,调用登录接口                                │
│     ↓                                                           │
│  登录成功,跳转首页                                              │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

六、安全设计

6.1 CSRF 防护

使用 state 参数防止 CSRF 攻击:
// 生成随机 state
String state = UUID.randomUUID().toString().replace("-", "");
// 存储到 Redis,有效期 10 分钟
redisTemplate.opsForValue().set(
    CacheConst.SOCIAL_STATE_PREFIX + state,
    redirect,
    10,
    TimeUnit.MINUTES
);

6.2 授权码安全

  • 授权码一次性使用
  • 授权码有效期 5 分钟
  • 使用后立即失效

6.3 默认菜单保护

当用户没有任何菜单权限时,系统默认返回探索首页:
langchat:
  auth:
    defaultMenu:
      enabled: true
      homeMenuId: acd27abc0fb323ef1a4f4c3581795690

七、前端集成

7.1 社交登录页面

前端通过 /auth/social-login 页面处理社交登录:
  1. 有 type 参数:获取授权 URL 并跳转到第三方
  2. 有 code 和 state 参数:调用登录接口
  3. 无参数:跳转到登录页

7.2 路由守卫

在路由守卫中添加社交登录回调处理:
router.beforeEach(async (to, from) => {
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  const state = urlParams.get('state');

  // 社交登录回调
  if (code && state && to.fullPath.startsWith('/auth/social-login')) {
    return true; // 允许访问
  }
});

7.3 登录按钮

在登录页面添加社交登录按钮:
<template>
  <div class="social-login">
    <button @click="loginWithWechat">微信登录</button>
    <button @click="loginWithEnterpriseWechat">企业微信登录</button>
    <button @click="loginWithDingtalk">钉钉登录</button>
  </div>
</template>

八、后续优化

  1. 多平台绑定优化:支持用户绑定多个社交账号
  2. 小程序登录:支持微信小程序登录
  3. 海外平台:支持 Google、Facebook 登录
  4. 登录统计:登录数据分析报表
  5. SSO 集成:与企业内部 SSO 系统集成

九、参考文档