Export by Blossom
《/技术分享/智慧互安认证平台-账号映射-若依框架接入教程》
目录

智慧互安认证平台-账号映射-若依框架接入教程

对接说明

1、注册【智慧互安认证系统】 2、应用管理->创建应用(认证类型默认不填)->新增映射账号->下载密钥文件

若依框架改造

后端

1、新增统一认证工具类

package com.detn.common.utils.authLogin;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.detn.common.exception.TClientException;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 统一认证信息
 */
public class AuthLoginUtils {

    /**
     * 接口请求地址(不可变)
     */
    private static final String BASEURL = "http://kf.ct35.top";

    /**
     * 统一认证页面地址(不可变)
     */
    private static final String AUTH_LOGIN_URL = "http://kf.ct35.top/authLogin?token=";

    /**
     * APPID
     */
    private static final String APPID = "1774927884659";

    /**
     * 应用密钥
     */
    private static final String SECRET = "310dbb58acb04596a5b67ce550163e07";

    /**
     * 公钥(用于参数加密)
     */
    private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCNQgfTDrq7sLxlNSZjGSk7OGGQZCd66qOLn7UlXQjiMoWdFl2A6xk+nk25g/XvgM/zJIP/MejAUJdlgQl8qrOGffEkMJFdJaBuIfWbfNupmTIphlmXDq/Fkeji9qU5RNJfyOJNC2kTXnAoHIfMOyMXm32bNHwqA+Mr+ug8VapVlQIDAQAB";

    /**
     * 私钥(用于参数解密)
     */
    private static final String PRIVATE_KEY = "MIICeQIBADANBgkqhkiG9w0BAQEFAASCAmMwggJfAgEAAoGBAM8BPUXg6yCuqqn2ENpB9HOemd9aMENiqUW6uMZhlNPSmySzU1rqN+p/leE2MjtczW4ynbwWEZ6tKu2bE8t6JF9qGKI3MvUAozNZ3zZF9DNw7U5+gnWpD1NVF79fOupdwluavHp5HLLszxlgKMD3CwI612M7txfWYJZjRmwMMO7tAgMBAAECgYEAjTe2OkavgRNHgD1Kz+qwvnOTW2jmhgH3ooc4aLXHVx5VEpt3oQ0p6kLx17smV5L+3k+PhfG0QYC3SXNC9Mw1pNuO8O7qlK54arK1INzVR8LExQF0afjC/5ta3B1GakoobI5uYMo3WPx9eq/+yPo/ufOY51+dGIFZ9jLcuxuiseECQQDwxmINB6JgMI+8KLPFoyMmO8Z3vWr/LYSCEP2omseENhQf/ywOrpljT3rXo8KOoPzyWCOax6dPrKAlIcECKDbVAkEA3BgxWMxTjdaIUeWl4cPkvASNflAfKVAARXTHCadtfss4Wjd2NRwL1Ic71TuNpyYjhRhjaxvoBgQcMHzBe5GTuQJBAKDOPgopwR6xEC5AmOA0dnNAlOq/zUgkJE2h/2euB/abD5HXIJulrLgRCyAeNQCBMK7CgvUKxwmFZ2elRXylUwkCQQCz8vfNwWmDK2dThm7FEtQeQZ6qKrn/nhBKuXrO0tw8cc1GPdhg/ehSQKtT4RglFJKwDEuk1DE1UgD2R9otvM3JAkEA1EWyGh/z6PjEOZ7WADaPsyn3E7vbul1IhwbFI04w9YvJDSUWtUWP/TBvEstq3YxfBOnmGx2iyBF4Vj2nodJiyw==";

    /**
     * 获取token令牌地址
     */
    private static final String GET_USER_TOKEN_URL = "/prod-api/open/api/getUserToken";

