AuthController.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. package com.jzg.controller;
  2. import cn.hutool.core.util.HexUtil;
  3. import cn.hutool.crypto.BCUtil;
  4. import cn.hutool.crypto.SmUtil;
  5. import cn.hutool.crypto.asymmetric.SM2;
  6. import cn.hutool.json.JSONUtil;
  7. import com.alibaba.fastjson.JSONObject;
  8. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  9. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  10. import com.jzg.commons.annotation.IgnoreTenant;
  11. import com.jzg.commons.constants.EncryptionConstants;
  12. import com.jzg.commons.entity.po.SysUserJzgApp;
  13. import com.jzg.commons.request.RequestSingleParam;
  14. import com.jzg.commons.util.AssertionUtils;
  15. import com.jzg.commons.util.MinioUtils;
  16. import com.jzg.commons.util.PasswordUtils;
  17. import com.jzg.config.JzgTokenService;
  18. import com.jzg.config.JzgUserDetailsService;
  19. import com.jzg.config.JzgUsernamePasswordAuthenticationToken;
  20. import com.jzg.commons.constants.RedisKeyConstants;
  21. import com.jzg.commons.core.base.BaseController;
  22. import com.jzg.commons.core.page.HttpResult;
  23. import com.jzg.entity.RedisToken;
  24. import com.jzg.entity.SysUploadFiles;
  25. import com.jzg.entity.SysUser;
  26. import com.jzg.entity.vo.*;
  27. import com.jzg.commons.exception.SystemException;
  28. import com.jzg.handler.JzgAuthenticationSuccessHandler;
  29. import com.jzg.mapper.SysUploadFilesMapper;
  30. import com.jzg.mapper.SysUserJzgAppMapper;
  31. import com.jzg.service.AlipayService;
  32. import com.jzg.service.SysUserService;
  33. import com.jzg.commons.util.ImageUtil;
  34. import com.jzg.service.WechatService;
  35. import com.wf.captcha.SpecCaptcha;
  36. import com.wf.captcha.base.Captcha;
  37. import io.swagger.v3.oas.annotations.Operation;
  38. import io.swagger.v3.oas.annotations.responses.ApiResponse;
  39. import io.swagger.v3.oas.annotations.tags.Tag;
  40. import jakarta.servlet.ServletException;
  41. import jakarta.servlet.ServletOutputStream;
  42. import jakarta.servlet.http.HttpServletRequest;
  43. import jakarta.servlet.http.HttpServletResponse;
  44. import lombok.extern.slf4j.Slf4j;
  45. import org.apache.commons.lang3.StringUtils;
  46. import org.apache.tomcat.util.http.fileupload.IOUtils;
  47. import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
  48. import org.redisson.api.RedissonClient;
  49. import org.springframework.beans.factory.annotation.Autowired;
  50. import org.springframework.beans.factory.annotation.Value;
  51. import org.springframework.security.authentication.AuthenticationManager;
  52. import org.springframework.security.core.Authentication;
  53. import org.springframework.security.core.context.SecurityContextHolder;
  54. import org.springframework.web.bind.annotation.*;
  55. import org.springframework.web.context.request.RequestContextHolder;
  56. import org.springframework.web.context.request.ServletRequestAttributes;
  57. import javax.imageio.ImageIO;
  58. import java.awt.image.BufferedImage;
  59. import java.io.IOException;
  60. import java.security.*;
  61. import java.util.List;
  62. import java.util.Objects;
  63. import java.util.concurrent.TimeUnit;
  64. import java.util.regex.Pattern;
  65. /**
  66. * 授权接口
  67. */
  68. @Tag(name = "授权接口")
  69. @RestController
  70. @RequestMapping("/auth")
  71. @Slf4j
  72. public class AuthController extends BaseController {
  73. @Value("${minio.bucket-name}")
  74. private String bucketName;
  75. @Autowired
  76. AuthenticationManager authenticationManager;
  77. @Autowired
  78. SysUserService sysUserService;
  79. @Autowired
  80. JzgUserDetailsService jzgUserDetailsService;
  81. @Autowired
  82. private JzgTokenService tokenStore;
  83. @Autowired
  84. RedissonClient redissonClient;
  85. @Autowired
  86. SysUploadFilesMapper sysUploadFilesMapper;
  87. @Autowired
  88. private WechatService wechatService;
  89. @Autowired
  90. private AlipayService alipayService;
  91. @Autowired
  92. private SysUserJzgAppMapper sysUserJzgAppMapper;
  93. /**
  94. * 登录接口
  95. *
  96. * * 用户名/手机号+密码登录
  97. * * 请求方式/路径:POST /auth/login
  98. * * 入参:LoginFormVo(JSON Body)
  99. * * 返回:无(直接通过过滤器向响应写入登录结果与令牌)
  100. * * 注意:成功后在响应中返回认证信息,方法本身不返回 HttpResult
  101. *
  102. * @param loginFormVo
  103. * @throws ServletException
  104. * @throws IOException
  105. */
  106. @Operation(
  107. summary = "登录接口",
  108. description = "登录接口",
  109. responses = {
  110. @ApiResponse(responseCode = "200", description = "登录成功"),
  111. @ApiResponse(responseCode = "403", description = "登录失败")
  112. })
  113. @PostMapping("/login")
  114. @IgnoreTenant
  115. public void login(@RequestBody LoginFormVo loginFormVo) throws ServletException, IOException {
  116. log.info("登录接口。loginFormVo=[{}]", JSONUtil.toJsonStr(loginFormVo));
  117. HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
  118. HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
  119. JzgUsernamePasswordAuthenticationToken authentication = new JzgUsernamePasswordAuthenticationToken(loginFormVo);
  120. Authentication authenticate = authenticationManager.authenticate(authentication);
  121. SecurityContextHolder.getContext().setAuthentication(authenticate);
  122. JzgAuthenticationSuccessHandler successHandler = new JzgAuthenticationSuccessHandler(tokenStore);
  123. successHandler.onAuthenticationSuccess(request, response, authenticate);
  124. }
  125. /**
  126. * 获取图形验证码(按用户ID)
  127. * 请求方式/路径:GET /auth/captchaById
  128. * 入参:captchaId(Query 参数)
  129. * 返回:直接向响应输出验证码图片(二进制)
  130. * 注意:验证码文本存入 Redis,键:VERIFICATION_CODE + captchaId
  131. */
  132. @GetMapping("captchaById")
  133. @Operation(summary = "获取验证码通过用户ID", description = "获取验证码通过用户ID")
  134. public void captchaByIdNew(HttpServletResponse response, String captchaId) throws Exception {
  135. response.setHeader("Cache-Control", "no-store, no-cache");
  136. response.setContentType("image/jpeg");
  137. String text;
  138. String imageId = null;
  139. if (Objects.isNull(redissonClient.getBucket(RedisKeyConstants.VERIFICATION_CODE + captchaId))) {
  140. imageId = redissonClient.getBucket(RedisKeyConstants.VERIFICATION_CODE + captchaId).get().toString();
  141. }
  142. log.info("imageId:{}", imageId);
  143. Captcha captcha = new SpecCaptcha();
  144. // 重复验证码问题
  145. if (StringUtils.isNotEmpty(imageId)) {
  146. text = imageId;
  147. } else {
  148. // 生成文字验证码
  149. text = captcha.text();
  150. }
  151. log.info("text:{}", text);
  152. // 生成图片验证码
  153. BufferedImage image = ImageUtil.base64ToBufferedImage(captcha.toBase64());
  154. if (StringUtils.isEmpty(captchaId)) {
  155. throw new SystemException("captchaId 必传");
  156. }
  157. // 保存到验证码到 redis
  158. redissonClient.getBucket(RedisKeyConstants.VERIFICATION_CODE + captchaId).set(text, 24, TimeUnit.HOURS);
  159. ServletOutputStream out = response.getOutputStream();
  160. ImageIO.write(image, "jpg", out);
  161. IOUtils.closeQuietly(out);
  162. }
  163. /**
  164. * 发送短信验证码(注册/登录)
  165. * 请求方式/路径:GET /auth/sendSmsCode
  166. * 入参:mobile(Query 参数,11位手机号)
  167. * 返回:HttpResult 文本提示
  168. * 注意:验证码暂固定为 123456,存入 Redis,120 秒过期
  169. */
  170. @GetMapping("sendSmsCode")
  171. @Operation(summary = "发送短信验证码")
  172. public HttpResult sendSmsCode(String mobile) {
  173. if (mobile.isEmpty() && mobile.length() != 11) {
  174. throw new SystemException("手机号格式错误");
  175. }
  176. //存储短信验证码
  177. // String code = String.format("%06d", new Random().nextInt(1000000));
  178. String code = "123456";
  179. redissonClient.getBucket(RedisKeyConstants.SMS_CODE + mobile).set(code, 120, TimeUnit.SECONDS);
  180. //调取三方发送短信接口
  181. return HttpResult.ok("验证码发送成功");
  182. }
  183. /**
  184. * 发送更换手机号的短信验证码
  185. * 请求方式/路径:GET /auth/sendSmsCodeMobile
  186. * 入参:mobile(Query 参数,11位手机号)
  187. * 返回:HttpResult 文本提示
  188. * 注意:验证码暂固定为 123456,存入 Redis,120 秒过期,键前缀为 SMS_REPLACE_MOBILE
  189. */
  190. @GetMapping("sendSmsCodeMobile")
  191. @Operation(summary = "发送更换手机短信验证码")
  192. public HttpResult sendSmsCodeMobile(String mobile) {
  193. if (mobile.isEmpty() && mobile.length() != 11) {
  194. throw new SystemException("手机号格式错误");
  195. }
  196. //存储短信验证码
  197. // String code = String.format("%06d", new Random().nextInt(1000000));
  198. String code = "123456";
  199. redissonClient.getBucket(RedisKeyConstants.SMS_REPLACE_MOBILE + mobile).set(code, 120, TimeUnit.SECONDS);
  200. //调取三方发送短信接口
  201. return HttpResult.ok("验证码发送成功");
  202. }
  203. @PostMapping("modifyPass")
  204. @Operation(summary = "修改密码")
  205. /**
  206. * 修改当前登录用户密码
  207. * 请求方式/路径:POST /auth/modifyPass
  208. * 入参:ModifyPassWordFormVo(JSON Body)
  209. * 返回:HttpResult 文本提示
  210. * 校验:密码需 8-20 位且包含字母与数字;需校验旧密码
  211. */
  212. public HttpResult modifyPass(@RequestBody ModifyPassWordFormVo modifyPassWordFormVo) {
  213. String userName = getUserName();
  214. SysUser sysUser = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
  215. .eq(SysUser::getUsername, userName)
  216. .or()
  217. .eq(SysUser::getMobile, userName));
  218. //检验密码
  219. String passwordPattern = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,20}$";
  220. Pattern pattern = Pattern.compile(passwordPattern);
  221. if (!pattern.matcher(modifyPassWordFormVo.getNewPassWord()).matches()) {
  222. throw new SystemException("密码格式错误");
  223. }
  224. //如果旧密码一致更改密码
  225. if (!PasswordUtils.encode(modifyPassWordFormVo.getOldPassWord(), sysUser.getSalt()).equals(sysUser.getPassword())) {
  226. throw new SystemException("旧密码错误");
  227. }
  228. sysUser.setPassword(PasswordUtils.encode(modifyPassWordFormVo.getNewPassWord(), sysUser.getSalt()));
  229. return sysUserService.updateById(sysUser) ? HttpResult.ok("密码修改成功") : HttpResult.error("密码修改失败");
  230. }
  231. /**
  232. * 用户注册
  233. * 请求方式/路径:POST /auth/register
  234. * 入参:AppRegisterFormVo(JSON Body)
  235. * 返回:HttpResult 文本提示
  236. * 校验:手机号未注册;校验图形验证码(可选短信验证码)
  237. */
  238. @PostMapping("register")
  239. @Operation(summary = "用户注册")
  240. public HttpResult register(@RequestBody AppRegisterFormVo appRegisterFormVo) {
  241. SysUser userById = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getMobile, appRegisterFormVo.getMobile()));
  242. AssertionUtils.isFail(Objects.nonNull(userById), "用户已注册,请直接登录");
  243. sysUserService.validateCode(appRegisterFormVo.getCaptchaId(), appRegisterFormVo.getCaptcha());
  244. // sysUserService.validateSmsCode(appRegisterFormVo.getMobile(), appRegisterFormVo.getSmsCode());
  245. boolean register = sysUserService.register(appRegisterFormVo);
  246. return register ? HttpResult.ok("注册成功") : HttpResult.error("注册失败");
  247. }
  248. @PostMapping("forgotPassword")
  249. @Operation(summary = "忘记密码")
  250. /**
  251. * 忘记密码重设
  252. * 请求方式/路径:POST /auth/forgotPassword
  253. * 入参:ForgotPasswordFormVo(JSON Body)
  254. * 返回:HttpResult 文本提示
  255. * 校验:手机号格式;短信验证码
  256. */
  257. public HttpResult forgotPassword(@RequestBody ForgotPasswordFormVo forgotPasswordFormVo) {
  258. if (forgotPasswordFormVo.getMobile().isEmpty() && forgotPasswordFormVo.getMobile().length() != 11) {
  259. throw new SystemException("手机号格式错误");
  260. }
  261. SysUser userById = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
  262. .eq(SysUser::getMobile, forgotPasswordFormVo.getMobile()));
  263. AssertionUtils.isFail(Objects.isNull(userById), "用户名不存在");
  264. //校验短信验证码
  265. sysUserService.validateSmsCode(forgotPasswordFormVo.getMobile(), forgotPasswordFormVo.getSmsCode());
  266. boolean result = sysUserService.forgotPassword(forgotPasswordFormVo);
  267. return result ? HttpResult.ok("修改密码成功") : HttpResult.error("修改密码失败");
  268. }
  269. @PostMapping("logout")
  270. @Operation(summary = "退出登录")
  271. /**
  272. * 退出登录
  273. * 请求方式/路径:POST /auth/logout
  274. * 入参:无(从请求头读取 Authorization)
  275. * 返回:HttpResult 文本提示
  276. * 注意:删除 Redis 中对应 token
  277. */
  278. public HttpResult logout() {
  279. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  280. HttpServletRequest request = attributes.getRequest();
  281. String authorizationHeader = request.getHeader("Authorization");
  282. boolean delete = redissonClient.getBucket(authorizationHeader).delete();
  283. return delete ? HttpResult.ok("退出登录成功") : HttpResult.error("退出登录失败");
  284. }
  285. @PostMapping("cancel")
  286. @Operation(summary = "注销账号")
  287. /**
  288. * 注销账号(物理删除 APP 侧账号并清空授权)
  289. * 请求方式/路径:POST /auth/cancel
  290. * 入参:AppCancelFormVo(JSON Body)
  291. * 返回:HttpResult 文本提示
  292. * 注意:校验短信验证码;删除 Redis token
  293. */
  294. public HttpResult cancel(@RequestBody AppCancelFormVo appCancelFormVo) {
  295. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  296. HttpServletRequest request = attributes.getRequest();
  297. String authorizationHeader = request.getHeader("Authorization");
  298. // 后面加余额校验不能大于0
  299. // 校验短信验证码
  300. sysUserService.validateSmsCode(appCancelFormVo.getMobile(), appCancelFormVo.getSmsCode());
  301. // 物理删除app账号,清空授权登录信息
  302. SysUser userById = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
  303. .eq(SysUser::getMobile, appCancelFormVo.getMobile()));
  304. AssertionUtils.isFail(Objects.isNull(userById), "用户名不存在");
  305. SysUserJzgApp appUser = sysUserJzgAppMapper.selectOne(new LambdaQueryWrapper<SysUserJzgApp>()
  306. .eq(SysUserJzgApp::getUserId, userById.getId()));
  307. appUser.setIsDelete(1);
  308. appUser.setAlipay(null);
  309. appUser.setOpenId(null);
  310. sysUserJzgAppMapper.updateById(appUser);
  311. boolean delete = redissonClient.getBucket(authorizationHeader).delete();
  312. return delete ? HttpResult.ok("注销账号成功") : HttpResult.error("注销账号失败");
  313. }
  314. @PostMapping("changeHeadSculpture")
  315. @Operation(summary = "修改头像")
  316. /**
  317. * 修改头像
  318. * 请求方式/路径:POST /auth/changeHeadSculpture
  319. * 入参:id(表单或查询参数,上传文件记录ID)
  320. * 返回:HttpResult<String>(头像访问的预签名 URL)
  321. */
  322. public HttpResult<String> changeHeadSculpture(@RequestSingleParam(value = "id") String id) {
  323. String userName = getUserName();
  324. SysUser sysUser = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
  325. .eq(SysUser::getMobile, userName));
  326. sysUser.setHeadSculpture(id);
  327. SysUploadFiles sysUploadFiles = sysUploadFilesMapper.selectById(id);
  328. String jzgTouXiang = MinioUtils.getPresignedObjectUrl(bucketName, sysUploadFiles.getFileName());
  329. return sysUserService.updateById(sysUser) ? HttpResult.ok(jzgTouXiang) : HttpResult.error("头像修改失败");
  330. }
  331. @PostMapping("getUsers")
  332. @Operation(summary = "用户列表")
  333. /**
  334. * 获取用户列表(分页:当前固定每页10条,第1页)
  335. * 请求方式/路径:POST /auth/getUsers
  336. * 入参:无
  337. * 返回:HttpResult(分页数据)
  338. */
  339. public HttpResult getUsers() {
  340. Page page = new Page();
  341. page.setSize(10);
  342. page.setPages(1);
  343. return HttpResult.ok(sysUserService.page(page));
  344. }
  345. /**
  346. * 获取当前登录用户的权限标识集合
  347. * 请求方式/路径:GET /auth/getUserPer
  348. * 入参:无
  349. * 返回:HttpResult<List<String>>(权限标识列表)
  350. */
  351. @Operation(summary = "获取用户权限标识")
  352. @GetMapping("/getUserPer")
  353. public HttpResult<List<String>> getUserPer() {
  354. List<String> userPerms = getUserPerms();
  355. return HttpResult.ok(userPerms);
  356. }
  357. @Operation(summary = "获取个人信息")
  358. @GetMapping("/getUserInfo")
  359. /**
  360. * 获取当前登录用户的个人信息
  361. * 请求方式/路径:GET /auth/getUserInfo
  362. * 入参:无
  363. * 返回:HttpResult<RedisToken>(从 token 解析的用户信息)
  364. */
  365. public HttpResult<RedisToken> getUserInfo() {
  366. String userTokenString = getUserTokenString();
  367. return HttpResult.ok(JSONObject.parseObject(userTokenString, RedisToken.class));
  368. }
  369. @Operation(summary = "公钥交换")
  370. @PostMapping("/publicKeyExchange")
  371. /**
  372. * 公钥交换(SM2):保存前端公钥并返回后端公钥
  373. * 请求方式/路径:POST /auth/publicKeyExchange
  374. * 入参:publicKey(表单或查询参数,前端 SM2 公钥)
  375. * 返回:HttpResult(data 为后端 SM2 公钥 Hex)
  376. * 注意:后端生成的私钥与前端公钥一起存入 Redis 以便后续加解密
  377. */
  378. public HttpResult publicKeyExchange(@RequestSingleParam("publicKey") String publicKey) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
  379. //redis 存储公钥
  380. JSONObject jsonObject = new JSONObject();
  381. jsonObject.put("vueSm2PublicKey", publicKey);
  382. //后端生成的SM2私钥
  383. SM2 sm2 = SmUtil.sm2();
  384. String sm2PrivateKeyHex = HexUtil.encodeHexStr(BCUtil.encodeECPrivateKey(sm2.getPrivateKey()));
  385. if (sm2PrivateKeyHex.length() > 64) {
  386. sm2PrivateKeyHex = sm2PrivateKeyHex.substring(2, sm2PrivateKeyHex.length());
  387. }
  388. String sm2PublicKeyHex = HexUtil.encodeHexStr(((BCECPublicKey) sm2.getPublicKey()).getQ().getEncoded(false));
  389. jsonObject.put("javaSm2PrivateKey", sm2PrivateKeyHex);
  390. redissonClient.getBucket(EncryptionConstants.SM2_PUBLIC_KEY_REDIS + "admin").set(jsonObject.toJSONString());
  391. System.out.printf("SM2公钥:" + sm2PublicKeyHex + "\n");
  392. System.out.printf("SM2私钥:" + sm2PrivateKeyHex + "\n");
  393. return HttpResult.ok("success", sm2PublicKeyHex);
  394. }
  395. }