feat: 新增验证码和代码优化重构

This commit is contained in:
haoxr
2023-03-24 22:41:59 +08:00
parent 3aea7729af
commit 20dec09bf5
19 changed files with 403 additions and 22 deletions

View File

@@ -0,0 +1,16 @@
package com.youlai.system.common.constant;
/**
* Redis 缓存常量
*
* @author: haoxr
* @date: 2023/03/24
*/
public interface CacheConstants {
/**
* 验证码缓存前缀
*/
String VERIFY_CODE_CACHE_PREFIX = "AUTH:VERIFY_CODE:";
}

View File

@@ -0,0 +1,16 @@
package com.youlai.system.common.constant;
/**
* Excel 常量
*
* @author: haoxr
* @date: 2023/03/24
*/
public interface ExcelConstants {
/**
* Excel 模板目录
*/
String EXCEL_TEMPLATE_DIR="excel-templates";
}

View File

@@ -23,4 +23,6 @@ public interface SystemConstants {
* 超级管理员角色编码
*/
String ROOT_ROLE_CODE = "ROOT";
}

View File

@@ -6,6 +6,8 @@ import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 响应码枚举
*
* @author haoxr
* @date 2020-06-23
**/
@@ -25,6 +27,10 @@ public enum ResultCode implements IResultCode, Serializable {
USERNAME_OR_PASSWORD_ERROR("A0210", "用户名或密码错误"),
PASSWORD_ENTER_EXCEED_LIMIT("A0211", "用户输入密码次数超限"),
CLIENT_AUTHENTICATION_FAILED("A0212", "客户端认证失败"),
VERIFY_CODE_TIMEOUT("A0213", "验证码已过期"),
VERIFY_CODE_ERROR("A0214", "验证码错误"),
TOKEN_INVALID("A0230", "token无效或已过期"),
TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"),

View File

@@ -1,6 +1,8 @@
package com.youlai.system.controller;
import com.youlai.system.common.result.Result;
import com.youlai.system.framework.easycaptcha.service.EasyCaptchaService;
import com.youlai.system.pojo.dto.CaptchaResult;
import com.youlai.system.pojo.dto.LoginResult;
import com.youlai.system.framework.security.JwtTokenManager;
import io.swagger.v3.oas.annotations.Parameter;
@@ -14,13 +16,14 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@Tag(name = "01.认证管理")
@Tag(name = "01.认证中心")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenManager jwtTokenManager;
private final EasyCaptchaService easyCaptchaService;
@Operation(summary = "登录")
@PostMapping("/login")
@@ -50,4 +53,11 @@ public class AuthController {
return Result.success("注销成功");
}
@Operation(summary = "获取验证码")
@GetMapping("/captcha")
public Result getCaptcha() {
CaptchaResult captcha = easyCaptchaService.getCaptcha();
return Result.success(captcha);
}
}

View File