    /**
     * 获取统一认证地址
     * @return
     */
    public static String getAuthLoginUrl(){
        try{
            JSONObject p = new JSONObject();
            p.put("appId", APPID);
            p.put("secret", SECRET);
            String resultData = doPostJson(BASEURL+GET_USER_TOKEN_URL,JSONObject.toJSONString(p),null);
            JSONObject resultDataJSON = JSON.parseObject(resultData);
            if (resultDataJSON.getInteger("code") == 200){
                return AUTH_LOGIN_URL+resultDataJSON.getJSONObject("data").getString("token");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 公钥加密方法
     *
     * @param text          将要加密的信息
     * @return Base64编码的字符串
     * @throws Exception
     */
    public static String encryptByPublicKey(String text) {
        try {
            // 返回按照 X.509 标准进行编码的密钥的字节
            // x509EncodedKeySpec2 是一种规范、规格
            X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(PUBLIC_KEY));
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            // 让密钥工厂按照指定的 规范 生成公钥
            PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
            Cipher cipher = Cipher.getInstance("RSA");
            // 对加密初始化,使用加密模式,公钥加密
            // Cipher.ENCRYPT_MODE 可以用 1 代替
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);

            byte[] result = cipher.doFinal(text.getBytes());
            // 返回 Base64编码的字符串
            return Base64.encodeBase64String(result);
        } catch (Exception e) {}
        return null;
    }

    /**
     * 私钥解密方法
     *
     * @param text  待解密的信息
     * @return 字符串
     * @throws Exception
     */
    public static String decryptByPrivateKey(String text) {
        try {
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(PRIVATE_KEY));
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] result = cipher.doFinal(Base64.decodeBase64(text));
            return new String(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * post请求
     * @param requestUrl 请求URL
     * @param paramsJson 请求参数
     * @param headers    头部参数
     * @return
     */
    private static String doPostJson(String requestUrl, String paramsJson, Map<String, String> headers) {
        if (org.apache.commons.lang3.StringUtils.isBlank(requestUrl)) {
            throw new IllegalArgumentException("请求URL不能为空");
        }
        HttpURLConnection conn = null;
        String var9;
        try {
            conn = getHttpConnection(requestUrl, headers);
            OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8);
            try {
                if (org.apache.commons.lang3.StringUtils.isNotBlank(paramsJson)) {
                    out.write(paramsJson);
                    out.flush();
                    System.out.println("POST请求发送成功 - URL: " + requestUrl + ", 参数: " + paramsJson);
                }
            } catch (Throwable var19) {
                try {
                    out.close();
                } catch (Throwable var18) {
                    var19.addSuppressed(var18);
                }

                throw var19;
            }
            out.close();
            int var23 = conn.getResponseCode();
            BufferedReader in = new BufferedReader(new InputStreamReader(var23 >= 200 && var23 <= 299 ? conn.getInputStream() : conn.getErrorStream(), StandardCharsets.UTF_8));
            try {
                StringBuilder responseBuilder = new StringBuilder();
                String line;
                while((line = in.readLine()) != null) {
                    responseBuilder.append(line);
                }
                String response = responseBuilder.toString();
                System.out.println("POST请求响应 - URL: " + requestUrl + ", 响应码: " + var23 + ", 结果: " + response);
                if (var23 < 200 || var23 > 299) {
                    throw new IOException(String.format("HTTP请求失败,响应码: %d,错误信息: %s", var23, response));
                }
                var9 = response;
            } catch (Throwable var20) {
                try {
                    in.close();
                } catch (Throwable var17) {
                    var20.addSuppressed(var17);
                }
                throw var20;
            }
            in.close();
        } catch (IOException e) {
            System.out.println("POST请求失败 - URL: " + requestUrl + ", 参数:  " + paramsJson + ", 异常: " + e.getMessage());
            e.printStackTrace();
            throw new RuntimeException("HTTP POST请求执行失败", e);
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
        return var9;
    }

    /**
     * 封装请求参数
     * @param requestUrl
     * @param headers
     * @return
     * @throws IOException
     */
    private static HttpURLConnection getHttpConnection(String requestUrl, Map<String, String> headers) throws IOException {
        URL url = new URL(requestUrl);
        HttpURLConnection conn = (HttpURLConnection)url.openConnection();
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setDoInput(true);
        conn.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(30L));
        conn.setReadTimeout((int) TimeUnit.SECONDS.toMillis(10L));
        conn.setUseCaches(false);
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setRequestProperty("Accept", "application/json");
        if (headers != null && !headers.isEmpty()) {
            headers.forEach((key, value) -> {
                if (org.apache.commons.lang3.StringUtils.isNotBlank(key) && org.apache.commons.lang3.StringUtils.isNotBlank(value)) {
                    conn.setRequestProperty(key, value);
                }
            });
        }
        return conn;
    }

    /**
     * 判断链接有效期
     * @param expireTime            登录链接到期时间
     * @return                      true 表示未过期,false 表示已过期
     * @throws TClientException
     */
    public static boolean checkUrlExpirationTime(String expireTime) throws TClientException {
        // 判断是否为空
        if (expireTime == null || expireTime.isEmpty()) {
            return false;
        }
        // 判断时间是否过期
        try {
            java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            java.time.LocalDateTime expireDateTime = java.time.LocalDateTime.parse(expireTime, formatter);
            if (java.time.LocalDateTime.now().isAfter(expireDateTime)) {
                return false;
            }
        } catch (java.time.format.DateTimeParseException e) {
            return false;
        }
        return true;
    }

}
java

2、新增对外接口控制层

package com.detn.web.controller.common;

import com.alibaba.fastjson2.JSONObject;
import com.detn.common.constant.CacheConstants;
import com.detn.common.constant.Constants;
import com.detn.common.core.domain.AjaxResult;
import com.detn.common.core.domain.entity.SysUser;
import com.detn.common.core.domain.model.LoginUser;
import com.detn.common.core.redis.RedisCache;
import com.detn.common.exception.TClientException;
import com.detn.common.utils.Result;
import com.detn.common.utils.SecurityUtils;
import com.detn.common.utils.authLogin.AuthLoginUtils;
import com.detn.framework.web.service.SysLoginService;
import com.detn.system.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

@RestController
@RequestMapping("/open/api/zhHaAuthLogin")
public class ZhHaAuthLoginController {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private SysLoginService sysLoginService;

