基于antd pro的企微验证码登录功能(前后端)

概述

企业微信作为一款即时办公通讯工具,在企业级应用中有很多功能场景,如:告警触达,数据推送,登录验证。其中登录验证方式,可以替换手机短信验证码登录方式,能够一定程度上为公司节约一笔费用支出。本文就登录验证这一功能,设计较为详细的一整套解决方案。

设计

登录界面设计:
在这里插入图片描述
大致交互设计:

  1. 登录账号,即域账户;密码为域账号密码,即开机密码。两者皆不可为空。前后端都需要拦截校验;
  2. 只有输入账号和密码后,才能点击获取验证码;即:点击获取验证码,需要拦截校验账号密码是否为空。为空,则不进入60s倒计时;不为空,则进入倒计时,并调用后端接口;
  3. 后端响应获取验证码,即getWechatCode,将验证码发送到用户的企微,验证码为4位随机数字
  4. 验证码不可为空,点击登录按钮前,前后端都需要校验;
  5. 登录成功,则跳转到首页;登录失败,弹窗展示失败信息;

实现

需求很明确,功能实现就不难。

登录界面是一个系统或者平台应用的first sight,需要给用户留一个好的印象。

前端

antd中并没有自带能够倒计时的按钮,但antd pro的ProForm components中倒是提供针对验证码相关的组件ProFormCaptcha。

前端代码:
service.ts文件:

export async function getWechatCode(params: any) {
  return request('/api/login/wechatCode', {
    method: 'POST',
    data: params,
  });
}

export async function accountLogin(params: LoginParamsType2) {
  return request('/api/login/login', {
    method: 'POST',
    data: params,
  });
}

model.ts文件:

import { stringify } from 'querystring';
import { history, Reducer, Effect } from 'umi';
import { accountLogin } from '@/services/login';
import { setAuthority, setCookie } from '@/utils/authority';
import { getPageQuery } from '@/utils/utils';
import { message } from 'antd';

export interface StateType {
  status?: 'ok' | 'error';
  type?: string;
  logMsg?: string;
  currentAuthority?: 'user' | 'guest' | 'admin';
}

export interface LoginModelType {
  namespace: string;
  state: StateType;
  effects: {
    login: Effect;
  };
  reducers: {
    changeLoginStatus: Reducer<StateType>;
    setLoginErrorMsg: Reducer<StateType>;
  };
}

