| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- 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<SysUser>()
- .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<SysUser>().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<SysUser>()
- .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<SysUser>()
- .eq(SysUser::getMobile, appCancelFormVo.getMobile()));
- AssertionUtils.isFail(Objects.isNull(userById), "用户名不存在");
- SysUserJzgApp appUser = sysUserJzgAppMapper.selectOne(new LambdaQueryWrapper<SysUserJzgApp>()
- .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<String>(头像访问的预签名 URL)
- */
- public HttpResult<String> changeHeadSculpture(@RequestSingleParam(value = "id") String id) {
- String userName = getUserName();
- SysUser sysUser = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
- .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<List<String>>(权限标识列表)
- */
- @Operation(summary = "获取用户权限标识")
- @GetMapping("/getUserPer")
- public HttpResult<List<String>> getUserPer() {
- List<String> userPerms = getUserPerms();
- return HttpResult.ok(userPerms);
- }
- @Operation(summary = "获取个人信息")
- @GetMapping("/getUserInfo")
- /**
- * 获取当前登录用户的个人信息
- * 请求方式/路径:GET /auth/getUserInfo
- * 入参:无
- * 返回:HttpResult<RedisToken>(从 token 解析的用户信息)
- */
- public HttpResult<RedisToken> 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);
- }
- }
|