    @Autowired
    private RedisCache redisCache;

    /**
     * 获取认证登录地址
     * @return
     */
    @PostMapping("/getAuthLoginUrl")
    public Result getAuthLoginUrl(){
        Result result = new Result();
        String authLoginUrl = AuthLoginUtils.getAuthLoginUrl();
        result.setData(authLoginUrl);
        return result;
    }

    /**
     * 统一认证登录地址
     * @param data
     * @throws TClientException
     */
    @PostMapping("/authLogin")
    public AjaxResult authLogin(@RequestBody String data) throws TClientException {
        data = JSONObject.parseObject(data).getString("token");
        data = AuthLoginUtils.decryptByPrivateKey(data);
        JSONObject object = JSONObject.parseObject(data);
        if (object.getString("appId") == null || "".equals(object.getString("appId"))){
            return AjaxResult.error("统一认证登录失败,请求参数异常");
        }

        //链接有效期验证
        String expireTime = object.getString("expireTime");
        if (!AuthLoginUtils.checkUrlExpirationTime(expireTime)){
            return AjaxResult.error("链接已过期!");
        }

        String username = object.getString("username");
        //登录方式 1-映射、2-透传
        Integer loginType = object.getInteger("loginType");
        //认证类型 0-账号密码 1-邮箱、2-短信、3-QQ、4-微信、5-支付宝
        Integer certificationType = object.getInteger("certificationType");
        AjaxResult ajax = AjaxResult.success();
        if (loginType == 1){
            //映射
            String token = null;
            token = sysLoginService.authLogin(username);
            ajax.put(Constants.TOKEN, token);
        }else{
            //穿透
            String password = object.getString("password");
            String token = null;
            SysUser sysUser = null;
            SysUser sysUserDo = null;

            //先查询用户是否存在,不存在的话,先手动注册一个
            sysUserDo = sysUserMapper.selectUserByUserName(username);
            //如果账号不存在,自动注册一个账号
            if (sysUserDo == null){
                if (password == null || "".equals(password)){
                    password = configService.selectConfigByKey("sys.user.initPassword");
                }
                //新增用户
                sysUserDo = new SysUser();
                sysUserDo.setUserName(username);
                sysUserDo.setNickName(username);
                sysUserDo.setPassword(SecurityUtils.encryptPassword(password));
                userService.registerUser(sysUserDo);
                //角色绑定
                sysUserDo.setRoleIds(new Long[]{2L});
                userService.insertUserRole(sysUserDo);
            }

            //根据认证类型处理登录操作
            sysUser = sysUserMapper.selectUserByUserName(username);
            if (sysUser != null){
                /*
                可选择校验,但是多平台的情况下要注意做好处理
                if (!SecurityUtils.matchesPassword(password, sysUser.getPassword())){
                    return AjaxResult.error("账号或密码错误!");
                }
                */
                token = sysLoginService.authLogin(username);
                ajax.put(Constants.TOKEN, token);
            }else {
                return AjaxResult.error("账号或密码错误!");
            }
        }
        return ajax;
    }


