重写用户快速登录逻辑

This commit is contained in:
YunaiV 2020-07-03 19:12:56 +08:00
parent ee7cf3e871
commit 3d6bd5e4ee
57 changed files with 1107 additions and 228 deletions

View File

@ -0,0 +1,14 @@
package cn.iocoder.common.framework.util;
import cn.hutool.crypto.digest.BCrypt;
/**
* 加解密工具类
*/
public class DigestUtils {
public static String bcrypt(String key, String salt) {
return BCrypt.hashpw(key, salt);
}
}

View File

@ -7,7 +7,7 @@ import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import java.util.UUID; import java.util.UUID;
public class MallUtil { public class MallUtils {
public static Integer getUserId(ServletRequest request) { public static Integer getUserId(ServletRequest request) {
return (Integer) request.getAttribute(MallConstants.REQUEST_ATTR_USER_ID_KEY); return (Integer) request.getAttribute(MallConstants.REQUEST_ATTR_USER_ID_KEY);
@ -46,7 +46,7 @@ public class MallUtil {
*/ */
public static String getTraceId() { public static String getTraceId() {
String traceId = TraceContext.traceId(); String traceId = TraceContext.traceId();
if (StringUtil.hasText(traceId)) { if (StringUtils.hasText(traceId)) {
return traceId; return traceId;
} }
// TODO 芋艿 多次调用会问题 // TODO 芋艿 多次调用会问题

View File

@ -85,7 +85,7 @@ public class ServiceExceptionUtil {
* @return 异常 * @return 异常
*/ */
public static ServiceException exception(Integer code) { public static ServiceException exception(Integer code) {
return new ServiceException(code, messages.get(code)); return exception(code, messages.get(code));
} }
/** /**
@ -96,11 +96,10 @@ public class ServiceExceptionUtil {
* @return 异常 * @return 异常
*/ */
public static ServiceException exception(Integer code, Object... params) { public static ServiceException exception(Integer code, Object... params) {
String message = doFormat(code, messages.get(code), params); return exception0(code, messages.get(code), params);
return new ServiceException(code, message);
} }
public static ServiceException exception(Integer code, String messagePattern, Object... params) { public static ServiceException exception0(Integer code, String messagePattern, Object... params) {
String message = doFormat(code, messagePattern, params); String message = doFormat(code, messagePattern, params);
return new ServiceException(code, message); return new ServiceException(code, message);
} }

View File

@ -1,29 +1,26 @@
package cn.iocoder.common.framework.util; package cn.iocoder.common.framework.util;
import org.springframework.util.StringUtils; import cn.hutool.core.lang.UUID;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class StringUtil { public class StringUtils {
public static boolean hasText(String str) { public static boolean hasText(String str) {
return StringUtils.hasText(str); return org.springframework.util.StringUtils.hasText(str);
} }
public static String join(Collection<?> coll, String delim) { public static String join(Collection<?> coll, String delim) {
return StringUtils.collectionToDelimitedString(coll, delim); return org.springframework.util.StringUtils.collectionToDelimitedString(coll, delim);
} }
public static List<String> split(String toSplit, String delim) { public static List<String> split(String toSplit, String delim) {
String[] stringArray = StringUtils.tokenizeToStringArray(toSplit, delim); String[] stringArray = org.springframework.util.StringUtils.tokenizeToStringArray(toSplit, delim);
return Arrays.asList(stringArray); return Arrays.asList(stringArray);
} }
public static List<Integer> splitToInt(String toSplit, String delim) { public static List<Integer> splitToInt(String toSplit, String delim) {
String[] stringArray = StringUtils.tokenizeToStringArray(toSplit, delim); String[] stringArray = org.springframework.util.StringUtils.tokenizeToStringArray(toSplit, delim);
List<Integer> array = new ArrayList<>(stringArray.length); List<Integer> array = new ArrayList<>(stringArray.length);
for (String string : stringArray) { for (String string : stringArray) {
array.add(Integer.valueOf(string)); array.add(Integer.valueOf(string));
@ -34,4 +31,9 @@ public class StringUtil {
public static String substring(String str, int start) { public static String substring(String str, int start) {
return org.apache.commons.lang3.StringUtils.substring(str, start); return org.apache.commons.lang3.StringUtils.substring(str, start);
} }
public static String uuid(boolean isSimple) {
return UUID.fastUUID().toString(isSimple);
}
} }

View File

@ -0,0 +1,28 @@
package cn.iocoder.common.framework.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = InEnumValidator.class
)
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.common.framework.validator;
import cn.iocoder.common.framework.util.ValidationUtil;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public void initialize(Mobile annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return ValidationUtil.isMobile(value);
}
}

View File

@ -1,26 +1,36 @@
package cn.iocoder.common.framework.vo; package cn.iocoder.common.framework.vo;
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.io.Serializable; import java.io.Serializable;
/**
* 通用返回
*
* @param <T> 数据泛型
*/
public final class CommonResult<T> implements Serializable { public final class CommonResult<T> implements Serializable {
private static Integer CODE_SUCCESS = 0; private static final Integer CODE_SUCCESS = 0;
/** /**
* 错误码 * 错误码
*/ */
private Integer code; private Integer code;
/**
* 错误提示
*/
private String message;
/** /**
* 返回数据 * 返回数据
*/ */
private T data; private T data;
/**
* 错误提示用户可阅读
*/
private String message;
/**
* 错误明细内部调试错误
*/
private String detailMessage;
/** /**
* 将传入的 result 对象转换成另外一个泛型结果的对象 * 将传入的 result 对象转换成另外一个泛型结果的对象
@ -75,6 +85,15 @@ public final class CommonResult<T> implements Serializable {
this.data = data; this.data = data;
} }
public String getDetailMessage() {
return detailMessage;
}
public CommonResult<T> setDetailMessage(String detailMessage) {
this.detailMessage = detailMessage;
return this;
}
@JsonIgnore @JsonIgnore
public boolean isSuccess() { public boolean isSuccess() {
return CODE_SUCCESS.equals(code); return CODE_SUCCESS.equals(code);
@ -85,6 +104,16 @@ public final class CommonResult<T> implements Serializable {
return !isSuccess(); return !isSuccess();
} }
/**
* 判断是否有异常如果有则抛出 {@link cn.iocoder.common.framework.exception.ServiceException} 异常
*/
public void checkError() {
if (isSuccess()) {
return;
}
throw ServiceExceptionUtil.exception0(code, message);
}
@Override @Override
public String toString() { public String toString() {
return "CommonResult{" + return "CommonResult{" +

View File

@ -44,4 +44,30 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
return this; return this;
} }
// ========== 重写父类方法方便链式调用 ==========
@Override
public QueryWrapperX<T> eq(boolean condition, String column, Object val) {
super.eq(condition, column, val);
return this;
}
@Override
public QueryWrapperX<T> eq(String column, Object val) {
super.eq(column, val);
return this;
}
@Override
public QueryWrapperX<T> orderByDesc(String column) {
super.orderByDesc(true, column);
return this;
}
@Override
public QueryWrapperX<T> last(String lastSql) {
super.last(lastSql);
return this;
}
} }

View File

@ -4,7 +4,7 @@ import cn.iocoder.common.framework.constant.SysErrorCodeEnum;
import cn.iocoder.common.framework.exception.ServiceException; import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.common.framework.util.ExceptionUtil; import cn.iocoder.common.framework.util.ExceptionUtil;
import cn.iocoder.common.framework.util.HttpUtil; import cn.iocoder.common.framework.util.HttpUtil;
import cn.iocoder.common.framework.util.MallUtil; import cn.iocoder.common.framework.util.MallUtils;
import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.system.rpc.api.systemlog.SystemLogRPC; import cn.iocoder.mall.system.rpc.api.systemlog.SystemLogRPC;
import cn.iocoder.mall.system.rpc.request.systemlog.ExceptionLogAddRequest; import cn.iocoder.mall.system.rpc.request.systemlog.ExceptionLogAddRequest;
@ -108,7 +108,7 @@ public class GlobalExceptionHandler {
exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName()); exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName());
exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
// 设置其它字段 // 设置其它字段
exceptionLog.setTraceId(MallUtil.getTraceId()) exceptionLog.setTraceId(MallUtils.getTraceId())
.setApplicationName(applicationName) .setApplicationName(applicationName)
.setUri(request.getRequestURI()) // TODO 提升如果想要优化可以使用 Swagger @ApiOperation 注解 .setUri(request.getRequestURI()) // TODO 提升如果想要优化可以使用 Swagger @ApiOperation 注解
.setQueryString(HttpUtil.buildQueryString(request)) .setQueryString(HttpUtil.buildQueryString(request))

View File

@ -1,7 +1,7 @@
package cn.iocoder.mall.web.core.interceptor; package cn.iocoder.mall.web.core.interceptor;
import cn.iocoder.common.framework.util.HttpUtil; import cn.iocoder.common.framework.util.HttpUtil;
import cn.iocoder.common.framework.util.MallUtil; import cn.iocoder.common.framework.util.MallUtils;
import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.system.rpc.api.systemlog.SystemLogRPC; import cn.iocoder.mall.system.rpc.api.systemlog.SystemLogRPC;
import cn.iocoder.mall.system.rpc.request.systemlog.AccessLogAddRequest; import cn.iocoder.mall.system.rpc.request.systemlog.AccessLogAddRequest;
@ -63,7 +63,7 @@ public class AccessLogInterceptor extends HandlerInterceptorAdapter {
accessLog.setErrorCode(result.getCode()) accessLog.setErrorCode(result.getCode())
.setErrorMessage(result.getMessage()); .setErrorMessage(result.getMessage());
// 设置其它字段 // 设置其它字段
accessLog.setTraceId(MallUtil.getTraceId()) accessLog.setTraceId(MallUtils.getTraceId())
.setApplicationName(applicationName) .setApplicationName(applicationName)
.setUri(request.getRequestURI()) // TODO 提升如果想要优化可以使用 Swagger @ApiOperation 注解 .setUri(request.getRequestURI()) // TODO 提升如果想要优化可以使用 Swagger @ApiOperation 注解
.setQueryString(HttpUtil.buildQueryString(request)) .setQueryString(HttpUtil.buildQueryString(request))

View File

@ -1,25 +0,0 @@
package cn.iocoder.mall.system.biz.dao.oauth2;
import cn.iocoder.mall.system.biz.dataobject.oauth2.OAuth2MobileCodeDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface OAuth2MobileCodeMapper extends BaseMapper<OAuth2MobileCodeDO> {
/**
* 获得手机号的最后一个手机验证码
*
* @param mobile 手机号
* @return 手机验证码
*/
default OAuth2MobileCodeDO selectLastByMobile(String mobile) {
QueryWrapper<OAuth2MobileCodeDO> query = new QueryWrapper<OAuth2MobileCodeDO>()
.eq("mobile", mobile)
.orderByDesc("id")
.last("limit 1");
return selectOne(query);
}
}

View File

@ -1,13 +1,10 @@
package cn.iocoder.mall.system.biz.dao.user; package cn.iocoder.mall.system.biz.dao.user;
import cn.iocoder.mall.mybatis.query.QueryWrapperX; import cn.iocoder.mall.mybatis.query.QueryWrapperX;
import cn.iocoder.mall.system.biz.dataobject.authorization.RoleDO;
import cn.iocoder.mall.system.biz.dataobject.user.UserDO; import cn.iocoder.mall.system.biz.dataobject.user.UserDO;
import cn.iocoder.mall.system.biz.dto.user.UserPageDTO; import cn.iocoder.mall.system.biz.dto.user.UserPageDTO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@ -15,12 +12,6 @@ import org.springframework.stereotype.Repository;
@Repository @Repository
public interface UserMapper extends BaseMapper<UserDO> { public interface UserMapper extends BaseMapper<UserDO> {
default UserDO selectByAccountId(Integer accountId) {
return selectOne(new QueryWrapper<UserDO>()
.eq("account_id", accountId)
);
}
/** /**
* 根据条件分页查询用户列表 * 根据条件分页查询用户列表
* @param userPageDTO * @param userPageDTO

View File

@ -4,7 +4,7 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.URLUtil; import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import cn.iocoder.common.framework.util.HttpUtil; import cn.iocoder.common.framework.util.HttpUtil;
import cn.iocoder.common.framework.util.MallUtil; import cn.iocoder.common.framework.util.MallUtils;
import cn.iocoder.mall.system.biz.log.operation.annotation.OperationLogging; import cn.iocoder.mall.system.biz.log.operation.annotation.OperationLogging;
import cn.iocoder.mall.system.biz.log.operation.enums.LogStatus; import cn.iocoder.mall.system.biz.log.operation.enums.LogStatus;
import cn.iocoder.mall.system.biz.log.operation.event.OperationLogEvent; import cn.iocoder.mall.system.biz.log.operation.event.OperationLogEvent;
@ -98,7 +98,7 @@ public class OperationLogAspect {
.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
return new OperationLogDTO() return new OperationLogDTO()
.setTraceId(MallUtil.getTraceId()) .setTraceId(MallUtils.getTraceId())
.setUri(URLUtil.getPath(request.getRequestURI())) .setUri(URLUtil.getPath(request.getRequestURI()))
.setUserAgent(HttpUtil.getUserAgent(request)) .setUserAgent(HttpUtil.getUserAgent(request))
.setIp(HttpUtil.getIp(request)) .setIp(HttpUtil.getIp(request))

View File

@ -1,7 +1,7 @@
package cn.iocoder.mall.system.biz.service.authorization; package cn.iocoder.mall.system.biz.service.authorization;
import cn.iocoder.common.framework.util.ServiceExceptionUtil; import cn.iocoder.common.framework.util.ServiceExceptionUtil;
import cn.iocoder.common.framework.util.StringUtil; import cn.iocoder.common.framework.util.StringUtils;
import cn.iocoder.common.framework.vo.PageResult; import cn.iocoder.common.framework.vo.PageResult;
import cn.iocoder.mall.mybatis.enums.DeletedStatusEnum; import cn.iocoder.mall.mybatis.enums.DeletedStatusEnum;
import cn.iocoder.mall.system.biz.bo.authorization.RoleBO; import cn.iocoder.mall.system.biz.bo.authorization.RoleBO;
@ -127,7 +127,7 @@ public class RoleServiceImpl implements RoleService {
throw ServiceExceptionUtil.exception(SystemErrorCodeEnum.ROLE_NAME_DUPLICATE, name); throw ServiceExceptionUtil.exception(SystemErrorCodeEnum.ROLE_NAME_DUPLICATE, name);
} }
// 2. 是否存在相同编码的角色 // 2. 是否存在相同编码的角色
if (!StringUtil.hasText(code)) { if (!StringUtils.hasText(code)) {
return; return;
} }
// code 编码被其它角色所使用 // code 编码被其它角色所使用

View File

@ -1,16 +0,0 @@
package cn.iocoder.mall.system.biz.service.oauth2;
import cn.iocoder.mall.system.biz.dto.oatuh2.OAuth2MobileCodeSendDTO;
/**
* OAuth2 手机验证码 Service 接口
*
* 我们将手机验证码登陆的方式作为一种拓展的 OAuth2 的认证方式因此我们放在了 `oauth2` 包下
*/
public interface OAuth2MobileCodeService {
void send(OAuth2MobileCodeSendDTO sendDTO);
void use(String mobile, String code);
}

View File

@ -1,89 +0,0 @@
package cn.iocoder.mall.system.biz.service.oauth2;
import cn.iocoder.common.framework.constant.SysErrorCodeEnum;
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
import cn.iocoder.common.framework.util.ValidationUtil;
import cn.iocoder.mall.system.biz.dao.oauth2.OAuth2MobileCodeMapper;
import cn.iocoder.mall.system.biz.dataobject.oauth2.OAuth2MobileCodeDO;
import cn.iocoder.mall.system.biz.dto.oatuh2.OAuth2MobileCodeSendDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
import static cn.iocoder.mall.system.biz.enums.SystemErrorCodeEnum.*;
@Service
public class OAuth2MobileCodeServiceImpl implements OAuth2MobileCodeService {
/**
* 每条验证码的过期时间单位毫秒
*/
@Value("${modules.oauth2-mobile-code-service.code-expire-time-millis}")
private int codeExpireTimes;
/**
* 每日发送最大数量
*/
@Value("${modules.oauth2-mobile-code-service.send-maximum-quantity-per-day}")
private int sendMaximumQuantityPerDay;
/**
* 短信发送频率单位毫秒
*/
@Value("${modules.oauth2-mobile-code-service.send-frequency}")
private int sendFrequency;
@Autowired
private OAuth2MobileCodeMapper oauth2MobileCodeMapper;
@Override
public void send(OAuth2MobileCodeSendDTO sendDTO) {
if (!ValidationUtil.isMobile(sendDTO.getMobile())) {
throw ServiceExceptionUtil.exception(SysErrorCodeEnum.VALIDATION_REQUEST_PARAM_ERROR.getCode(), "手机格式不正确"); // TODO 有点搓
}
// 校验是否可以发送验证码
OAuth2MobileCodeDO lastMobileCodePO = oauth2MobileCodeMapper.selectLastByMobile(sendDTO.getMobile());
if (lastMobileCodePO != null) {
if (lastMobileCodePO.getTodayIndex() >= sendMaximumQuantityPerDay) { // 超过当天发送的上限
throw ServiceExceptionUtil.exception(OAUTH2_MOBILE_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
}
if (System.currentTimeMillis() - lastMobileCodePO.getCreateTime().getTime() < sendFrequency) { // 发送过于频繁
throw ServiceExceptionUtil.exception(OAUTH2_MOBILE_CODE_SEND_TOO_FAST);
}
// TODO 提升每个 IP 每天可发送数量
// TODO 提升每个 IP 每小时可发送数量
}
// 创建验证码记录
OAuth2MobileCodeDO newMobileCodePO = new OAuth2MobileCodeDO().setMobile(sendDTO.getMobile())
.setCode("9999") // TODO 芋艿随机 4 位验证码 or 6 位验证码
.setTodayIndex(lastMobileCodePO != null ? lastMobileCodePO.getTodayIndex() : 1)
.setCreateIp(sendDTO.getIp())
.setUsed(false);
newMobileCodePO.setCreateTime(new Date());
oauth2MobileCodeMapper.insert(newMobileCodePO);
// TODO 发送验证码短信
}
@Override
public void use(String mobile, String code) {
// 校验验证码
OAuth2MobileCodeDO mobileCodeDO = oauth2MobileCodeMapper.selectLastByMobile(mobile);
if (mobileCodeDO == null) { // 若验证码不存在抛出异常
throw ServiceExceptionUtil.exception(OAUTH2_MOBILE_CODE_NOT_FOUND);
}
if (System.currentTimeMillis() - mobileCodeDO.getCreateTime().getTime() >= codeExpireTimes) { // 验证码已过期
throw ServiceExceptionUtil.exception(OAUTH2_MOBILE_CODE_EXPIRED);
}
if (mobileCodeDO.getUsed()) { // 验证码已使用
throw ServiceExceptionUtil.exception(OAUTH2_MOBILE_CODE_USED);
}
if (!mobileCodeDO.getCode().equals(code)) {
throw ServiceExceptionUtil.exception(OAUTH2_MOBILE_CODE_NOT_CORRECT);
}
// 使用验证码
OAuth2MobileCodeDO update = new OAuth2MobileCodeDO().setId(mobileCodeDO.getId())
.setUsed(true).setUsedTime(new Date()); // TODO usedIp
oauth2MobileCodeMapper.updateById(update);
}
}

View File

@ -1,29 +1,14 @@
package cn.iocoder.mall.system.rest.controller.oauth2; package cn.iocoder.mall.system.rest.controller.oauth2;
import cn.iocoder.common.framework.constant.MallConstants; import cn.iocoder.common.framework.constant.MallConstants;
import cn.iocoder.common.framework.util.HttpUtil;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.security.core.annotation.RequiresNone;
import cn.iocoder.mall.system.biz.bo.user.UserAuthenticateBO;
import cn.iocoder.mall.system.biz.dto.oatuh2.OAuth2MobileCodeAuthenticateDTO;
import cn.iocoder.mall.system.biz.dto.oatuh2.OAuth2MobileCodeSendDTO;
import cn.iocoder.mall.system.biz.service.oauth2.OAuth2MobileCodeService; import cn.iocoder.mall.system.biz.service.oauth2.OAuth2MobileCodeService;
import cn.iocoder.mall.system.biz.service.oauth2.OAuth2Service; import cn.iocoder.mall.system.biz.service.oauth2.OAuth2Service;
import cn.iocoder.mall.system.biz.service.user.UserService; import cn.iocoder.mall.system.biz.service.user.UserService;
import cn.iocoder.mall.system.rest.convert.oauth2.UsersOAuth2Convert;
import cn.iocoder.mall.system.rest.request.oauth2.UsersOAuth2MobileCodeAuthenticateRequest;
import cn.iocoder.mall.system.rest.response.user.UsersOAuth2AuthenticateResponse;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController @RestController
@RequestMapping(MallConstants.ROOT_PATH_USER + "/oauth2") @RequestMapping(MallConstants.ROOT_PATH_USER + "/oauth2")
@Api(tags = "用户 - OAuth2 API") @Api(tags = "用户 - OAuth2 API")
@ -36,33 +21,6 @@ public class UsersOAuth2Controller {
@Autowired @Autowired
private OAuth2MobileCodeService oauth2MobileCodeService; private OAuth2MobileCodeService oauth2MobileCodeService;
@PostMapping("/mobile-code-authenticate")
@ApiOperation("手机验证码认证")
@RequiresNone
public CommonResult<UsersOAuth2AuthenticateResponse> mobileCodeAuthenticate(UsersOAuth2MobileCodeAuthenticateRequest request,
HttpServletRequest httpRequest) {
// 执行认证
OAuth2MobileCodeAuthenticateDTO authenticateDTO = UsersOAuth2Convert.INSTANCE.convert(request)
.setIp(HttpUtil.getIp(httpRequest));
UserAuthenticateBO userAuthenticateBO = userService.authenticate(authenticateDTO);
// 转换返回
return CommonResult.success(
UsersOAuth2Convert.INSTANCE.convert(userAuthenticateBO)
);
}
@PostMapping("/send-mobile-code")
@ApiOperation("发送手机验证码")
@ApiImplicitParam(name = "mobile", value = "手机号", required = true, example = "15601691234")
@RequiresNone
public CommonResult<Boolean> sendMobileCode(@RequestParam("mobile") String mobile,
HttpServletRequest request) {
// 执行发送验证码
OAuth2MobileCodeSendDTO sendDTO = new OAuth2MobileCodeSendDTO()
.setMobile(mobile).setIp(HttpUtil.getIp(request));
oauth2MobileCodeService.send(sendDTO);
// 返回成功
return CommonResult.success(true);
}
} }

View File

@ -16,6 +16,16 @@
<groupId>cn.iocoder.mall</groupId> <groupId>cn.iocoder.mall</groupId>
<artifactId>common-framework</artifactId> <artifactId>common-framework</artifactId>
</dependency> </dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -0,0 +1,72 @@
package cn.iocoder.mall.userservice.enums;
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
/**
* 错误码枚举类
*
* system 系统使用 1-002-000-000
*/
public enum UserErrorCodeEnum implements ServiceExceptionUtil.Enumerable<UserErrorCodeEnum> {
// ========== OAUTH2 模块 ==========
OAUTH2_UNKNOWN(1001001000, "未知错误"), // 预留
// 预留 1001001001 ~ 1001001099 错误码方便前端
OAUTH2_ACCESS_TOKEN_NOT_FOUND(1001001001, "访问令牌不存在"),
OAUTH2_ACCESS_TOKEN_TOKEN_EXPIRED(1001001002, "访问令牌已过期"),
OAUTH2_ACCESS_TOKEN_INVALID(1001001003, "访问令牌已失效"),
OAUTH2_NOT_AUTHENTICATE(1001001004, "账号未登陆"),
OAUTH2_REFRESH_TOKEN_NOT_FOUND(1001001005, "刷新令牌不存在"),
OAUTH_REFRESH_TOKEN_EXPIRED(1001001006, "访问令牌已过期"),
OAUTH_REFRESH_TOKEN_INVALID(1001001007, "刷新令牌已失效"),
// 其它 1001001100 开始
OAUTH2_ACCOUNT_NOT_FOUND(1001001100, "账号不存在"),
OAUTH2_ACCOUNT_PASSWORD_ERROR(1001001101, "密码不正确"),
// ========== 用户手机验证码模块 ==========
USER_SMS_CODE_NOT_FOUND(1001001200, "验证码不存在"),
USER_SMS_CODE_EXPIRED(1001001201, "验证码已过期"),
USER_SMS_CODE_USED(1001001202, "验证码已使用"),
USER_SMS_CODE_NOT_CORRECT(1001001203, "验证码不正确"),
USER_SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY(1001001204, "超过每日短信发送数量"),
USER_SMS_CODE_SEND_TOO_FAST(1001001205, "短信发送过于频率"),
// ========== 用户地址 ==========
USER_ADDRESS_NOT_EXISTENT(1001004000, "用户地址不存在!"),
USER_ADDRESS_IS_DELETED(1001004001, "用户地址已被删除!"),
USER_GET_ADDRESS_NOT_EXISTS(1001004002, "获取的地址不存在!"),
// ========== 用户信息模块 1004004100 ==========
USER_NOT_EXISTS(1004004100, "用户不存在"),
USER_STATUS_NOT_EXISTS(1004004101, "用户状态不存在"),
USER_STATUS_EQUALS(1004004101, "用户已经是该状态"),
;
private final int code;
private final String message;
UserErrorCodeEnum(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
// TODO: 2020-05-22 封装成start的时候直接在start中定义一个统一的枚举从中取值
@Override
public int getGroup() {
return 0;
}
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.mall.userservice.enums.sms;
import cn.iocoder.common.framework.core.IntArrayValuable;
import java.util.Arrays;
/**
* 用户短信验证码发送场景的枚举
*/
public enum UserSmsSceneEnum implements IntArrayValuable {
LOGIN_BY_SMS(1, "手机号登陆"),
CHANGE_MOBILE_BY_SMS(2, "更换手机号"),
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserSmsSceneEnum::getValue).toArray();
private final Integer value;
private final String name;
UserSmsSceneEnum(Integer value, String name) {
this.value = value;
this.name = name;
}
@Override
public int[] array() {
return ARRAYS;
}
public Integer getValue() {
return value;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,16 @@
package cn.iocoder.mall.userservice.rpc.sms;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.userservice.rpc.sms.vo.UserSendSmsCodeDTO;
import cn.iocoder.mall.userservice.rpc.sms.vo.UserVerifySmsCodeDTO;
/**
* 用户短信验证码 Rpc 接口
*/
public interface UserSmsCodeRpc {
CommonResult<Boolean> sendSmsCode(UserSendSmsCodeDTO sendSmsCodeDTO);
CommonResult<Boolean> verifySmsCode(UserVerifySmsCodeDTO verifySmsCodeDTO);
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.mall.userservice.rpc.sms.vo;
import cn.iocoder.common.framework.validator.InEnum;
import cn.iocoder.mall.userservice.enums.sms.UserSmsSceneEnum;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 用户发送短信验证码 DTO
*/
@Data
@Accessors(chain = true)
@Builder
public class UserSendSmsCodeDTO implements Serializable {
/**
* 手机号码
*/
@NotNull(message = "手机号码不能为空")
private String mobile;
/**
* IP
*/
@NotNull(message = "IP 不能为空")
private String ip;
/**
* 发送场景
*/
@NotNull(message = "发送场景不能为空")
@InEnum(value = UserSmsSceneEnum.class, message = "发送场景不能为空")
private Integer scene;
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.mall.userservice.rpc.sms.vo;
import cn.iocoder.common.framework.validator.InEnum;
import cn.iocoder.mall.userservice.enums.sms.UserSmsSceneEnum;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 用户校验验证码 DTO
*/
@Data
@Accessors(chain = true)
@Builder
public class UserVerifySmsCodeDTO implements Serializable {
/**
* 手机号码
*/
@NotNull(message = "手机号码不能为空")
private String mobile;
/**
* IP
*/
@NotNull(message = "IP 不能为空")
private String ip;
/**
* 发送场景
*/
@NotNull(message = "发送场景不能为空")
@InEnum(value = UserSmsSceneEnum.class, message = "发送场景不能为空")
private Integer scene;
/**
* 验证码
*/
@NotNull(message = "验证码不能为空")
private String code;
}

View File

@ -1,10 +1,13 @@
package cn.iocoder.mall.userservice.rpc.user; package cn.iocoder.mall.userservice.rpc.user;
import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.userservice.rpc.user.dto.UserCreateDTO;
import cn.iocoder.mall.userservice.rpc.user.vo.UserVO; import cn.iocoder.mall.userservice.rpc.user.vo.UserVO;
public interface UserRpc { public interface UserRpc {
CommonResult<UserVO> getUser(Integer id); CommonResult<UserVO> getUser(Integer id);
CommonResult<UserVO> createUserIfAbsent(UserCreateDTO createDTO);
} }

View File

@ -0,0 +1,35 @@
package cn.iocoder.mall.userservice.rpc.user.dto;
import cn.iocoder.common.framework.validator.Mobile;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* 用户创建 DTO
*/
@Data
@Accessors(chain = true)
public class UserCreateDTO implements Serializable {
/**
* 手机号
*/
@NotNull(message = "手机号不能为空")
@Mobile(message = "手机格式不正确")
private String mobile;
/**
* 密码
*
* 允许为空自动生成
*/
private String password;
/**
* IP
*/
@NotNull(message = "IP 不能为空")
private String ip;
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.mall.userservice.rpc.user.vo; package cn.iocoder.mall.userservice.rpc.user.vo;
import cn.iocoder.common.framework.constant.CommonStatusEnum;
import java.io.Serializable; import java.io.Serializable;
/** /**

View File

@ -69,6 +69,19 @@
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId> <artifactId>mapstruct-jdk8</artifactId>
</dependency> </dependency>
<!-- <dependency>-->
<!-- <groupId>javax.el</groupId>-->
<!-- <artifactId>javax.el-api</artifactId>-->
<!-- <version>3.0.0</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>javax.el</groupId>-->
<!-- <artifactId>javax.el-api</artifactId>-->
<!-- <version>3.0.1-b06</version>-->
<!-- </dependency>-->
</dependencies> </dependencies>
</project> </project>

View File

@ -3,7 +3,7 @@ package cn.iocoder.mall.userservice;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"cn.iocoder.mall.userservice"}) @SpringBootApplication
public class UserServiceApplication { public class UserServiceApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -3,7 +3,9 @@ package cn.iocoder.mall.userservice.convert.user;
import cn.iocoder.mall.userservice.service.user.bo.UserBO; import cn.iocoder.mall.userservice.service.user.bo.UserBO;
import cn.iocoder.mall.userservice.dal.mysql.dataobject.user.UserDO; import cn.iocoder.mall.userservice.dal.mysql.dataobject.user.UserDO;
import cn.iocoder.mall.userservice.rpc.user.vo.UserVO; import cn.iocoder.mall.userservice.rpc.user.vo.UserVO;
import cn.iocoder.mall.userservice.service.user.bo.UserCreateBO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
@Mapper @Mapper
@ -15,4 +17,7 @@ public interface UserConvert {
UserBO convert(UserDO bean); UserBO convert(UserDO bean);
@Mapping(source = "ip", target = "createIp")
UserDO convert(UserCreateBO bean);
} }

View File

@ -1,6 +1,7 @@
package cn.iocoder.mall.system.biz.dataobject.oauth2; package cn.iocoder.mall.userservice.dal.mysql.dataobject.sms;
import cn.iocoder.mall.mybatis.dataobject.BaseDO; import cn.iocoder.mall.mybatis.dataobject.BaseDO;
import cn.iocoder.mall.userservice.enums.sms.UserSmsSceneEnum;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -9,13 +10,13 @@ import lombok.experimental.Accessors;
import java.util.Date; import java.util.Date;
/** /**
* OAuth2 手机验证码 * 手机验证码 DO
*/ */
@TableName("oauth2_mobile_code") @TableName("user_sms_code")
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Accessors(chain = true) @Accessors(chain = true)
public class OAuth2MobileCodeDO extends BaseDO { public class UserSmsCodeDO extends BaseDO {
/** /**
* 编号 * 编号
@ -29,6 +30,12 @@ public class OAuth2MobileCodeDO extends BaseDO {
* 验证码 * 验证码
*/ */
private String code; private String code;
/**
* 发送场景
*
* 外键 {@link UserSmsSceneEnum}
*/
private Integer scene;
/** /**
* 创建 IP * 创建 IP
*/ */
@ -48,6 +55,6 @@ public class OAuth2MobileCodeDO extends BaseDO {
/** /**
* 使用 IP * 使用 IP
*/ */
private Date usedIp; private String usedIp;
} }

View File

@ -46,5 +46,9 @@ public class UserDO extends DeletableDO {
* {@link #password} 的盐 * {@link #password} 的盐
*/ */
private String passwordSalt; private String passwordSalt;
/**
* 注册 IP
*/
private String createIp;
} }

View File

@ -1 +0,0 @@
package cn.iocoder.mall.userservice.dal.mysql.mapper;

View File

@ -0,0 +1,27 @@
package cn.iocoder.mall.userservice.dal.mysql.mapper.sms;
import cn.iocoder.mall.mybatis.query.QueryWrapperX;
import cn.iocoder.mall.userservice.dal.mysql.dataobject.sms.UserSmsCodeDO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface UserSmsCodeMapper extends BaseMapper<UserSmsCodeDO> {
/**
* 获得手机号的最后一个手机验证码
*
* @param mobile 手机号
* @param scene 发送场景选填
* @return 手机验证码
*/
default UserSmsCodeDO selectLastByMobile(String mobile, Integer scene) {
QueryWrapperX<UserSmsCodeDO> query = new QueryWrapperX<UserSmsCodeDO>()
.eq("mobile", mobile)
.eqIfPresent("scene", scene)
.orderByDesc("id")
.last("limit 1");
return selectOne(query);
}
}

View File

@ -1,12 +1,17 @@
package cn.iocoder.mall.userservice.dal.mysql.mapper.user; package cn.iocoder.mall.userservice.dal.mysql.mapper.user;
import cn.iocoder.mall.userservice.dal.mysql.dataobject.user.UserDO; import cn.iocoder.mall.userservice.dal.mysql.dataobject.user.UserDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface UserMapper extends BaseMapper<UserDO> { public interface UserMapper extends BaseMapper<UserDO> {
default UserDO selectByMobile(String mobile) {
return selectOne(new QueryWrapper<UserDO>()
.eq("mobile", mobile)
);
}
} }

View File

@ -0,0 +1,27 @@
package cn.iocoder.mall.userservice.manager.sms;
import cn.iocoder.mall.userservice.rpc.sms.vo.UserSendSmsCodeDTO;
import cn.iocoder.mall.userservice.rpc.sms.vo.UserVerifySmsCodeDTO;
import cn.iocoder.mall.userservice.service.sms.UserSmsCodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserSmsCodeManager {
@Autowired
private UserSmsCodeService userSmsCodeService;
public void sendSmsCode(UserSendSmsCodeDTO sendSmsCodeDTO) {
// 生成短信验证码
String smsCode = userSmsCodeService.createSmsCode(sendSmsCodeDTO.getMobile(),
sendSmsCodeDTO.getScene(), sendSmsCodeDTO.getIp());
// TODO 调用发送验证码
}
public void verifySmsCode(UserVerifySmsCodeDTO verifySmsCodeDTO) {
userSmsCodeService.verifySmsCode(verifySmsCodeDTO.getMobile(), verifySmsCodeDTO.getCode(),
verifySmsCodeDTO.getScene(), verifySmsCodeDTO.getIp());
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.mall.userservice.manager.user; package cn.iocoder.mall.userservice.manager.user;
import cn.iocoder.mall.userservice.rpc.user.dto.UserCreateDTO;
import cn.iocoder.mall.userservice.service.user.bo.UserBO; import cn.iocoder.mall.userservice.service.user.bo.UserBO;
import cn.iocoder.mall.userservice.service.user.UserService; import cn.iocoder.mall.userservice.service.user.UserService;
import cn.iocoder.mall.userservice.convert.user.UserConvert; import cn.iocoder.mall.userservice.convert.user.UserConvert;
@ -18,4 +19,15 @@ public class UserManager {
return UserConvert.INSTANCE.convert(userBO); return UserConvert.INSTANCE.convert(userBO);
} }
public UserVO createUserIfAbsent(UserCreateDTO createDTO) {
// 用户已经存在
UserBO userBO = userService.getUser(createDTO.getMobile());
if (userBO != null) {
return UserConvert.INSTANCE.convert(userBO);
}
// 用户不存在则进行创建
return null;
}
} }

View File

@ -0,0 +1,28 @@
package cn.iocoder.mall.userservice.rpc.sms;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.userservice.manager.sms.UserSmsCodeManager;
import cn.iocoder.mall.userservice.rpc.sms.vo.UserSendSmsCodeDTO;
import cn.iocoder.mall.userservice.rpc.sms.vo.UserVerifySmsCodeDTO;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;
@Service(version = "${dubbo.provider.UserSmsCodeRpc.version}", validation = "false")
public class UserSmsCodeRpcImpl implements UserSmsCodeRpc {
@Autowired
private UserSmsCodeManager userSmsCodeManager;
@Override
public CommonResult<Boolean> sendSmsCode(UserSendSmsCodeDTO sendSmsCodeDTO) {
userSmsCodeManager.sendSmsCode(sendSmsCodeDTO);
return CommonResult.success(true);
}
@Override
public CommonResult<Boolean> verifySmsCode(UserVerifySmsCodeDTO verifySmsCodeDTO) {
userSmsCodeManager.verifySmsCode(verifySmsCodeDTO);
return CommonResult.success(true);
}
}

View File

@ -2,13 +2,14 @@ package cn.iocoder.mall.userservice.rpc.user;
import cn.iocoder.common.framework.vo.CommonResult; import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.userservice.manager.user.UserManager; import cn.iocoder.mall.userservice.manager.user.UserManager;
import cn.iocoder.mall.userservice.rpc.user.dto.UserCreateDTO;
import cn.iocoder.mall.userservice.rpc.user.vo.UserVO; import cn.iocoder.mall.userservice.rpc.user.vo.UserVO;
import org.apache.dubbo.config.annotation.Service; import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import static cn.iocoder.common.framework.vo.CommonResult.success; import static cn.iocoder.common.framework.vo.CommonResult.success;
@Service(version = "${dubbo.provider.UserRpc.version}", validation = "true") @Service(version = "${dubbo.provider.UserRpc.version}", validation = "false")
public class UserRpcImpl implements UserRpc { public class UserRpcImpl implements UserRpc {
@Autowired @Autowired
@ -19,4 +20,9 @@ public class UserRpcImpl implements UserRpc {
return success(userManager.getUser(id)); return success(userManager.getUser(id));
} }
@Override
public CommonResult<UserVO> createUserIfAbsent(UserCreateDTO createDTO) {
return success(userManager.createUserIfAbsent(createDTO));
}
} }

View File

@ -0,0 +1,107 @@
package cn.iocoder.mall.userservice.service.sms;
import cn.iocoder.common.framework.constant.SysErrorCodeEnum;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.common.framework.util.ServiceExceptionUtil;
import cn.iocoder.common.framework.util.ValidationUtil;
import cn.iocoder.mall.userservice.dal.mysql.dataobject.sms.UserSmsCodeDO;
import cn.iocoder.mall.userservice.dal.mysql.mapper.sms.UserSmsCodeMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
import static cn.iocoder.mall.userservice.enums.UserErrorCodeEnum.*;
@Service
public class UserSmsCodeService {
/**
* 每条验证码的过期时间单位毫秒
*/
@Value("${modules.user-sms-code-service.code-expire-time-millis}")
private int codeExpireTimes;
/**
* 每日发送最大数量
*/
@Value("${modules.user-sms-code-service.send-maximum-quantity-per-day}")
private int sendMaximumQuantityPerDay;
/**
* 短信发送频率单位毫秒
*/
@Value("${modules.user-sms-code-service.send-frequency}")
private int sendFrequency;
@Autowired
private UserSmsCodeMapper userSmsCodeMapper;
/**
* 创建短信验证码并返回它
*
* 注意不包括发送逻辑
*
* @param mobile 手机号
* @param scene 发送场景
* @param ip IP
* @return 短信验证码
*/
public String createSmsCode(String mobile, Integer scene, String ip) {
if (!ValidationUtil.isMobile(mobile)) {
throw ServiceExceptionUtil.exception(SysErrorCodeEnum.VALIDATION_REQUEST_PARAM_ERROR.getCode(), "手机格式不正确"); // TODO 有点搓
}
// 校验是否可以发送验证码不用筛选场景
UserSmsCodeDO lastUserSmsCodeDO = userSmsCodeMapper.selectLastByMobile(mobile, null);
if (lastUserSmsCodeDO != null) {
if (lastUserSmsCodeDO.getTodayIndex() >= sendMaximumQuantityPerDay) { // 超过当天发送的上限
throw ServiceExceptionUtil.exception(USER_SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
}
if (System.currentTimeMillis() - lastUserSmsCodeDO.getCreateTime().getTime() < sendFrequency) { // 发送过于频繁
throw ServiceExceptionUtil.exception(USER_SMS_CODE_SEND_TOO_FAST);
}
// TODO 提升每个 IP 每天可发送数量
// TODO 提升每个 IP 每小时可发送数量
}
// 创建验证码记录
UserSmsCodeDO newMobileCodePO = new UserSmsCodeDO().setMobile(mobile)
.setCode("9999") // TODO 芋艿随机 4 位验证码 or 6 位验证码
.setScene(scene)
.setTodayIndex(lastUserSmsCodeDO != null ? lastUserSmsCodeDO.getTodayIndex() : 1)
.setCreateIp(ip).setUsed(false);
newMobileCodePO.setCreateTime(new Date());
userSmsCodeMapper.insert(newMobileCodePO);
return newMobileCodePO.getCode();
}
/**
* 验证短信验证码是否正确
* 如果正确则将验证码标记成已使用
* 如果错误则抛出 {@link ServiceException} 异常
*
* @param mobile 手机号
* @param code 验证码
* @param scene 发送场景
* @param ip IP
*/
public void verifySmsCode(String mobile, String code, Integer scene, String ip) {
// 校验验证码
UserSmsCodeDO mobileCodeDO = userSmsCodeMapper.selectLastByMobile(mobile, scene);
if (mobileCodeDO == null) { // 若验证码不存在抛出异常
throw ServiceExceptionUtil.exception(USER_SMS_CODE_NOT_FOUND);
}
if (System.currentTimeMillis() - mobileCodeDO.getCreateTime().getTime() >= codeExpireTimes) { // 验证码已过期
throw ServiceExceptionUtil.exception(USER_SMS_CODE_EXPIRED);
}
if (mobileCodeDO.getUsed()) { // 验证码已使用
throw ServiceExceptionUtil.exception(USER_SMS_CODE_USED);
}
if (!mobileCodeDO.getCode().equals(code)) {
throw ServiceExceptionUtil.exception(USER_SMS_CODE_NOT_CORRECT);
}
// 使用验证码
UserSmsCodeDO updateObj = new UserSmsCodeDO().setId(mobileCodeDO.getId())
.setUsed(true).setUsedTime(new Date()).setUsedIp(ip);
userSmsCodeMapper.updateById(updateObj);
}
}

View File

@ -1,9 +1,12 @@
package cn.iocoder.mall.userservice.service.user; package cn.iocoder.mall.userservice.service.user;
import cn.iocoder.common.framework.util.DigestUtils;
import cn.iocoder.common.framework.util.StringUtils;
import cn.iocoder.mall.userservice.convert.user.UserConvert; import cn.iocoder.mall.userservice.convert.user.UserConvert;
import cn.iocoder.mall.userservice.dal.mysql.dataobject.user.UserDO; import cn.iocoder.mall.userservice.dal.mysql.dataobject.user.UserDO;
import cn.iocoder.mall.userservice.dal.mysql.mapper.user.UserMapper; import cn.iocoder.mall.userservice.dal.mysql.mapper.user.UserMapper;
import cn.iocoder.mall.userservice.service.user.bo.UserBO; import cn.iocoder.mall.userservice.service.user.bo.UserBO;
import cn.iocoder.mall.userservice.service.user.bo.UserCreateBO;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -18,4 +21,36 @@ public class UserService {
return UserConvert.INSTANCE.convert(userDO); return UserConvert.INSTANCE.convert(userDO);
} }
public UserBO getUser(String mobile) {
UserDO userDO = userMapper.selectByMobile(mobile);
return UserConvert.INSTANCE.convert(userDO);
}
public UserBO createUser(UserCreateBO createBO) {
UserDO userDO = UserConvert.INSTANCE.convert(createBO);
// 加密密码
String passwordSalt = genPasswordSalt();
String password = createBO.getPassword();
if (!StringUtils.hasText(password)) {
password = genPassword();
}
password = encodePassword(password, passwordSalt);
userDO.setPassword(password).setPasswordSalt(passwordSalt);
// 保存用户
userMapper.insert(userDO);
return UserConvert.INSTANCE.convert(userDO);
}
private String genPasswordSalt() {
return StringUtils.uuid(true);
}
private String genPassword() {
return StringUtils.uuid(true);
}
private String encodePassword(String password, String salt) {
return DigestUtils.bcrypt(password, salt);
}
} }

View File

@ -1,4 +1,41 @@
package cn.iocoder.mall.userservice.service.user.bo; package cn.iocoder.mall.userservice.service.user.bo;
import cn.iocoder.common.framework.constant.CommonStatusEnum;
/**
* 用户信息
*/
public class UserBO { public class UserBO {
/**
* 用户编号
*/
private Integer id;
/**
* 昵称
*/
private String nickname;
/**
* 头像
*/
private String avatar;
/**
* 用户状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 手机
*/
private String mobile;
/**
* 经过加密的密码串
*/
private String password;
/**
* {@link #password} 的盐
*/
private String passwordSalt;
} }

View File

@ -0,0 +1,39 @@
package cn.iocoder.mall.userservice.service.user.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
/**
* 用户创建 BO
*/
@Data
@Accessors(chain = true)
public class UserCreateBO {
/**
* 昵称允许空
*/
private String nickname;
/**
* 头像允许空
*/
private String avatar;
/**
* 手机
*/
@NotNull(message = "手机号不能为空")
private String mobile;
/**
* 原始密码允许空
*
* 当为空时会自动进行生成
*/
private String password;
/**
* IP 地址
*/
private String ip;
}

View File

@ -0,0 +1,5 @@
##################### 业务模块 #####################
## UserSmsCodeService
modules.user-sms-code-service.code-expire-time-millis = 600000
modules.user-sms-code-service.send-maximum-quantity-per-day = 10
modules.user-sms-code-service.send-frequency = 60000

View File

@ -35,3 +35,5 @@ dubbo:
filter: -exception filter: -exception
UserRpc: UserRpc:
version: 1.0.0 version: 1.0.0
UserSmsCodeRpc:
version: 1.0.0

View File

@ -11,16 +11,20 @@
<artifactId>user-web-app</artifactId> <artifactId>user-web-app</artifactId>
<dependencies> <dependencyManagement>
<!-- onemall 基础 bom 文件 --> <dependencies>
<dependency> <!-- onemall 基础 bom 文件 -->
<groupId>cn.iocoder.mall</groupId> <dependency>
<artifactId>mall-dependencies</artifactId> <groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version> <artifactId>mall-dependencies</artifactId>
<type>pom</type> <version>1.0-SNAPSHOT</version>
<scope>import</scope> <type>pom</type>
</dependency> <scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Web 相关 --> <!-- Web 相关 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@ -45,6 +49,33 @@
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> </dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>javax.el</groupId>-->
<!-- <artifactId>javax.el-api</artifactId>-->
<!-- <version>3.0.0</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>javax.el</groupId>-->
<!-- <artifactId>javax.el-api</artifactId>-->
<!-- <version>3.0.1-b06</version>-->
<!-- </dependency>-->
</dependencies> </dependencies>
</project> </project>

View File

@ -0,0 +1,13 @@
package cn.iocoder.mall.userweb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserWebApplication {
public static void main(String[] args) {
SpringApplication.run(UserWebApplication.class, args);
}
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.mall.userweb.controller.passport;
import cn.iocoder.common.framework.util.HttpUtil;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.userweb.controller.passport.dto.UserPassportLoginBySmsDTO;
import cn.iocoder.mall.userweb.controller.passport.dto.UserPassportSendSmsCodeDTO;
import cn.iocoder.mall.userweb.controller.passport.vo.UserPassportVO;
import cn.iocoder.mall.userweb.manager.passport.UserPassportManager;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/passport")
public class UserPassportController {
@Autowired
private UserPassportManager userPassportManager;
@PostMapping("/login_by_sms")
@ApiOperation("手机验证码登陆")
// @RequiresNone TODO 晚点加上
public CommonResult<UserPassportVO> loginBySms(UserPassportLoginBySmsDTO loginBySmsDTO,
HttpServletRequest request) {
return CommonResult.success(userPassportManager.loginBySms(loginBySmsDTO, HttpUtil.getIp(request)));
}
@PostMapping("/send_sms_code")
@ApiOperation("发送手机验证码")
// @RequiresNone TODO 晚点加上
public CommonResult<Boolean> sendSmsCode(UserPassportSendSmsCodeDTO sendSmsCodeDTO,
HttpServletRequest request) {
userPassportManager.sendSmsCode(sendSmsCodeDTO, HttpUtil.getIp(request));
// 返回成功
return CommonResult.success(true);
}
}

View File

@ -0,0 +1,24 @@
package cn.iocoder.mall.userweb.controller.passport.dto;
import cn.iocoder.common.framework.validator.Mobile;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@ApiModel("用户短信验证码登陆 DTO")
@Data
@Accessors(chain = true)
public class UserPassportLoginBySmsDTO implements Serializable {
@ApiModelProperty(value = "手机号", example = "15601691234")
@Mobile
private String mobile;
@ApiModelProperty(value = "验证码", example = "1234")
@NotNull(message = "验证码不能为空")
private String code;
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.mall.userweb.controller.passport.dto;
import cn.iocoder.common.framework.validator.Mobile;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
@ApiModel("发送手机验证码 DTO")
@Data
@Accessors(chain = true)
public class UserPassportSendSmsCodeDTO {
@ApiModelProperty(value = "手机号", example = "15601691234")
@Mobile
private String mobile;
@ApiModelProperty(value = "发送场景", example = "1", notes = "对应 UserSmsSceneEnum 枚举")
@NotNull(message = "发送场景不能为空")
private Integer scene;
}

View File

@ -0,0 +1,72 @@
package cn.iocoder.mall.userweb.controller.passport.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 用户通信证信息
*/
@Data
@Accessors(chain = true)
public class UserPassportVO {
/**
* 认证信息
*/
@Data
@Accessors(chain = true)
public static class Authentication {
/**
* 访问令牌
*/
private String accessToken;
/**
* 刷新令牌
*/
private String refreshToken;
/**
* 账号编号
*/
private Integer accountId;
/**
* 过期时间
*/
private Date expiresTime;
}
/**
* 用户信息
*/
@Data
@Accessors(chain = true)
public static class User {
/**
* 用户编号
*/
private Integer id;
/**
* 昵称
*/
private String nickname;
/**
* 头像
*/
private String avatar;
}
/**
* 用户信息
*/
private User user;
/**
* 认证信息
*/
private Authentication authorization;
}

View File

@ -0,0 +1,4 @@
package cn.iocoder.mall.userweb.controller.user;
public class UserController {
}

View File

@ -0,0 +1 @@
package cn.iocoder.mall.userweb.convert;

View File

@ -0,0 +1,26 @@
package cn.iocoder.mall.userweb.convert.passport;
import cn.iocoder.mall.userservice.rpc.sms.vo.UserSendSmsCodeDTO;
import cn.iocoder.mall.userservice.rpc.sms.vo.UserVerifySmsCodeDTO;
import cn.iocoder.mall.userservice.rpc.user.dto.UserCreateDTO;
import cn.iocoder.mall.userservice.rpc.user.vo.UserVO;
import cn.iocoder.mall.userweb.controller.passport.dto.UserPassportLoginBySmsDTO;
import cn.iocoder.mall.userweb.controller.passport.dto.UserPassportSendSmsCodeDTO;
import cn.iocoder.mall.userweb.controller.passport.vo.UserPassportVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserPassportConvert {
UserPassportConvert INSTANCE = Mappers.getMapper(UserPassportConvert.class);
UserVerifySmsCodeDTO convert(UserPassportLoginBySmsDTO bean);
UserCreateDTO convert02(UserPassportLoginBySmsDTO bean);
UserPassportVO convert(UserVO userVO);
UserSendSmsCodeDTO convert(UserPassportSendSmsCodeDTO bean);
}

View File

@ -0,0 +1,40 @@
package cn.iocoder.mall.userweb.manager.passport;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.userservice.enums.sms.UserSmsSceneEnum;
import cn.iocoder.mall.userservice.rpc.sms.UserSmsCodeRpc;
import cn.iocoder.mall.userservice.rpc.user.UserRpc;
import cn.iocoder.mall.userservice.rpc.user.vo.UserVO;
import cn.iocoder.mall.userweb.controller.passport.dto.UserPassportLoginBySmsDTO;
import cn.iocoder.mall.userweb.controller.passport.dto.UserPassportSendSmsCodeDTO;
import cn.iocoder.mall.userweb.controller.passport.vo.UserPassportVO;
import cn.iocoder.mall.userweb.convert.passport.UserPassportConvert;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
@Service
public class UserPassportManager {
@Reference(version = "${dubbo.consumer.UserSmsCodeRpc.version}", validation = "false")
private UserSmsCodeRpc userSmsCodeRpc;
@Reference(version = "${dubbo.consumer.UserRpc.version}", validation = "false")
private UserRpc userRpc;
public UserPassportVO loginBySms(UserPassportLoginBySmsDTO loginBySmsDTO, String ip) {
// 校验验证码
CommonResult<Boolean> verifySmsCodeResult = userSmsCodeRpc.verifySmsCode(UserPassportConvert.INSTANCE.convert(loginBySmsDTO)
.setScene(UserSmsSceneEnum.LOGIN_BY_SMS.getValue()).setIp(ip));
verifySmsCodeResult.checkError();
// 获得用户
CommonResult<UserVO> createUserResult = userRpc.createUserIfAbsent(UserPassportConvert.INSTANCE.convert02(loginBySmsDTO).setIp(ip));
createUserResult.checkError();
// 创建访问令牌
return UserPassportConvert.INSTANCE.convert(createUserResult.getData());
}
public void sendSmsCode(UserPassportSendSmsCodeDTO sendSmsCodeDTO, String ip) {
CommonResult<Boolean> sendSmsCodeResult = userSmsCodeRpc.sendSmsCode(UserPassportConvert.INSTANCE.convert(sendSmsCodeDTO).setIp(ip));
sendSmsCodeResult.checkError();
}
}

View File

@ -0,0 +1,14 @@
spring:
# Spring Cloud 配置项
cloud:
nacos:
# Spring Cloud Nacos Discovery 配置项
discovery:
server-addr: 400-infra.server.iocoder.cn:8848 # Nacos 服务器地址
namespace: dev # Nacos 命名空间
# Dubbo 配置项
dubbo:
# Dubbo 注册中心
registry:
address: spring-cloud://400-infra.server.iocoder.cn:8848 # 指定 Dubbo 服务注册中心的地址

View File

@ -0,0 +1,14 @@
spring:
# Spring Cloud 配置项
cloud:
nacos:
# Spring Cloud Nacos Discovery 配置项
discovery:
server-addr: 400-infra.server.iocoder.cn:8848 # Nacos 服务器地址
namespace: local # Nacos 命名空间
# Dubbo 配置项
dubbo:
# Dubbo 注册中心
registry:
address: spring-cloud://400-infra.server.iocoder.cn:8848 # 指定 Dubbo 服务注册中心的地址

View File

@ -0,0 +1,25 @@
# 服务器的配置项
server:
port: 18082
servlet:
context-path: /user-api/
spring:
# Application 的配置项
application:
name: user-web
# Profile 的配置项
profiles:
active: local
# Dubbo 配置项
dubbo:
# Spring Cloud Alibaba Dubbo 专属配置
cloud:
subscribed-services: 'user-service' # 设置订阅的应用列表,默认为 * 订阅所有应用
# Dubbo 服务消费者的配置
consumer:
UserSmsCodeRpc:
version: 1.0.0
UserRpc:
version: 1.0.0