项目结构:
ruoyi‑common:存放注解、切面ruoyi‑gateway:yml 配置放行接口ruoyi‑modules‑system:部门 Controller
1、自定义注解 CheckKeyId.java
路径:ruoyi‑common/src/main/java/com/ruoyi/common/annotation/CheckKeyId.java
package com.ruoyi.common.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckKeyId {
}
2、AOP 切面 CheckKeyIdAspect.java
路径:ruoyi‑common/src/main/java/com/ruoyi/common/aspect/CheckKeyIdAspect.java
package com.ruoyi.common.aspect;
import com.ruoyi.common.annotation.CheckKeyId;
import com.ruoyi.common.core.domain.AjaxResult;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
public class CheckKeyIdAspect {
/**
* 切点:拦截标记@CheckKeyId的方法
*/
@Pointcut("@annotation(com.ruoyi.common.annotation.CheckKeyId)")
public void pointCut() {
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return AjaxResult.error("无法获取请求上下文");
}
HttpServletRequest request = attributes.getRequest();
// 获取请求头keyid
String keyid = request.getHeader("keyid");
final String validKey = "12345678";
if (keyid == null || !validKey.equals(keyid.trim())) {
return AjaxResult.error(401, "header keyid校验失败");
}
// 校验通过,执行controller原方法
return joinPoint.proceed();
}
}
3、SysDeptController 新增部门接口
路径:ruoyi‑modules‑system/src/main/java/com/ruoyi/system/controller/SysDeptController.java
import com.ruoyi.common.annotation.CheckKeyId;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.service.ISysDeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/system/dept")
public class SysDeptController {
@Autowired
private ISysDeptService deptService;
/**
* 新增部门(外部API,跳过登录,使用header keyid鉴权)
*/
// @RequiresPermissions("system:dept:add") 注释掉权限注解
@CheckKeyId
@Log(title = "部门管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDept dept)
{
if (!deptService.checkDeptNameUnique(dept))
{
return AjaxResult.error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
// 兼容未登录场景,防止SecurityUtils空指针
String username = SecurityUtils.getUsername();
dept.setCreateBy(username == null ? "api_external" : username);
return AjaxResult.toAjax(deptService.insertDept(dept));
}
}
4、网关 ruoyi‑gateway application.yml 配置【微服务必配!】
路径:ruoyi‑gateway/src/main/resources/application.yml
sa‑token 网关层放行接口,跳过 Sa‑Token 登录校验,请求才能转发到 system 服务,之后才走 AOP 校验 header
sa-token:
# 网关放行路径列表,不校验token
exclude:
- /system/dept
📝 Postman 调用示例
-
请求方式:
POST -
URL:
http://网关IP:端口/system/dept -
请求头:
Content-Type: application/json
keyid: 12345678 -
Body JSON 示例:
{
"deptName":"测试外部部门",
"parentId":0,
"sort":1,
"status":"0"
}
关键注意事项
ruoyi‑modules‑system必须依赖ruoyi‑common,默认若依已经依赖无需修改 pom- 鉴权顺序:网关放行 token → 请求进入 system 服务 → AOP 切面校验 header keyid → 执行业务
- 没有
keyid或者 keyid 不等于12345678,直接返回401 header keyid校验失败,不会进入新增逻辑 - 生产环境务必开启 HTTPS,防止 keyid 泄露;上线不需要该接口对外开放时,恢复
@RequiresPermissions、删除 yml 放行路径、移除@CheckKeyId。
排错清单
- 切面不执行:确认切面类有
@Component,注解全类名写对; - 依旧跳登录:检查 gateway 的
sa-token.exclude配置是否写对路径,改完重启 gateway 服务; - 空指针:确认代码中
SecurityUtils.getUsername()做了 null 判断。