      /**
     * 统一认证重置密码
     * @param data
     * @return
     * @throws TClientException
     */
    @PostMapping("/resetPassword")
    public Result resetPassword(@RequestBody String data) throws TClientException {
        data = JSONObject.parseObject(data).getString("token");
        String decryptData = AuthLoginUtils.decryptByPrivateKey(data);
        JSONObject object = JSONObject.parseObject(decryptData);
        String appId = object.getString("appId");
        if (appId == null || "".equals(appId)) {
            throw new TClientException("统一认证重置密码,请求参数异常");
        }
        String username = object.getString("username");
        if (username == null || "".equals(username)) {
            throw new TClientException("统一认证重置密码,用户名不能为空");
        }
        //查询用户
        SysUser sysUser = sysUserMapper.selectUserByUserName(username);
        if (sysUser == null){
            throw new TClientException("统一认证重置密码,用户不存在");
        }
        String password = object.getString("password");
        if (password == null || "".equals(password)) {
            throw new TClientException("统一认证重置密码,密码不能为空");
        }
        int row = userService.resetUserPwd(username, SecurityUtils.encryptPassword(password));
        if (row > 0){
            Result result = new Result();
            result.setSccess("重置成功");
            return result;
        }else{
            throw new TClientException("统一认证重置密码,重置失败");
        }
    }


    /**
     * 统一认证退出登录接口
     * @param data
     * @return
     * @throws TClientException
     */
    @PostMapping("/authLogOut")
    public AjaxResult authLogOut(@RequestBody String data) throws TClientException {
        data = JSONObject.parseObject(data).getString("token");
        String decryptData = AuthLoginUtils.decryptByPrivateKey(data);
        JSONObject object = JSONObject.parseObject(decryptData);

        String appId = object.getString("appId");
        if (appId == null || "".equals(appId)) {
            throw new TClientException("统一认证退出登录,请求参数异常");
        }
        String username = object.getString("username");
        if (username == null || "".equals(username)) {
            throw new TClientException("统一认证退出登录,用户名不能为空");
        }
        logoutByUsername(username);
        return AjaxResult.success("用户已执行注销登录");
    }

    /**
     * 根据用户名强制退出登录(踢下线)
     */
    public void logoutByUsername(String username) {
        Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
        for (String key : keys)
        {
            try{
                LoginUser user = redisCache.getCacheObject(key);
                if(user.getUsername().equals(username)){
                    redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + user.getToken());
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

}
java

3、封装统一认证登录方法

方法:sysLoginService.authLogin(username);

/**
 * 统一认证登录
 *
 * @param username 用户名
 * @return 结果
 */
public String authLogin(String username)
{
    LoginUser loginUser = new LoginUser();
    SysUser sysUser = userService.selectUserByUserName(username);
    try
    {
        //自定义封装
        loginUser.setUserId(sysUser.getUserId());
        loginUser.setDeptId(sysUser.getDeptId());
        loginUser.setUser(userService.selectUserByUserName(sysUser.getUserName()));
        loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
    }
    catch (Exception e)
    {
        if (e instanceof BadCredentialsException)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }
        else
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
            throw new ServiceException(e.getMessage());
        }
    }
    finally
    {
        AuthenticationContextHolder.clearContext();
    }
    recordLoginInfo(loginUser.getUserId());
    // 生成token
    return tokenService.createToken(loginUser);
}
java

4、拦截器调整

位置:com.detn.framework.config.SecurityConfig

新增:"/open/api/zhHaAuthLogin/**"