@@ -45,7 +45,7 @@ public class SysDeptController {
return Result.success(list);
}
@Operation(summary = "获取部门详情", security = {@SecurityRequirement(name = "Authorization")})
@Operation(summary = "获取部门表单数据", security = {@SecurityRequirement(name = "Authorization")})
@GetMapping("/{deptId}/form")
public Result<DeptForm> getDeptForm(
@Parameter(description ="部门ID") @PathVariable Long deptId

View File

@@ -4,6 +4,7 @@ import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.system.common.constant.ExcelConstants;
import com.youlai.system.common.result.PageResult;
import com.youlai.system.common.result.Result;
import com.youlai.system.common.util.ExcelUtils;
@@ -136,7 +137,7 @@ public class SysUserController {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
String fileClassPath = "excel-templates" + File.separator + fileName;
String fileClassPath = ExcelConstants.EXCEL_TEMPLATE_DIR + File.separator + fileName;
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fileClassPath);
ServletOutputStream outputStream = response.getOutputStream();

View File

@@ -0,0 +1,60 @@
package com.youlai.system.framework.easycaptcha.config;
import com.youlai.system.framework.easycaptcha.enums.VerifyCodeTypeEnum;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.awt.*;
/**
* @author: haoxr
* @date: 2023/03/24
*/
@ConfigurationProperties(prefix = "easycaptcha")
@Configuration
@Data
public class EasyCaptchaConfig {
/**
* 验证码类型
*/
private VerifyCodeTypeEnum verifyCodeType = VerifyCodeTypeEnum.ARITHMETIC;
/**
* 验证码缓存过期时间(单位:秒)
*/
private long ttl = 120l;
/**
* 验证码内容长度
*/
private int length = 4;
/**
* 验证码宽度
*/
private int width = 120;
/**
* 验证码高度
*/
private int height = 36;
/**
* 验证码字体
*/
private String fontName = "Verdana";
/**
* 字体风格
*/
private Integer fontStyle = Font.PLAIN;
/**
* 字体大小
*/
private int fontSize = 20;
}

View File

@@ -0,0 +1,28 @@
package com.youlai.system.framework.easycaptcha.enums;
/**
* EasyCaptcha 验证码类型枚举
*
* @author: haoxr
* @date: 2023/03/24
*/
public enum VerifyCodeTypeEnum {
/**
* 算数
*/
ARITHMETIC,
/**
* 中文
*/
CHINESE,
/**
* 中文闪图
*/
CHINESE_GIF,
/**
* 闪图
*/
GIF,
SPEC
}

View File

@@ -0,0 +1,61 @@
package com.youlai.system.framework.easycaptcha.producer;
import com.wf.captcha.*;
import com.wf.captcha.base.Captcha;
import com.youlai.system.framework.easycaptcha.config.EasyCaptchaConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.awt.*;
/**
* 验证码生成器
*
* @author: haoxr
* @date: 2023/03/24
*/
@Component
@RequiredArgsConstructor
public class EasyCaptchaProducer {
private final EasyCaptchaConfig easyCaptchaConfig;
public Captcha getCaptcha() {
Captcha captcha;
int width = easyCaptchaConfig.getWidth();
int height = easyCaptchaConfig.getHeight();
int length = easyCaptchaConfig.getLength();
String fontName = easyCaptchaConfig.getFontName();
switch (easyCaptchaConfig.getVerifyCodeType()) {
case ARITHMETIC:
captcha = new ArithmeticCaptcha(width, height);
//固定设置为两位,图片为算数运算表达式
captcha.setLen(2);
break;
case CHINESE:
captcha = new ChineseCaptcha(width, height);
captcha.setLen(length);
break;
case CHINESE_GIF:
captcha = new ChineseGifCaptcha(width, height);
captcha.setLen(length);
break;
case GIF:
captcha = new GifCaptcha(width, height);//最后一位是位数
captcha.setLen(length);
break;
case SPEC:
captcha = new SpecCaptcha(width, height);
captcha.setLen(length);
break;
default:
throw new RuntimeException("验证码配置信息错误!正确配置查看 VerifyCodeTypeEnum ");
}
captcha.setFont(new Font(fontName, easyCaptchaConfig.getFontStyle(), easyCaptchaConfig.getFontSize()));
return captcha;
}
}

View File

@@ -0,0 +1,55 @@
package com.youlai.system.framework.easycaptcha.service;
import cn.hutool.core.util.IdUtil;
import com.wf.captcha.base.Captcha;
import com.youlai.system.common.constant.CacheConstants;
import com.youlai.system.framework.easycaptcha.config.EasyCaptchaConfig;
import com.youlai.system.framework.easycaptcha.producer.EasyCaptchaProducer;
import com.youlai.system.pojo.dto.CaptchaResult;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* EasyCaptcha 业务类
*
* @author: haoxr
* @date: 2023/03/24
*/
@Service
@RequiredArgsConstructor
public class EasyCaptchaService {
private final EasyCaptchaProducer easyCaptchaProducer;
private final StringRedisTemplate redisTemplate;
private final EasyCaptchaConfig easyCaptchaConfig;
/**
* 获取验证码
*
* @return
*/
public CaptchaResult getCaptcha() {
// 获取验证码
Captcha captcha = easyCaptchaProducer.getCaptcha();
String captchaText = captcha.text(); // 验证码文本
String captchaBase64 = captcha.toBase64(); // 验证码图片Base64字符串
// 验证码文本缓存至Redis用于登录比较
String verifyCodeKey = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(CacheConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey, captchaText,
easyCaptchaConfig.getTtl(), TimeUnit.SECONDS);
CaptchaResult captchaResult = CaptchaResult.builder()
.verifyCodeKey(verifyCodeKey)
.verifyCodeBase64(captchaBase64)
.build();
return captchaResult;
}
}

View File

@@ -1,9 +1,10 @@
package com.youlai.system.config;
package com.youlai.system.framework.security.config;
import com.youlai.system.framework.security.filter.JwtAuthenticationFilter;
import com.youlai.system.framework.security.exception.MyAccessDeniedHandler;
import com.youlai.system.framework.security.exception.MyAuthenticationEntryPoint;
import com.youlai.system.framework.security.JwtTokenManager;
import com.youlai.system.framework.security.filter.VerifyCodeFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -44,6 +45,9 @@ public class SecurityConfig {
.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/api/v1/auth/login").permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
@@ -52,6 +56,9 @@ public class SecurityConfig {
// disable cache
http.headers().cacheControl();
// 验证码校验过滤器
http.addFilterAt(new VerifyCodeFilter(),UsernamePasswordAuthenticationFilter.class);
// JWT 校验过滤器
http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class);
return http.build();
@@ -61,7 +68,7 @@ public class SecurityConfig {
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers(
"/api/v1/auth/login",
"/api/v1/auth/captcha",
"/webjars/**",
"/doc.html",
"/swagger-resources/**",

View File

@@ -2,27 +2,29 @@ package com.youlai.system.framework.security.filter;
import cn.hutool.core.util.StrUtil;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.framework.security.JwtTokenManager;
import com.youlai.system.common.util.ResponseUtils;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import com.youlai.system.framework.security.JwtTokenManager;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* JWT token校验拦截
* JWT 校验过滤
*
* @author haoxr
* @date 2022/10/1
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/api/v1/auth/login", "POST");
private static final String TOKEN_PREFIX = "Bearer ";
private final JwtTokenManager tokenManager;
@@ -32,15 +34,17 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if(HttpMethod.OPTIONS.matches(request.getMethod()) ){
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (DEFAULT_ANT_PATH_REQUEST_MATCHER.matches(request)) {
// 非登录接口放行
chain.doFilter(request, response);
return;
}
String jwt = resolveToken(request);
if (StrUtil.isNotBlank(jwt) && SecurityContextHolder.getContext().getAuthentication() == null) {
try {
// 验证token
// 验证JWT
this.tokenManager.validateToken(jwt);
// JWT验证有效获取Authentication存入Security上下文

View File

@@ -0,0 +1,66 @@
package com.youlai.system.framework.security.filter;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.youlai.system.common.constant.CacheConstants;
import com.youlai.system.common.result.ResultCode;
import com.youlai.system.common.util.ResponseUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* 验证码校验过滤器
*
* @author haoxr
* @date 2022/10/1
*/
public class VerifyCodeFilter extends OncePerRequestFilter {
/**
* 拦截路径
*/
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/api/v1/auth/login", "POST");
public static final String VERIFY_CODE = "verifyCode";
public static final String VERIFY_CODE_KEY = "verifyCodeKey";
RedisTemplate redisTemplate;
public VerifyCodeFilter() {
this.redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if (!DEFAULT_ANT_PATH_REQUEST_MATCHER.matches(request)) {
// 非登录接口放行
chain.doFilter(request, response);
} else {
// 请求中的验证码
String requestVerifyCode = request.getParameter(VERIFY_CODE);
// 缓存中的验证码
String verifyCodeKey = request.getParameter(VERIFY_CODE_KEY);
Object cacheVerifyCode = redisTemplate.opsForValue().get(CacheConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey);
if (cacheVerifyCode == null) {
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
} else {
// 验证码比对
if (StrUtil.equals(requestVerifyCode, Convert.toStr(cacheVerifyCode))) {
chain.doFilter(request, response);
} else {
ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
package com.youlai.system.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 验证码响应对象
*
* @author: haoxr
* @date: 2023/03/24
*/
@Schema(description ="验证码响应对象")
@Builder
@Data
public class CaptchaResult {
@Schema(description = "验证码缓存key")
private String verifyCodeKey;
@Schema(description = "验证码图片Base64字符串")
private String verifyCodeBase64;
}