diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java index 7796ab589..f7e6cc4a5 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java @@ -31,7 +31,7 @@ import java.util.stream.Collectors; */ @Api(tags = "登录") @RestController -@RequestMapping("/rest/v1/logon") +@RequestMapping("/rest/v1/login") public class SysLoginController { @Autowired @@ -55,15 +55,43 @@ public class SysLoginController { */ @ApiOperation(value = "用户名密码登录") @PostMapping("/loginByUname") - public AjaxResult login(@RequestBody LoginBody loginBody) { + public AjaxResult login(@Validated @RequestBody LoginByUsernameVO loginBody) { AjaxResult ajax = AjaxResult.success(); // 生成令牌 - String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), - loginBody.getUuid()); + LoginCredential credential = LoginCredential.builder() + .loginType(ELoginType.USERNAME) + .username(loginBody.getUsername()) + .password(loginBody.getPassword()) + .imgUuid(loginBody.getUuid()) + .imgVerificationCode(loginBody.getCode()) + .build(); + String token = loginService.login(credential); ajax.put(Constants.TOKEN, token); return ajax; } + @ApiOperation(value = "短信验证码登录") + @PostMapping("/loginBySms") + public AjaxResult loginBySms(@Validated @RequestBody LoginBySmsCodeVO loginBody) { + AjaxResult ajax = AjaxResult.success(); + // 生成令牌 + LoginCredential credential = LoginCredential.builder() + .loginType(ELoginType.SMS_VERIFICATION_CODE) + .phoneNumber(loginBody.getPhoneNumber()) + .smsVerificationCode(loginBody.getCode()) + .build(); + String token = loginService.login(credential); + ajax.put(Constants.TOKEN, token); + return ajax; + } + + @ApiOperation(value = "发送登录短信验证码") + @PostMapping("/sendSmsVerificationCode") + public R sendSmsVerificationCode(@Validated @RequestBody LoginSmsReqVO vo) { + loginService.sendSmsVerificationCode(vo.getPhoneNumber(), vo.getCode(), vo.getUuid()); + return R.ok(); + } + /** * 获取用户信息 * diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java index 1fa8f88ae..133b549fa 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java @@ -1,13 +1,20 @@ package com.ruoyi.web.controller.system; +import com.ruoyi.common.constant.Constants; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.domain.model.ESystemRole; import com.ruoyi.common.core.domain.model.RegisterBody; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.web.service.SysRegisterService; import com.ruoyi.system.service.ISysConfigService; +import com.ruoyi.web.controller.system.vo.LoginSmsReqVO; +import com.ruoyi.web.controller.system.vo.RegisterBySmsCodeVO; import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -28,6 +35,33 @@ public class SysRegisterController extends BaseController { @Autowired private ISysConfigService configService; + @ApiOperation(value = "档口供应商注册") + @PostMapping("/registerStore") + public AjaxResult registerStore(@Validated @RequestBody RegisterBySmsCodeVO vo) { + AjaxResult ajax = AjaxResult.success(); + String token = registerService.registerByPhoneNumber(vo.getPhoneNumber(), vo.getPassword(), vo.getCode(), + ESystemRole.SUPPLIER); + ajax.put(Constants.TOKEN, token); + return ajax; + } + + @ApiOperation(value = "卖家注册") + @PostMapping("/registerSeller") + public AjaxResult registerSeller(@Validated @RequestBody RegisterBySmsCodeVO vo) { + AjaxResult ajax = AjaxResult.success(); + String token = registerService.registerByPhoneNumber(vo.getPhoneNumber(), vo.getPassword(), vo.getCode(), + ESystemRole.SELLER); + ajax.put(Constants.TOKEN, token); + return ajax; + } + + @ApiOperation(value = "发送登录短信验证码") + @PostMapping("/sendSmsVerificationCode") + public R sendSmsVerificationCode(@Validated @RequestBody LoginSmsReqVO vo) { + registerService.sendSmsVerificationCode(vo.getPhoneNumber(), vo.getCode(), vo.getUuid()); + return R.ok(); + } + @PostMapping("/register") public AjaxResult register(@RequestBody RegisterBody user) { if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) { diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginBySmsCodeVO.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginBySmsCodeVO.java new file mode 100644 index 000000000..ed7449c0c --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginBySmsCodeVO.java @@ -0,0 +1,25 @@ +package com.ruoyi.web.controller.system.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * @author liangyq + * @date 2025-06-05 15:41 + */ +@ApiModel +@Data +public class LoginBySmsCodeVO { + + @NotEmpty(message = "手机号不能为空") + @ApiModelProperty("手机号") + private String phoneNumber; + + @NotEmpty(message = "验证码不能为空") + @ApiModelProperty("验证码") + private String code; + +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginByUsernameVO.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginByUsernameVO.java new file mode 100644 index 000000000..129d8ada3 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginByUsernameVO.java @@ -0,0 +1,41 @@ +package com.ruoyi.web.controller.system.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * @author liangyq + * @date 2025-06-05 15:41 + */ +@ApiModel +@Data +public class LoginByUsernameVO { + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + @ApiModelProperty("用户名") + private String username; + + /** + * 用户密码 + */ + @NotEmpty(message = "用户密码不能为空") + @ApiModelProperty("用户密码") + private String password; + + /** + * 验证码 + */ + @ApiModelProperty("验证码") + private String code; + + /** + * 唯一标识 + */ + @ApiModelProperty("唯一标识") + private String uuid; +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginSmsReqVO.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginSmsReqVO.java new file mode 100644 index 000000000..fde46b9d0 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/LoginSmsReqVO.java @@ -0,0 +1,28 @@ +package com.ruoyi.web.controller.system.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +/** + * @author liangyq + * @date 2025-06-05 15:41 + */ +@ApiModel +@Data +public class LoginSmsReqVO { + + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + @NotEmpty(message = "手机号不能为空") + @ApiModelProperty("手机号") + private String phoneNumber; + + @ApiModelProperty("验证码") + private String code; + + @ApiModelProperty("唯一标识") + private String uuid; +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/RegisterBySmsCodeVO.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/RegisterBySmsCodeVO.java new file mode 100644 index 000000000..eb5e1661b --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/vo/RegisterBySmsCodeVO.java @@ -0,0 +1,28 @@ +package com.ruoyi.web.controller.system.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * @author liangyq + * @date 2025-06-04 15:41 + */ +@ApiModel +@Data +public class RegisterBySmsCodeVO { + + @NotEmpty(message = "手机号不能为空") + @ApiModelProperty("手机号") + private String phoneNumber; + + @NotEmpty(message = "短信验证码不能为空") + @ApiModelProperty("短信验证码") + private String code; + + @ApiModelProperty("用户密码") + private String password; + +} diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index cd8b44eae..be016cbf0 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -37,6 +37,9 @@ sms: accessKeySecret: gVpsVKSds5keQeN0mDDVKmY8YTGOkk regionId: send: true + verificationCode: + signName: 成都一方鹏商贸 + templateCode: SMS_318925388 es: #多个用","分割 hosts: 49.234.41.39:9222 diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java index 7722d7bf1..8a92a77ac 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java @@ -22,6 +22,11 @@ public class CacheConstants { */ public static final String CAPTCHA_CODE_KEY = "captcha_codes:"; + /** + * 短信登录/注册验证码 redis key + */ + public static final String SMS_LOGIN_CAPTCHA_CODE_KEY = "sms_login_captcha_codes:"; + /** * 参数管理 cache key */ diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/ELoginType.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/ELoginType.java new file mode 100644 index 000000000..7612bc52c --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/ELoginType.java @@ -0,0 +1,29 @@ +package com.ruoyi.common.core.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author liangyq + * @date 2025-05-27 + */ +@Getter +@AllArgsConstructor +public enum ELoginType { + + + USERNAME(1, "用户名登录"), + SMS_VERIFICATION_CODE(2, "短信验证码登录"); + + private final Integer value; + private final String label; + + public static ELoginType of(Integer value) { + for (ELoginType e : ELoginType.values()) { + if (e.getValue().equals(value)) { + return e; + } + } + return null; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginCredential.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginCredential.java new file mode 100644 index 000000000..e9b79fd3d --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/LoginCredential.java @@ -0,0 +1,32 @@ +package com.ruoyi.common.core.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author liangyq + * @date 2025-06-05 11:16 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class LoginCredential { + + private ELoginType loginType; + + private String username; + + private String password; + + private String phoneNumber; + + private String smsVerificationCode; + + private String imgUuid; + + private String imgVerificationCode; + +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index b1ca72e3e..30720f97e 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -111,7 +111,9 @@ public class SecurityConfig .authorizeHttpRequests((requests) -> { permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - requests.antMatchers("/rest/v1/logon/loginByUname", "/rest/v1/reg/register", "/rest/v1/captcha/image").permitAll() + requests.antMatchers("/rest/v1/login/loginByUname", "/rest/v1/login/loginBySms", + "/rest/v1/login/sendSmsVerificationCode", "/rest/v1/reg/registerStore", "/rest/v1/reg/registerSeller", + "/rest/v1/reg/sendSmsVerificationCode", "/rest/v1/reg/register", "/rest/v1/captcha/image").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/sms/SmsClientWrapper.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/sms/SmsClientWrapper.java index 9ee646eaa..efd44d243 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/sms/SmsClientWrapper.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/sms/SmsClientWrapper.java @@ -17,6 +17,12 @@ public class SmsClientWrapper { @Value("${sms.send:true}") private Boolean doSend; + @Value("${sms.verificationCode.signName:}") + private String verificationCodeSignName; + + @Value("${sms.verificationCode.templateCode:}") + private String verificationCodeTemplateCode; + public boolean sendSms(String signName, String phoneNumber, String templateCode, String templateParams) { boolean sendResult; if (doSend) { @@ -31,4 +37,9 @@ public class SmsClientWrapper { return sendResult; } + public boolean sendVerificationCode(String phoneNumber, String code) { + return sendSms(verificationCodeSignName, phoneNumber, verificationCodeTemplateCode, + "{\"code\":\"" + code + "\"}"); + } + } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java index 216bda4a5..d24e2ad58 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java @@ -1,10 +1,15 @@ package com.ruoyi.framework.web.service; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.UserConstants; +import com.ruoyi.common.core.domain.model.LoginCredential; import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.core.domain.model.UserInfo; import com.ruoyi.common.core.redis.RedisCache; +import com.ruoyi.common.enums.UserStatus; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.user.*; import com.ruoyi.common.utils.DateUtils; @@ -13,26 +18,28 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.ip.IpUtils; import com.ruoyi.framework.manager.AsyncManager; import com.ruoyi.framework.manager.factory.AsyncFactory; -import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import com.ruoyi.framework.sms.SmsClientWrapper; import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysUserService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; /** * 登录校验方法 - * + * * @author ruoyi */ +@Slf4j @Component -public class SysLoginService -{ +public class SysLoginService { + @Autowired private TokenService tokenService; @@ -41,87 +48,150 @@ public class SysLoginService @Autowired private RedisCache redisCache; - + @Autowired private ISysUserService userService; @Autowired private ISysConfigService configService; + @Autowired + private SysPasswordService passwordService; + + @Autowired + private SmsClientWrapper smsClient; + /** * 登录验证 - * - * @param username 用户名 - * @param password 密码 - * @param code 验证码 - * @param uuid 唯一标识 - * @return 结果 + * + * @param loginCredential + * @return */ - public String login(String username, String password, String code, String uuid) - { + public String login(LoginCredential loginCredential) { + LoginUser loginUser; + switch (loginCredential.getLoginType()) { + case USERNAME: + loginUser = loginByUsername(loginCredential.getUsername(), loginCredential.getPassword(), + loginCredential.getImgVerificationCode(), loginCredential.getImgUuid()); + break; + case SMS_VERIFICATION_CODE: + loginUser = loginBySmsVerificationCode(loginCredential.getPhoneNumber(), + loginCredential.getSmsVerificationCode()); + break; + default: + throw new ServiceException("未知登录类型"); + } + return createToken(loginUser); + } - // 验证码校验 - validateCaptcha(username, code, uuid); + /** + * 登录成功,生成token + * + * @param loginUser + * @return + */ + public String createToken(LoginUser loginUser) { + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); - // 登录前置校验 - loginPreCheck(username, password); - // 用户验证 - Authentication authentication = null; - try - { - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); - AuthenticationContextHolder.setContext(authenticationToken); - // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername - authentication = authenticationManager.authenticate(authenticationToken); - } - 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(); - } - AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); - LoginUser loginUser = (LoginUser) authentication.getPrincipal(); + AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getUsername(), Constants.LOGIN_SUCCESS, + MessageUtils.message("user.login.success"))); recordLoginInfo(loginUser.getUserId()); // 生成token return tokenService.createToken(loginUser); } /** - * 校验验证码 - * + * 登录验证 + * + * @param phoneNumber + * @param smsCode + * @return + */ + private LoginUser loginBySmsVerificationCode(String phoneNumber, String smsCode) { + // 验证码校验 + validateSmsVerificationCode(phoneNumber, smsCode); + // 用户验证 + UserInfo user = userService.getUserByPhoneNumber(phoneNumber); + loginUserInfoCheck(phoneNumber, user); + return new LoginUser(user); + } + + /** + * 登录验证 + * * @param username 用户名 - * @param code 验证码 - * @param uuid 唯一标识 + * @param password 密码 + * @param code 验证码 + * @param uuid 唯一标识 * @return 结果 */ - public void validateCaptcha(String username, String code, String uuid) - { + private LoginUser loginByUsername(String username, String password, String code, String uuid) { + + // 验证码校验 + validateCaptcha(username, code, uuid); + + // 登录前置校验 + loginPreCheck(username, password); + + // 用户验证 + UserInfo user = userService.getUserByUsername(username); + loginUserInfoCheck(username, user); + + // 密码验证 + try { + passwordService.validate(user, password); + } catch (Exception e) { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); + throw new ServiceException(e.getMessage()); + } + return new LoginUser(user); + } + + private void loginUserInfoCheck(String username, UserInfo user) { + if (StringUtils.isNull(user)) { + log.info("登录用户:{} 不存在.", username); + String msg = MessageUtils.message("user.not.exists"); + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, msg)); + throw new ServiceException(msg); + } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) { + log.info("登录用户:{} 已被删除.", username); + String msg = MessageUtils.message("user.password.delete"); + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, msg)); + throw new ServiceException(msg); + } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) { + log.info("登录用户:{} 已被停用.", username); + String msg = MessageUtils.message("user.blocked"); + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, msg)); + throw new ServiceException(msg); + } + } + + /** + * 校验验证码 + * + * @param username 用户名 + * @param code 验证码 + * @param uuid 唯一标识 + * @return 结果 + */ + public void validateCaptcha(String username, String code, String uuid) { boolean captchaEnabled = configService.selectCaptchaEnabled(); - if (captchaEnabled) - { + if (captchaEnabled) { String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); String captcha = redisCache.getCacheObject(verifyKey); - if (captcha == null) - { - AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + if (captcha == null) { + if (username != null) { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); + } throw new CaptchaExpireException(); } redisCache.deleteObject(verifyKey); - if (!code.equalsIgnoreCase(captcha)) - { - AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + if (!code.equalsIgnoreCase(captcha)) { + if (username != null) { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); + } throw new CaptchaException(); } } @@ -129,35 +199,31 @@ public class SysLoginService /** * 登录前置校验 + * * @param username 用户名 * @param password 用户密码 */ - public void loginPreCheck(String username, String password) - { + public void loginPreCheck(String username, String password) { // 用户名或密码为空 错误 - if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) - { + if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null"))); throw new UserNotExistsException(); } // 密码如果不在指定范围内 错误 if (password.length() < UserConstants.PASSWORD_MIN_LENGTH - || password.length() > UserConstants.PASSWORD_MAX_LENGTH) - { + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); } // 用户名不在指定范围内 错误 if (username.length() < UserConstants.USERNAME_MIN_LENGTH - || username.length() > UserConstants.USERNAME_MAX_LENGTH) - { + || username.length() > UserConstants.USERNAME_MAX_LENGTH) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); } // IP黑名单校验 String blackStr = configService.selectConfigByKey("sys.login.blackIPList"); - if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) - { + if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked"))); throw new BlackListException(); } @@ -168,8 +234,52 @@ public class SysLoginService * * @param userId 用户ID */ - public void recordLoginInfo(Long userId) - { + public void recordLoginInfo(Long userId) { userService.updateLoginInfo(userId, IpUtils.getIpAddr(), DateUtils.getNowDate()); } + + /** + * 发送登录/注册短信验证码 + * + * @param phoneNumber 电话号码 + * @param code 图形验证码code + * @param uuid 图形验证码uuid + */ + public void sendSmsVerificationCode(String phoneNumber, String code, String uuid) { + validateCaptcha(null, code, uuid); + sendSmsVerificationCode(phoneNumber); + } + + /** + * 发送登录/注册短信验证码 + * + * @param phoneNumber 电话号码 + */ + public void sendSmsVerificationCode(String phoneNumber) { + String code = RandomUtil.randomNumbers(6); + boolean success = smsClient.sendVerificationCode(phoneNumber, code); + if (success) { + String rk = CacheConstants.SMS_LOGIN_CAPTCHA_CODE_KEY + phoneNumber; + redisCache.setCacheObject(rk, code, 5, TimeUnit.MINUTES); + } + } + + /** + * 验证登录/注册短信验证码 + * + * @param phoneNumber 电话号码 + * @param code 验证码 + * @return + */ + public void validateSmsVerificationCode(String phoneNumber, String code) { + String rk = CacheConstants.SMS_LOGIN_CAPTCHA_CODE_KEY + phoneNumber; + String cacheCode = redisCache.getCacheObject(rk); + if (cacheCode == null) { + throw new CaptchaExpireException(); + } + redisCache.deleteObject(rk); + if (!StrUtil.equals(cacheCode, code)) { + throw new CaptchaException(); + } + } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java index 1f3b6d0d7..6e37791f2 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java @@ -1,18 +1,16 @@ package com.ruoyi.framework.web.service; -import java.util.concurrent.TimeUnit; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.Authentication; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.stereotype.Component; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.exception.user.UserPasswordNotMatchException; import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException; -import com.ruoyi.common.utils.SecurityUtils; -import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; /** * 登录密码方法 @@ -42,11 +40,9 @@ public class SysPasswordService return CacheConstants.PWD_ERR_CNT_KEY + username; } - public void validate(SysUser user) + public void validate(SysUser user, String password) { - Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); - String username = usernamePasswordAuthenticationToken.getName(); - String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + String username = user.getUserName(); Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java index cb7a43310..adae65f99 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java @@ -1,14 +1,11 @@ package com.ruoyi.framework.web.service; -import com.ruoyi.common.core.domain.model.UserInfoEdit; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.UserConstants; -import com.ruoyi.common.core.domain.entity.SysUser; -import com.ruoyi.common.core.domain.model.RegisterBody; +import com.ruoyi.common.core.domain.model.*; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.exception.user.CaptchaException; import com.ruoyi.common.exception.user.CaptchaExpireException; @@ -17,18 +14,33 @@ import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.framework.manager.AsyncManager; import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.sms.SmsClientWrapper; import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * 注册校验方法 - * + * * @author ruoyi */ @Slf4j @Component -public class SysRegisterService -{ +public class SysRegisterService { + + @Value("${sms.verificationCode.signName:}") + private String verificationCodeSignName; + + @Value("${sms.verificationCode.templateCode:}") + private String verificationCodeTemplateCode; + @Autowired private ISysUserService userService; @@ -38,61 +50,81 @@ public class SysRegisterService @Autowired private RedisCache redisCache; + @Autowired + private SmsClientWrapper smsClient; + + @Autowired + private SysLoginService loginService; + + /** + * 注册 + * + * @param phoneNumber + * @param password + * @param smsVerificationCode + * @param roles + * @return + */ + public String registerByPhoneNumber(String phoneNumber, String password, String smsVerificationCode, + ESystemRole... roles) { + // 短信验证 + validateSmsVerificationCode(phoneNumber, smsVerificationCode); + UserInfoEdit userEdit = new UserInfoEdit(); + userEdit.setUserName(phoneNumber); + userEdit.setNickName(phoneNumber); + userEdit.setPhonenumber(phoneNumber); + userEdit.setPassword(password); + if (roles != null) { + userEdit.setRoleIds(Arrays.stream(roles).map(ESystemRole::getId).collect(Collectors.toList())); + } + //创建账号 + Long userId = userService.createUser(userEdit); + //登录 + UserInfo userInfo = userService.getUserById(userId); + LoginUser loginUser = new LoginUser(userInfo); + // 生成token + return loginService.createToken(loginUser); + } + /** * 注册 */ - public String register(RegisterBody registerBody) - { + public String register(RegisterBody registerBody) { String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword(); UserInfoEdit sysUser = new UserInfoEdit(); sysUser.setUserName(username); // 验证码开关 boolean captchaEnabled = configService.selectCaptchaEnabled(); - if (captchaEnabled) - { + if (captchaEnabled) { validateCaptcha(username, registerBody.getCode(), registerBody.getUuid()); } - if (StringUtils.isEmpty(username)) - { + if (StringUtils.isEmpty(username)) { msg = "用户名不能为空"; - } - else if (StringUtils.isEmpty(password)) - { + } else if (StringUtils.isEmpty(password)) { msg = "用户密码不能为空"; - } - else if (username.length() < UserConstants.USERNAME_MIN_LENGTH - || username.length() > UserConstants.USERNAME_MAX_LENGTH) - { + } else if (username.length() < UserConstants.USERNAME_MIN_LENGTH + || username.length() > UserConstants.USERNAME_MAX_LENGTH) { msg = "账户长度必须在2到20个字符之间"; - } - else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH - || password.length() > UserConstants.PASSWORD_MAX_LENGTH) - { + } else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH + || password.length() > UserConstants.PASSWORD_MAX_LENGTH) { msg = "密码长度必须在5到20个字符之间"; - } - else if (!userService.checkUserNameUnique(sysUser)) - { + } else if (!userService.checkUserNameUnique(sysUser)) { msg = "保存用户'" + username + "'失败,注册账号已存在"; - } - else - { + } else { sysUser.setNickName(username); sysUser.setPassword(SecurityUtils.encryptPassword(password)); boolean regFlag = false; try { //TODO USER userService.createUser(sysUser); - }catch (Exception e){ - log.error("用户注册失败",e); + } catch (Exception e) { + log.error("用户注册失败", e); } - if (!regFlag) - { + if (!regFlag) { msg = "注册失败,请联系系统管理人员"; - } - else - { + } else { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"))); } } @@ -101,23 +133,65 @@ public class SysRegisterService /** * 校验验证码 - * + * * @param username 用户名 - * @param code 验证码 - * @param uuid 唯一标识 + * @param code 验证码 + * @param uuid 唯一标识 * @return 结果 */ - public void validateCaptcha(String username, String code, String uuid) - { + public void validateCaptcha(String username, String code, String uuid) { String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); String captcha = redisCache.getCacheObject(verifyKey); redisCache.deleteObject(verifyKey); - if (captcha == null) - { + if (captcha == null) { throw new CaptchaExpireException(); } - if (!code.equalsIgnoreCase(captcha)) - { + if (!code.equalsIgnoreCase(captcha)) { + throw new CaptchaException(); + } + } + + /** + * 发送登录/注册短信验证码 + * + * @param phoneNumber 电话号码 + * @param code 图形验证码code + * @param uuid 图形验证码uuid + */ + public void sendSmsVerificationCode(String phoneNumber, String code, String uuid) { + validateCaptcha(null, code, uuid); + sendSmsVerificationCode(phoneNumber); + } + + /** + * 发送登录/注册短信验证码 + * + * @param phoneNumber 电话号码 + */ + public void sendSmsVerificationCode(String phoneNumber) { + String code = RandomUtil.randomNumbers(6); + boolean success = smsClient.sendVerificationCode(phoneNumber, code); + if (success) { + String rk = CacheConstants.SMS_LOGIN_CAPTCHA_CODE_KEY + phoneNumber; + redisCache.setCacheObject(rk, code, 5, TimeUnit.MINUTES); + } + } + + /** + * 验证登录/注册短信验证码 + * + * @param phoneNumber 电话号码 + * @param code 验证码 + * @return + */ + public void validateSmsVerificationCode(String phoneNumber, String code) { + String rk = CacheConstants.SMS_LOGIN_CAPTCHA_CODE_KEY + phoneNumber; + String cacheCode = redisCache.getCacheObject(rk); + if (cacheCode == null) { + throw new CaptchaExpireException(); + } + redisCache.deleteObject(rk); + if (!StrUtil.equals(cacheCode, code)) { throw new CaptchaException(); } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java index e62588562..1ce2d3485 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java @@ -6,10 +6,12 @@ import com.ruoyi.common.enums.UserStatus; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; import com.ruoyi.system.service.ISysUserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -51,7 +53,10 @@ public class UserDetailsServiceImpl implements UserDetailsService throw new ServiceException(MessageUtils.message("user.blocked")); } - passwordService.validate(user); + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + passwordService.validate(user, password); return createLoginUser(user); } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java index bee5f8731..44fed1e16 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysUserMapper.java @@ -33,6 +33,14 @@ public interface SysUserMapper extends BaseMapper { */ UserInfo getUserInfoByUsername(@Param("userName") String userName); + /** + * 根据手机号获取用户 + * + * @param phoneNumber + * @return + */ + UserInfo getUserInfoByPhoneNumber(@Param("phoneNumber") String phoneNumber); + /** * 用户列表 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java index 6b7f4c929..e3c199d0f 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java @@ -31,6 +31,14 @@ public interface ISysUserService { */ UserInfo getUserByUsername(String userName); + /** + * 获取用户信息 + * + * @param phoneNumber + * @return + */ + UserInfo getUserByPhoneNumber(String phoneNumber); + /** * 获取用户基本信息 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java index 3ccddc874..06ba0fb71 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -69,6 +69,13 @@ public class SysUserServiceImpl implements ISysUserService { return userInfo; } + @Override + public UserInfo getUserByPhoneNumber(String phoneNumber) { + UserInfo userInfo = userMapper.getUserInfoByPhoneNumber(phoneNumber); + fillMenus(userInfo); + return userInfo; + } + @Override public SysUser getBaseUser(Long userId) { return userMapper.selectById(userId); @@ -451,12 +458,15 @@ public class SysUserServiceImpl implements ISysUserService { Assert.notNull(user); Assert.notEmpty(user.getUserName(), "用户名称不能为空"); Assert.notEmpty(user.getNickName(), "用户昵称不能为空"); - Assert.isFalse(checkUserNameUnique(user), "用户名称已被注册"); + if (StrUtil.isNotEmpty(user.getPhonenumber())) { + Assert.isTrue(checkPhoneUnique(user), "手机号已被注册"); + } + Assert.isTrue(checkUserNameUnique(user), "用户名称已被注册"); if (StrUtil.isNotEmpty(user.getEmail())) { - Assert.isFalse(checkEmailUnique(user), "邮箱已被注册"); + Assert.isTrue(checkEmailUnique(user), "邮箱已被注册"); } if (StrUtil.isNotEmpty(user.getPhonenumber())) { - Assert.isFalse(checkPhoneUnique(user), "手机号已被注册"); + Assert.isTrue(checkPhoneUnique(user), "手机号已被注册"); } } diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml index 6b28e415e..60e553f4d 100644 --- a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -172,6 +172,47 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" su.del_flag = '0' AND su.user_name = #{userName} + + + + \ No newline at end of file