        httpSecurity
        // CSRF禁用,因为不使用session
        .csrf().disable()
        // 禁用HTTP响应标头
        .headers().cacheControl().disable().and()
        // 认证失败处理类
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
        // 基于token,所以不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        // 过滤请求
        .authorizeRequests()
        // 对于登录login 注册register 验证码captchaImage 允许匿名访问
        .antMatchers("/login", "/register", "/captchaImage","/open/api/zhHaAuthLogin/**").permitAll()
        // 静态资源,可匿名访问
        .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
        .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
        // 除上面外的所有请求全部需要鉴权认证
        .anyRequest().authenticated()
        .and()
        .headers().frameOptions().disable();
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
java

前端

新增JS

名称:api/authLogin.js

import request from '@/utils/request'

export function getAuthLoginUrl() {
  return request({
    url: '/open/api/zhHaAuthLogin/getAuthLoginUrl',
    headers: {
      isToken: false
    },
    method: 'post'
  })
}
JavaScript

登录页面改造

注意:请备份原页面后再调整

页面:/login.vue

<template>
  <div class="login">

  </div>
</template>

<script>
import { getAuthLoginUrl } from "@/api/authLogin";

export default {
  name: "Login",
  data() {
    return {

    };
  },
  created() {
    this.getUrl()
  },
  methods: {
    getUrl() {
      getAuthLoginUrl().then(res => {
        if (res.data && res.data) {
          window.location.href = res.data
        }
      })
    },
  }
};
</script>
html

新增认证登录页面

文件名称:/authLogin.vue

<template>
  <div></div>
</template>

<script>

export default {
  name: "AuthLogin",
  data() {
    return {
      loading: false,
      redirect: "/index"
    };
  },
  created() {
    // 页面加载完成后,立即执行自动登录
    this.autoLoginByUrlToken();
  },
  methods: {
    /**
     * 核心逻辑:从URL获取token(加密数据),自动完成登录
     */
    autoLoginByUrlToken() {
      const urlParams = new URLSearchParams(window.location.search);
      const encryptToken = urlParams.get("token");

      if (!encryptToken) {
        this.$message.error("未检测到登录凭证,请从认证系统跳转");
        this.$router.push("/login");
        return;
      }

      this.$store.dispatch("authLogin", encryptToken).then(() => {
        this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
      }).catch(() => {});
    }
  }
};
</script>
html

新增 authLogin 登录方法

位置:store/modules/user.js

顶部添加 authLogin方法

import { login, logout, getInfo, authLogin } from '@/api/login'


// 登录
authLogin({ commit }, token) {
  return new Promise((resolve, reject) => {
    authLogin(token).then(res => {
      setToken(res.token)
      commit('SET_TOKEN', res.token)
      resolve()
    }).catch(error => {
      reject(error)
    })
  })
},
html

位置:api/login.js

// 登录方法
export function authLogin(token) {
  const data = {
    token
  }
  return request({
    url: '/open/api/zhHaAuthLogin/authLogin',
    headers: {
      isToken: false
    },
    method: 'post',
    data: data
  })
}
html

新增 authLogin 路由

位置:router/index.js

{
  path: '/authLogin',
  component: () => import('@/views/authLogin'),
  hidden: true
},

新增 authLogin 页面白名单

位置:/permission.js

const whiteList = ['/login', '/authLogin', '/auth-redirect','/register']

智慧互安认证平台

应用配置说明

1、应用名称

统一认证页面会显示应用名称,一般填写自己的系统名称

2、认证类型

默认不选则自动会是密码认证(密码认证不管选不选都会有),其余认证需要购买资源包才可以使用。

3、认证策略

单点登录功能,如无特殊需求,选择不限制即可

4、登录方式

1、账号映射:需要提前配置好映射账号才能进行登录

2、账号透传:账号直接透传给接入系统,如果有验证码是由认证平台进行发送和验证,只会透传账号和密码,或者邮箱手机号等,不会传入验证码,接入平台可以根据传入信息自动注册账号并实现自动登录。

5、登录地址

该登录地址是指接入系统的登录跳转页面地址,比如上方的 authLogin.vue 页面,访问地址如:http://localhost/authLogin

6、注销地址

该地址填写注销接口地址,请求方式:POST JSON ,一般认证平台用于认证策略时调用,地址:http://localhost/open/api/zhHaAuthLogin/authLogOut

注意

1、登录地址有效期为1分钟,超时需要重新登录




👇🏼👇🏼👇🏼扫描关注微信订阅号 或者 扫码加入交流群 了解更多技术分享,工作室官网:www.uvstu.com
微信订阅号 QQ交流群
描述 描述