package com.jzg.controller; import cn.hutool.core.util.HexUtil; import cn.hutool.crypto.BCUtil; import cn.hutool.crypto.SmUtil; import cn.hutool.crypto.asymmetric.SM2; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.jzg.commons.annotation.IgnoreTenant; import com.jzg.commons.constants.EncryptionConstants; import com.jzg.commons.entity.po.SysUserJzgApp; import com.jzg.commons.request.RequestSingleParam; import com.jzg.commons.util.AssertionUtils; import com.jzg.commons.util.MinioUtils; import com.jzg.commons.util.PasswordUtils; import com.jzg.config.JzgTokenService; import com.jzg.config.JzgUserDetailsService; import com.jzg.config.JzgUsernamePasswordAuthenticationToken; import com.jzg.commons.constants.RedisKeyConstants; import com.jzg.commons.core.base.BaseController; import com.jzg.commons.core.page.HttpResult; import com.jzg.entity.RedisToken; import com.jzg.entity.SysUploadFiles; import com.jzg.entity.SysUser; import com.jzg.entity.vo.*; import com.jzg.commons.exception.SystemException; import com.jzg.handler.JzgAuthenticationSuccessHandler; import com.jzg.mapper.SysUploadFilesMapper; import com.jzg.mapper.SysUserJzgAppMapper; import com.jzg.service.AlipayService; import com.jzg.service.SysUserService; import com.jzg.commons.util.ImageUtil; import com.jzg.service.WechatService; import com.wf.captcha.SpecCaptcha; import com.wf.captcha.base.Captcha; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.ServletException; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.tomcat.util.http.fileupload.IOUtils; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.security.*; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; /** * 授权接口 */ @Tag(name = "授权接口") @RestController @RequestMapping("/auth") @Slf4j public class AuthController extends BaseController { @Value("${minio.bucket-name}") private String bucketName; @Autowired AuthenticationManager authenticationManager; @Autowired SysUserService sysUserService; @Autowired JzgUserDetailsService jzgUserDetailsService; @Autowired private JzgTokenService tokenStore; @Autowired RedissonClient redissonClient; @Autowired SysUploadFilesMapper sysUploadFilesMapper; @Autowired private WechatService wechatService; @Autowired private AlipayService alipayService; @Autowired private SysUserJzgAppMapper sysUserJzgAppMapper; /** * 登录接口 * * * 用户名/手机号+密码登录 * * 请求方式/路径:POST /auth/login * * 入参:LoginFormVo(JSON Body) * * 返回:无(直接通过过滤器向响应写入登录结果与令牌) * * 注意:成功后在响应中返回认证信息,方法本身不返回 HttpResult * * @param loginFormVo * @throws ServletException * @throws IOException */ @Operation( summary = "登录接口", description = "登录接口", responses = { @ApiResponse(responseCode = "200", description = "登录成功"), @ApiResponse(responseCode = "403", description = "登录失败") }) @PostMapping("/login") @IgnoreTenant public void login(@RequestBody LoginFormVo loginFormVo) throws ServletException, IOException { log.info("登录接口。loginFormVo=[{}]", JSONUtil.toJsonStr(loginFormVo)); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); JzgUsernamePasswordAuthenticationToken authentication = new JzgUsernamePasswordAuthenticationToken(loginFormVo); Authentication authenticate = authenticationManager.authenticate(authentication); SecurityContextHolder.getContext().setAuthentication(authenticate); JzgAuthenticationSuccessHandler successHandler = new JzgAuthenticationSuccessHandler(tokenStore); successHandler.onAuthenticationSuccess(request, response, authenticate); } /** * 获取图形验证码(按用户ID) * 请求方式/路径:GET /auth/captchaById * 入参:captchaId(Query 参数) * 返回:直接向响应输出验证码图片(二进制) * 注意:验证码文本存入 Redis,键:VERIFICATION_CODE + captchaId */ @GetMapping("captchaById") @Operation(summary = "获取验证码通过用户ID", description = "获取验证码通过用户ID") public void captchaByIdNew(HttpServletResponse response, String captchaId) throws Exception { response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); String text; String imageId = null; if (Objects.isNull(redissonClient.getBucket(RedisKeyConstants.VERIFICATION_CODE + captchaId))) { imageId = redissonClient.getBucket(RedisKeyConstants.VERIFICATION_CODE + captchaId).get().toString(); } log.info("imageId:{}", imageId); Captcha captcha = new SpecCaptcha(); // 重复验证码问题 if (StringUtils.isNotEmpty(imageId)) { text = imageId; } else { // 生成文字验证码 text = captcha.text(); } log.info("text:{}", text); // 生成图片验证码 BufferedImage image = ImageUtil.base64ToBufferedImage(captcha.toBase64()); if (StringUtils.isEmpty(captchaId)) { throw new SystemException("captchaId 必传"); } // 保存到验证码到 redis redissonClient.getBucket(RedisKeyConstants.VERIFICATION_CODE + captchaId).set(text, 24, TimeUnit.HOURS); ServletOutputStream out = response.getOutputStream(); ImageIO.write(image, "jpg", out); IOUtils.closeQuietly(out); } /** * 发送短信验证码(注册/登录) * 请求方式/路径:GET /auth/sendSmsCode * 入参:mobile(Query 参数,11位手机号) * 返回:HttpResult 文本提示 * 注意:验证码暂固定为 123456,存入 Redis,120 秒过期 */ @GetMapping("sendSmsCode") @Operation(summary = "发送短信验证码") public HttpResult sendSmsCode(String mobile) { if (mobile.isEmpty() && mobile.length() != 11) { throw new SystemException("手机号格式错误"); } //存储短信验证码 // String code = String.format("%06d", new Random().nextInt(1000000)); String code = "123456"; redissonClient.getBucket(RedisKeyConstants.SMS_CODE + mobile).set(code, 120, TimeUnit.SECONDS); //调取三方发送短信接口 return HttpResult.ok("验证码发送成功"); } /** * 发送更换手机号的短信验证码 * 请求方式/路径:GET /auth/sendSmsCodeMobile * 入参:mobile(Query 参数,11位手机号) * 返回:HttpResult 文本提示 * 注意:验证码暂固定为 123456,存入 Redis,120 秒过期,键前缀为 SMS_REPLACE_MOBILE */ @GetMapping("sendSmsCodeMobile") @Operation(summary = "发送更换手机短信验证码") public HttpResult sendSmsCodeMobile(String mobile) { if (mobile.isEmpty() && mobile.length() != 11) { throw new SystemException("手机号格式错误"); } //存储短信验证码 // String code = String.format("%06d", new Random().nextInt(1000000)); String code = "123456"; redissonClient.getBucket(RedisKeyConstants.SMS_REPLACE_MOBILE + mobile).set(code, 120, TimeUnit.SECONDS); //调取三方发送短信接口 return HttpResult.ok("验证码发送成功"); } @PostMapping("modifyPass") @Operation(summary = "修改密码") /** * 修改当前登录用户密码 * 请求方式/路径:POST /auth/modifyPass * 入参:ModifyPassWordFormVo(JSON Body) * 返回:HttpResult 文本提示 * 校验:密码需 8-20 位且包含字母与数字;需校验旧密码 */ public HttpResult modifyPass(@RequestBody ModifyPassWordFormVo modifyPassWordFormVo) { String userName = getUserName(); SysUser sysUser = sysUserService.getOne(new LambdaQueryWrapper() .eq(SysUser::getUsername, userName) .or() .eq(SysUser::getMobile, userName)); //检验密码 String passwordPattern = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,20}$"; Pattern pattern = Pattern.compile(passwordPattern); if (!pattern.matcher(modifyPassWordFormVo.getNewPassWord()).matches()) { throw new SystemException("密码格式错误"); } //如果旧密码一致更改密码 if (!PasswordUtils.encode(modifyPassWordFormVo.getOldPassWord(), sysUser.getSalt()).equals(sysUser.getPassword())) { throw new SystemException("旧密码错误"); } sysUser.setPassword(PasswordUtils.encode(modifyPassWordFormVo.getNewPassWord(), sysUser.getSalt())); return sysUserService.updateById(sysUser) ? HttpResult.ok("密码修改成功") : HttpResult.error("密码修改失败"); } /** * 用户注册 * 请求方式/路径:POST /auth/register * 入参:AppRegisterFormVo(JSON Body) * 返回:HttpResult 文本提示 * 校验:手机号未注册;校验图形验证码(可选短信验证码) */ @PostMapping("register") @Operation(summary = "用户注册") public HttpResult register(@RequestBody AppRegisterFormVo appRegisterFormVo) { SysUser userById = sysUserService.getOne(new LambdaQueryWrapper().eq(SysUser::getMobile, appRegisterFormVo.getMobile())); AssertionUtils.isFail(Objects.nonNull(userById), "用户已注册,请直接登录"); sysUserService.validateCode(appRegisterFormVo.getCaptchaId(), appRegisterFormVo.getCaptcha()); // sysUserService.validateSmsCode(appRegisterFormVo.getMobile(), appRegisterFormVo.getSmsCode()); boolean register = sysUserService.register(appRegisterFormVo); return register ? HttpResult.ok("注册成功") : HttpResult.error("注册失败"); } @PostMapping("forgotPassword") @Operation(summary = "忘记密码") /** * 忘记密码重设 * 请求方式/路径:POST /auth/forgotPassword * 入参:ForgotPasswordFormVo(JSON Body) * 返回:HttpResult 文本提示 * 校验:手机号格式;短信验证码 */ public HttpResult forgotPassword(@RequestBody ForgotPasswordFormVo forgotPasswordFormVo) { if (forgotPasswordFormVo.getMobile().isEmpty() && forgotPasswordFormVo.getMobile().length() != 11) { throw new SystemException("手机号格式错误"); } SysUser userById = sysUserService.getOne(new LambdaQueryWrapper() .eq(SysUser::getMobile, forgotPasswordFormVo.getMobile())); AssertionUtils.isFail(Objects.isNull(userById), "用户名不存在"); //校验短信验证码 sysUserService.validateSmsCode(forgotPasswordFormVo.getMobile(), forgotPasswordFormVo.getSmsCode()); boolean result = sysUserService.forgotPassword(forgotPasswordFormVo); return result ? HttpResult.ok("修改密码成功") : HttpResult.error("修改密码失败"); } @PostMapping("logout") @Operation(summary = "退出登录") /** * 退出登录 * 请求方式/路径:POST /auth/logout * 入参:无(从请求头读取 Authorization) * 返回:HttpResult 文本提示 * 注意:删除 Redis 中对应 token */ public HttpResult logout() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String authorizationHeader = request.getHeader("Authorization"); boolean delete = redissonClient.getBucket(authorizationHeader).delete(); return delete ? HttpResult.ok("退出登录成功") : HttpResult.error("退出登录失败"); } @PostMapping("cancel") @Operation(summary = "注销账号") /** * 注销账号(物理删除 APP 侧账号并清空授权) * 请求方式/路径:POST /auth/cancel * 入参:AppCancelFormVo(JSON Body) * 返回:HttpResult 文本提示 * 注意:校验短信验证码;删除 Redis token */ public HttpResult cancel(@RequestBody AppCancelFormVo appCancelFormVo) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String authorizationHeader = request.getHeader("Authorization"); // 后面加余额校验不能大于0 // 校验短信验证码 sysUserService.validateSmsCode(appCancelFormVo.getMobile(), appCancelFormVo.getSmsCode()); // 物理删除app账号,清空授权登录信息 SysUser userById = sysUserService.getOne(new LambdaQueryWrapper() .eq(SysUser::getMobile, appCancelFormVo.getMobile())); AssertionUtils.isFail(Objects.isNull(userById), "用户名不存在"); SysUserJzgApp appUser = sysUserJzgAppMapper.selectOne(new LambdaQueryWrapper() .eq(SysUserJzgApp::getUserId, userById.getId())); appUser.setIsDelete(1); appUser.setAlipay(null); appUser.setOpenId(null); sysUserJzgAppMapper.updateById(appUser); boolean delete = redissonClient.getBucket(authorizationHeader).delete(); return delete ? HttpResult.ok("注销账号成功") : HttpResult.error("注销账号失败"); } @PostMapping("changeHeadSculpture") @Operation(summary = "修改头像") /** * 修改头像 * 请求方式/路径:POST /auth/changeHeadSculpture * 入参:id(表单或查询参数,上传文件记录ID) * 返回:HttpResult(头像访问的预签名 URL) */ public HttpResult changeHeadSculpture(@RequestSingleParam(value = "id") String id) { String userName = getUserName(); SysUser sysUser = sysUserService.getOne(new LambdaQueryWrapper() .eq(SysUser::getMobile, userName)); sysUser.setHeadSculpture(id); SysUploadFiles sysUploadFiles = sysUploadFilesMapper.selectById(id); String jzgTouXiang = MinioUtils.getPresignedObjectUrl(bucketName, sysUploadFiles.getFileName()); return sysUserService.updateById(sysUser) ? HttpResult.ok(jzgTouXiang) : HttpResult.error("头像修改失败"); } @PostMapping("getUsers") @Operation(summary = "用户列表") /** * 获取用户列表(分页:当前固定每页10条,第1页) * 请求方式/路径:POST /auth/getUsers * 入参:无 * 返回:HttpResult(分页数据) */ public HttpResult getUsers() { Page page = new Page(); page.setSize(10); page.setPages(1); return HttpResult.ok(sysUserService.page(page)); } /** * 获取当前登录用户的权限标识集合 * 请求方式/路径:GET /auth/getUserPer * 入参:无 * 返回:HttpResult>(权限标识列表) */ @Operation(summary = "获取用户权限标识") @GetMapping("/getUserPer") public HttpResult> getUserPer() { List userPerms = getUserPerms(); return HttpResult.ok(userPerms); } @Operation(summary = "获取个人信息") @GetMapping("/getUserInfo") /** * 获取当前登录用户的个人信息 * 请求方式/路径:GET /auth/getUserInfo * 入参:无 * 返回:HttpResult(从 token 解析的用户信息) */ public HttpResult getUserInfo() { String userTokenString = getUserTokenString(); return HttpResult.ok(JSONObject.parseObject(userTokenString, RedisToken.class)); } @Operation(summary = "公钥交换") @PostMapping("/publicKeyExchange") /** * 公钥交换(SM2):保存前端公钥并返回后端公钥 * 请求方式/路径:POST /auth/publicKeyExchange * 入参:publicKey(表单或查询参数,前端 SM2 公钥) * 返回:HttpResult(data 为后端 SM2 公钥 Hex) * 注意:后端生成的私钥与前端公钥一起存入 Redis 以便后续加解密 */ public HttpResult publicKeyExchange(@RequestSingleParam("publicKey") String publicKey) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException { //redis 存储公钥 JSONObject jsonObject = new JSONObject(); jsonObject.put("vueSm2PublicKey", publicKey); //后端生成的SM2私钥 SM2 sm2 = SmUtil.sm2(); String sm2PrivateKeyHex = HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(sm2.getPrivateKey())); if (sm2PrivateKeyHex.length() > 64) { sm2PrivateKeyHex = sm2PrivateKeyHex.substring(2, sm2PrivateKeyHex.length()); } String sm2PublicKeyHex = HexUtil.encodeHexStr(((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(false)); jsonObject.put("javaSm2PrivateKey", sm2PrivateKeyHex); redissonClient.getBucket(EncryptionConstants.SM2_PUBLIC_KEY_REDIS + "admin").set(jsonObject.toJSONString()); System.out.printf("SM2公钥:" + sm2PublicKeyHex + "\n"); System.out.printf("SM2私钥:" + sm2PrivateKeyHex + "\n"); return HttpResult.ok("success", sm2PublicKeyHex); } }