项目搭建
项目初始搭建
创建项目

创建springboot项目 并添加依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.52</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.23</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
server:
port: 8080
spring:
application:
name: algorithm
datasource:
url: jdbc:mysql://localhost:3306/algorithm?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
创建项目结构

根据mybatisX生成代码
选择所有数据库表
按如图配置生成
b3bb986085a7b32aef3a843c75aae471 bukKVLBvJaPqX3x 最后在生成代码文件夹"generator"里把mapper,service,domain文件夹拖入项目文件夹
把resource文件夹里的mapper文件夹删除
Result的封装
在src目录下创建result目录并创建result类
package com.codelong.result; import lombok.Data; import java.io.Serializable; /** * 后端统一返回结果 * @param <T> */ @Data public class Result<T> implements Serializable { private Integer code; //编码:1成功,0和其它数字为失败 private String msg; //错误信息 private T data; //数据 public static <T> Result<T> success() { Result<T> result = new Result<T>(); result.code = 1; return result; } public static <T> Result<T> success(T object) { Result<T> result = new Result<T>(); result.data = object; result.code = 1; return result; } public static <T> Result<T> error(String msg) { Result result = new Result(); result.msg = msg; result.code = 0; return result; } }
登录功能的编写
Jwt令牌相关配置
Jwt工具类java-jwt的导入
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.3.0</version> </dependency>
jwt工具类的编写
package com.codelong.utils; import com.alibaba.fastjson.JSON; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import lombok.extern.slf4j.Slf4j; import java.util.Base64; import java.util.Date; import java.util.HashMap; import java.util.Map; @Slf4j public class JwtUtils { //过期时间 一天 private static final long TOKEN_EXPIRE_TIME = 24 * 60 * 60 * 1000; //私钥,随机的uuid private static final String TOKEN_SECRET = "ca58f51e-05be-61e8-cbe2-33cebd1e69e8"; /** * 生成签名,15分钟过期 * 根据内部改造,支持6中类型,Integer,Long,Boolean,Double,String,Date * * @param map * @return */ public static String sign(Map<String, Object> map) { try { // 设置过期时间 Date date = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME); // 私钥和加密算法 Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); // 设置头部信息 Map<String, Object> header = new HashMap<>(2); header.put("typ", "jwt"); // 返回token字符串 JWTCreator.Builder builder = JWT.create() .withHeader(header) .withIssuedAt(new Date()) //发证时间 .withExpiresAt(date); //过期时间 // .sign(algorithm); //密钥 // map.entrySet().forEach(entry -> builder.withClaim( entry.getKey(),entry.getValue())); map.forEach((key, value) -> { if (value instanceof Integer) { builder.withClaim(key, (Integer) value); } else if (value instanceof Long) { builder.withClaim(key, (Long) value); } else if (value instanceof Boolean) { builder.withClaim(key, (Boolean) value); } else if (value instanceof String) { builder.withClaim(key, String.valueOf(value)); } else if (value instanceof Double) { builder.withClaim(key, (Double) value); } else if (value instanceof Date) { builder.withClaim(key, (Date) value); } }); return builder.sign(algorithm); } catch (Exception e) { log.error(e.getMessage()); return null; } } /** * 生成签名,15分钟过期 * 根据内部改造,支持6中类型,Integer,Long,Boolean,Double,String,Date * * @param o 对象 * @param key 键 * @return 秘钥 */ public static String sign(String key, Object o) { try { // 设置过期时间 Date date = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME); // 私钥和加密算法 Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); // 设置头部信息 Map<String, Object> header = new HashMap<>(2); header.put("typ", "jwt"); // 返回token字符串 JWTCreator.Builder builder = JWT.create() .withHeader(header) .withIssuedAt(new Date()) //发证时间 .withExpiresAt(date) //过期时间 .withClaim(key, JSON.toJSONString(o)); return builder.sign(algorithm); } catch (Exception e) { log.error(e.getMessage()); return null; } } /** * 检验token是否正确 * * @param **token** * @return */ public static boolean verify(String token) { try { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(token); return true; } catch (Exception e) { log.error(e.getMessage()); return false; } } /** * 获取用户自定义Claim集合 * * @param token * @return */ public static Map<String, Claim> getClaims(String token) { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); JWTVerifier verifier = JWT.require(algorithm).build(); Map<String, Claim> jwt = verifier.verify(token).getClaims(); return jwt; } /** * 获取用户自定义根据token和字符串拿到String数据 * * @param token * @return String */ public static String getString(String token, String z) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim(z).asString(); } catch (JWTDecodeException e) { log.error(e.getMessage()); return null; } } /** * 获取用户自定义根据token和字符串拿到对象 * * @param token * @return String */ public static <T> T getObject(String token, String z, Class<T> tClass) { try { DecodedJWT jwt = JWT.decode(token); String o = jwt.getClaim(z).asString(); return JSON.parseObject(o, tClass); } catch (JWTDecodeException e) { log.error(e.getMessage()); return null; } } /** * 获取用户自定义根据token和字符串拿到Integer数据 * * @param token * @return Integer */ public static Integer getInteger(String token, String z) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim(z).asInt(); } catch (JWTDecodeException e) { log.error(e.getMessage()); return null; } } /** * 获取过期时间 * * @param token * @return */ public static Date getExpiresAt(String token) { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); return JWT.require(algorithm).build().verify(token).getExpiresAt(); } /** * 获取jwt发布时间 */ public static Date getIssuedAt(String token) { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); return JWT.require(algorithm).build().verify(token).getIssuedAt(); } /** * 验证token是否失效 * * @param token * @return true:过期 false:没过期 */ public static boolean isExpired(String token) { try { final Date expiration = getExpiresAt(token); return expiration.before(new Date()); } catch (TokenExpiredException e) { log.error(e.getMessage()); return true; } } /** * 直接Base64解密获取header内容 * * @param token * @return */ public static String getHeaderByBase64(String token) { if (StringUtils.isEmpty(token)) { return null; } else { byte[] header_byte = Base64.getDecoder().decode(token.split("\\.")[0]); String header = new String(header_byte); return header; } } /** * 直接Base64解密获取payload内容 * * @param token * @return */ public static String getPayloadByBase64(String token) { if (StringUtils.isEmpty(token)) { return null; } else { byte[] payload_byte = Base64.getDecoder().decode(token.split("\\.")[1]); String payload = new String(payload_byte); return payload; } } }
改写相关配置
- 把时间改为30天
- 随机生成uuid为新的秘钥
controller层代码编写
创建UserController
@RestController @RequestMapping("/user") @Slf4j @RequiredArgsConstructor public class LoginController { private final UserService userService; }
在UserController添加代码
@Operation(summary = "用户登录") @PostMapping("/login") public Result<LoginVO> login(@RequestBody UserDTO userDTO) { LoginVO login = userService.login(userDTO.getUsername(), userDTO.getPassword()); return Result.success(login); }
service层代码编写
创建UserService
创建消息常量类
public class MessageConstant { public static final String PASSWORD_ERROR = "密码错误"; public static final String ACCOUNT_NOT_FOUND = "账号不存在"; public static final String UNKNOWN_ERROR = "未知错误"; public static final String USER_NOT_LOGIN = "用户未登录"; public static final String LOGIN_FAILED = "登录失败"; public static final String UPLOAD_FAILED = "文件上传失败"; public static final String PASSWORD_EDIT_FAILED = "密码修改失败"; }
创建异常信息类
Clip_2024-10-10_19-43-26 编写BaseException
public class BaseException extends RuntimeException { public BaseException() { } public BaseException(String msg) { super(msg); } }
在UserService添加代码
@Slf4j @RequiredArgsConstructor @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { private final UserMapper userMapper; @Override public LoginVO login(String username, String password) { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.lambda().eq(User::getUsername, username); User user = userMapper.selectOne(wrapper); //判断用户是否存在 if (user != null) { String md5Password = DigestUtils.md5DigestAsHex(password.getBytes()); if (user.getStatus().equals(UserStatusConstant.USER_ERROR)) { throw new UserStatusException(MessageConstant.USER_STATUS_ERROR); } if (user.getPassword().equals(md5Password)) { //判断密码是否正确 Map<String, Object> map = new HashMap<>(); map.put("id", user.getId()); map.put("username", user.getUsername()); return LoginVO.builder() .expire(LocalDateTime.now().plusDays(30)) .token(JwtUtils.sign(map)) .avatar(user.getAvatar()) .id(user.getId()) .userName(user.getUsername()) .nickName(user.getNickname()) .email(user.getEmail()) .build(); } else { throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR); } } else { throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND); } } }
intercept的配置
在interceptor文件夹下创建
LoginCheckInterceptor.java
文件@Component @Slf4j public class LoginCheckInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("请求前的登录校验"); String token = request.getHeader("Authorization"); if (JwtUtils.verify(token)) { Map<String, Claim> claims = JwtUtils.getClaims(token); Claim id = claims.get("id"); CurrentIdUtils.setCurrentId(id.asLong()); return true; } response.setStatus(401); return false; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { CurrentIdUtils.removeCurrentId(); } }
然后在config文件夹创建拦截器类
@Configuration @RequiredArgsConstructor @Slf4j public class WebMvcConfig implements WebMvcConfigurer { private final LoginCheckInterceptor loginCheckInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器对象 log.info("注册自定义拦截器"); registry.addInterceptor(loginCheckInterceptor) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/login"); registry.addInterceptor(loginCheckInterceptor) .addPathPatterns("/user/**") .excludePathPatterns("/user/login"); } }
全局异常处理器
添加Handler文件夹并创建GlobalExceptionHandler.java文件
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 捕获业务异常 * * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(BaseException ex) { log.error("异常信息:{}", ex.getMessage()); return Result.error(ex.getMessage()); } /** * 处理SQL异常 * * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) { return Result.error(MessageConstant.SQL_ERROR); } /** * 处理未知异常 * * @param ex * @return */ @ExceptionHandler(Exception.class) public Result exceptionHandler(Exception ex) { log.error("未知异常:{}", ex.getMessage()); return Result.error(MessageConstant.UNKNOWN_ERROR); } }
接口文档swagger配置
添加依赖
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency>
在yml进行相关配置
# springdoc-openapi项目配置 springdoc: swagger-ui: path: /swagger-ui.html tags-sorter: alpha operations-sorter: alpha api-docs: path: /v3/api-docs group-configs: - group: 'default' paths-to-match: '/**' packages-to-scan: com.codelong.controller //配置扫描目录 # knife4j的增强配置,不需要增强可以不配 knife4j: enable: false production: true #本项目使用apifox作为api管理工具,使用这里默认开启 setting: language: zh_cn
使用
@Tag
和@Operation
标注controller层和相关方法@RestController @RequestMapping("/api/user") @Slf4j @RequiredArgsConstructor @Tag(name = "登录相关接口") public class LoginController { private final UserService userService; @Operation(summary ="用户登录") @PostMapping("/login") public Result login(@RequestBody UserDTO userDTO) { String token = userService.login(userDTO.getUsername(), userDTO.getPassword()); if (token != null) { return Result.success(token); } return Result.error("登陆失败"); } }
跨域请求的配置
在WebMvcconfig
文件下添加如下配置:
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/user/**")
.allowedOrigins("http://localhost:80")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
registry.addMapping("/admin/**")
.allowedOrigins("http://localhost:80")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
log.info("跨域请求服务启用");
}
这样本机的80端口和5173端口就能进行请求了
在调试中发现了一处跨域bug,原因如下:
在使用axios发送请求的时候,加入了自定义请求头headers: { ‘Content-Type’: 'application/json' // 其他需要的头},然后 axios 就会产生options试探请求。
折腾了很久才知道。是配置的拦截器拦截了预检请求(preflight request)。预检请求是一种由浏览器自动发送的、使用OPTIONS方法的HTTP请求,它用于检查实际请求是否可以被服务器接受。
当你的拦截器拦截到这种OPTIONS请求时,如果没有正确地处理(例如,返回正确的CORS头部信息),那么浏览器就会认为服务器是不可连接到的所以阻止实际的请求发送,从而导致你看到的错误Response to preflight request doesn’t pass access control check: It does not have HTTP ok status.
所以我们需要确保拦截器在处理OPTIONS请求时,返回正确的CORS头部信息。要在拦截器中检查请求的HTTP方法,如果是OPTIONS方法,那么就直接返回一个包含正确CORS头部信息的响应。
所以在LoginCheckInterceptor
拦截器配置如下代码:
if (request.getMethod().equals("OPTIONS")) {
response.setHeader("Access-Control-Allow-Origin", "*");//*表示放行所有的源,http://127.0.0.1:5500
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, HEAD, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setStatus(HttpServletResponse.SC_OK);
return false;
}
完整代码如下:
@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getMethod().equals("OPTIONS")) {
response.setHeader("Access-Control-Allow-Origin", "*");//*表示放行所有的源,http://127.0.0.1:5500
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, HEAD, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setStatus(HttpServletResponse.SC_OK);
return false;
}
log.info("请求前的登录校验");
String token = request.getHeader("Authorization");
log.info(token);
if (JwtUtils.verify(token)) {
Map<String, Claim> claims = JwtUtils.getClaims(token);
Claim id = claims.get("id");
CurrentIdUtils.setCurrentId(id.asLong());
log.info("请求通过");
return true;
}
response.setStatus(401);
log.info("未登录用户请求已被拦截");
return false;
}
}