const Model: LoginModelType = {
  namespace: 'login',

  state: {
    status: undefined,
  },

  effects: {
    *login({ payload, callback }, { call, put, select }) {
      const response = yield call(accountLogin, {...payload});
      yield put({
        type: 'changeLoginStatus',
        payload: response,
      });
      // Login successfully
      if (response.status === 'success') {
        setCookie("token", response.data?.jwtToken);
        const urlParams = new URL(window.location.href);
        const params = getPageQuery();
        message.success('登录成功!');
        let { redirect } = params as { redirect: string };
        if (redirect) {
          const redirectUrlParams = new URL(redirect);
          if (redirectUrlParams.origin === urlParams.origin) {
            redirect = redirect.substr(urlParams.origin.length);
            if (redirect.match(/^\/.*#/)) {
              redirect = redirect.substr(redirect.indexOf('#') + 1);
            }
          } else {
            window.location.href = '/';
            return;
          }
        }
        history.replace(redirect || '/');
      } else if (response.status === 'error' && callback) {
        callback()
      }
    },
  },

  reducers: {
    changeLoginStatus(state, { payload }) {
      setAuthority(payload.currentAuthority);
      return {
        ...state,
        status: payload.status,
        logMsg: payload.msg,
        type: 'account',
      };
    },
  },
};

export default Model;

Login.tsx文件,其中核心代码为throw new Error("获取验证码错误");

import React, {useState} from "react";
import {Form, message} from 'antd';
import {connect, Dispatch, FormattedMessage, useIntl} from "umi";
import type {StateType} from '@/models/login';
import ProForm, {ProFormCaptcha, ProFormText} from '@ant-design/pro-form';
import type {ConnectState} from '@/models/connect';
import type {LoginParamsType2} from "@/services/login";
import {getWechatCode} from "@/services/login";
import styles from './index.less';
import {PasswordIcon, UserIdIcon, WechatCodeIcon} from './svg/svg';

interface LoginProps {
  dispatch: Dispatch;
  userLogin: StateType;
  submitting?: boolean;
}

const LoginMessage: React.FC<{
  content: string;
}> = ({content}) => (
  <div className={styles.loginMessage}>
    {content}
  </div>
);

const Login: React.FC<LoginProps> = (props) => {
  const {userLogin = {}, submitting} = props;
  const {status, type: loginType,} = userLogin;
  const [type] = useState<string>('account');
  const intl = useIntl();
  const [form] = Form.useForm();
  const [wechatCodeInfo, setWechatCodeInfo] = useState<any>({})
  const handleSubmit = (values: LoginParamsType2) => {
    const {dispatch} = props;
    dispatch({
      type: 'login/login',
      payload: {...values, type},
      callback: () => {
        dispatch({
          type: 'imagecode/getImgCode',
        })
      }
    })
  };

  return (
    <div className={styles.main}>

      <ProForm
        form={form}
        initialValues={{
          autoLogin: true,
        }}
        submitter={{
          render: (_, dom) => dom.pop(),
          submitButtonProps: {
            loading: submitting,
            size: 'large',
            style: {
              width: '100%',
              background: '#3F81EE',
              borderColor: '#3F81EE',
            },
          },
          searchConfig: {
            submitText: '登录',
          },
        }}
        onFinish={async (values) => {
          handleSubmit(values);
        }}
      >

        {status === 'error' && (
          <LoginMessage
            content={userLogin.logMsg ?? ''}
          />
        )}
        {wechatCodeInfo.status === 'error' && (
          <LoginMessage
            content={wechatCodeInfo.msg ?? ''}
          />
        )}
        {type === 'account' && (
          <>
            <ProFormText
              name="loginName"
              fieldProps={{
                size: 'large',
                prefix: <UserIdIcon className={styles.prefixIcon}/>,
              }}
              placeholder={intl.formatMessage({
                id: 'pages.login.username.placeholder',
                defaultMessage: '请输入账号',
              })}
              rules={[
                {
                  required: true,
                  message: (
                    <FormattedMessage
                      id="pages.login.username.required"
                      defaultMessage="请输入账号!"
                    />
                  ),
                },
              ]}
            />
            <ProFormText.Password
              name="password"
              fieldProps={{
                size: 'large',
                prefix: <PasswordIcon className={styles.prefixIcon}/>,
              }}
              placeholder={intl.formatMessage({
                id: 'pages.login.password.placeholder',
                defaultMessage: '请输入您的密码',
              })}
              rules={[
                {
                  required: true,
                  message: (
                    <FormattedMessage
                      id="pages.login.password.required"
                      defaultMessage="请输入密码!"
                    />
                  ),
                },
              ]}
            />
            <ProFormCaptcha
              name="wechatCode"
              fieldProps={{
                size: 'large',
                prefix: <WechatCodeIcon className={styles.prefixIcon}/>,
              }}
              captchaProps={{
                size: 'small',
              }}
              placeholder={intl.formatMessage({
                id: 'pages.login.wechatCode.placeholder',
                defaultMessage: '请输入企微验证码',
              })}
              captchaTextRender={(timing, count) => {
                if (timing) {
                  return `${count} 获取验证码`;
                }
                return '获取验证码';
              }}
              rules={[
                {
                  required: true,
                  message: (
                    <FormattedMessage
                      id="pages.login.wechatCode.required"
                      defaultMessage="请输入企微验证码!"
                    />
                  ),
                },
                {
                  pattern: /^\d{4}$/,
                  message: '请输入4位数字企微验证码!',
                },
              ]}
              onGetCaptcha={async () => {
                if (!form.getFieldValue('loginName') || !form.getFieldValue('password')) {
                  message.error('请输入用户名密码');
                  // 满足拦截条件时,不触发60s倒计时
                  throw new Error("获取验证码错误");
                  return;
                }
                const values = form.getFieldsValue();
                getWechatCode({
                  loginName: values.loginName,
                  password: values.password,
                }).then((res: any) => {
                  setWechatCodeInfo(res)
                })
              }}
            />
          </>
        )}

      </ProForm>
    </div>
  );
}

export default connect(({login, loading}: ConnectState) => ({
  userLogin: login,
  submitting: loading.effects['login/login'],
}))(Login);

后端

Controller类,个人认为不应该有任何数据校验逻辑,应该足够轻量化:

@RequestMapping("wechatCode")
public String getWechatCode(@RequestBody JSONObject jsonObject) {
    return userService.getWechatCode(jsonObject);
}

@RequestMapping("login")
public String login(@RequestBody JSONObject jsonObject) {
    return userService.login(jsonObject);
}

service类:

@Override
public String getWechatCode(JSONObject jsonObject) {
    String loginName = jsonObject.getString("loginName");
    String password = jsonObject.getString("password");
    if (StringUtils.isEmpty(loginName) || StringUtils.isEmpty(password)) {
        return JSONObject.toJSONString(ServiceUtil.returnError("用户名或密码不能为空!"));
    }
    try {
        // 判断用户是否存在
        DashboardUser user = userMapper.findUserByName(loginName);
        if (user == null) {
            return JSONObject.toJSONString(ServiceUtil.returnError(Constant.USERNAME_PASSWORD_ERR_MSG));
        }
        // 判断密码是否正确
        if (adminUserId.equals(user.getUserId()) || 1 == user.getUserType()) {
            // 普通用户
            if (!StringUtil.getMd5(password).equals(user.getUserPassword())) {
                return JSONObject.toJSONString(ServiceUtil.returnError(Constant.USERNAME_PASSWORD_ERR_MSG));
            }
        } else {
            // 域用户
            Boolean status = HttpUtil.checkDomain("CORP\\" + loginName, password, domainIp, domainPort);
            if (!status) {
                return JSONObject.toJSONString(ServiceUtil.returnError(Constant.USERNAME_PASSWORD_ERR_MSG));
            }
        }
        // 生成4位随机数字验证码并放入缓存中,缓存key是用户名_login_valid_qiwei,value是验证码,有效期1分钟
        String wechatCode = DataUtil.getRandomNum(4);
        log.info("4位随机数字验证码是:{}", wechatCode);
        String wechatKey = loginName.concat("_login_valid_qiwei");
        RedisTool.setKeyValueExpire(jedisCluster, wechatKey, wechatCode, 60);
        // 发送验证码到用户企微
        SendWeChatUtil.sendWeChat("你的账号正在尝试登录http://report.teslacorp.com,验证码:" + wechatCode + ",有效期1分钟,请及时处理。", user.getUserQiwei());
        return JSONObject.toJSONString(ServiceUtil.returnSuccess(wechatCode));
    } catch (Exception e) {
        return JSONObject.toJSONString(ServiceUtil.returnError(e.getMessage()));
    }
}

@Override
public String login(JSONObject jsonObject) {
    String wechatCode = jsonObject.getString("wechatCode");
    String loginName = jsonObject.getString("loginName");
    String password = jsonObject.getString("password");
    if (StringUtils.isEmpty(wechatCode)) {
		return JSONObject.toJSONString(ServiceUtil.returnError("企微验证码过期,请重试"));
    }
    if (StringUtils.isEmpty(loginName) || StringUtils.isEmpty(password)) {
        return JSONObject.toJSONString(ServiceUtil.returnError("用户名或密码不能为空!"));
    }
    // 验证企业微信验证码
    String wechatKey = loginName.concat("_login_valid_qiwei");
    try {
        String cacheWechatCode = RedisTool.getValueByKey(jedisCluster, wechatKey);
        // 验证企业微信验证码是否过期
        if (StringUtils.isEmpty(cacheWechatCode)) {
			return JSONObject.toJSONString(ServiceUtil.returnError("企微验证码过期,请重试"));
        }
        // 验证企业微信验证码是否正确
        if(!cacheWechatCode.equals(wechatCode)){
            return JSONObject.toJSONString(ServiceUtil.returnError("企微验证码错误,请重试"));
        }
        RedisTool.delKey(jedisCluster,wechatKey);
    } catch (Exception e) {
        return JSONObject.toJSONString(ServiceUtil.returnError(e.getMessage()));
    } finally {
        RedisTool.delKey(jedisCluster, wechatKey);
    }
    try {
        // 判断用户是否存在
        DashboardUser user = userMapper.findUserByName(loginName);
        if (user == null) {
            return JSONObject.toJSONString(ServiceUtil.returnError(Constant.USERNAME_PASSWORD_ERR_MSG));
        }
        // 普通用户
        if (adminUserId.equals(user.getUserId()) || 1 == user.getUserType()) {
            if (!StringUtil.getMd5(password).equals(user.getUserPassword())) {
                return JSONObject.toJSONString(ServiceUtil.returnError(Constant.USERNAME_PASSWORD_ERR_MSG));
            }
        } else {
            // 域用户
            Boolean status = HttpUtil.checkDomain("CORP\\" + loginName, password, domainIp, domainPort);
            if (!status) {
                return JSONObject.toJSONString(ServiceUtil.returnError(Constant.USERNAME_PASSWORD_ERR_MSG));
            }
        }
        List<DashboardRole> roleList = dashboardRoleResMapper.getRoleIdByUserId(user.getUserId());
        List<String> roleIds = roleList.stream().map(DashboardRole::getRoleId).collect(Collectors.toList());
        List<String> roleNames = roleList.stream().map(DashboardRole::getRoleName).collect(Collectors.toList());
        String jwtToken = JwtUtil.createJwt(user.getUserName(), user.getLoginName(), user.getUserId(), clientId, roleIds, roleNames,
                name, expiresSecond * 1000L, base64Secret);
        JSONObject resultJson = new JSONObject();
        resultJson.put("jwtToken", jwtToken);
        return JSONObject.toJSONString(ServiceUtil.returnSuccessData(resultJson));
    } catch (Exception e) {
        return JSONObject.toJSONString(ServiceUtil.returnError(e.getMessage()));
    }
}

参考

JWT入门教程
LDAP入门教程
企微工具类

posted @ 2022-05-31 12:14  johnny233  阅读(90)  评论(0编辑  收藏  举报  来源