# db mysql
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/ry-config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user=数据库账号
db.password=数据库密码
6.1.3 项目配置文件
Nacos配置好之后,启动成功后访问:http://localhost:8848/nacos 账号/密码 nacos/nacos
修改下面画红线的服务配置文件
主要改动:
Redis 的ip,端口,密码
MySQL ip,端口,账号,密码,数据库等。
6.2 后端启动
打开项目,执行XxxxApplication的启动类(启动没有先后顺序)
RuoYiGatewayApplication (网关模块 必须)
RuoYiAuthApplication (认证模块 必须)
RuoYiSystemApplication (系统模块 必须)
RuoYiMonitorApplication (监控中心 可选)
RuoYiGenApplication (代码生成 可选)
RuoYiJobApplication (定时任务 可选)
RuoYFileApplication (文件服务 可选)
加粗的启动即可。
6.3 前端配置与启动
若依(RuoYi-Cloud) 项目前端跟后端是保存在同一个目录夹的。
先进入这个目录,然后使用前端编辑工具打开即可,这里使用Visual Studio Code 打开
进入vscode之后,打开项目README.md文件
打开vscode 的终端,执行上面命令
6.4 访问
当后端,前端都启动成功之后,若依(RuoYi-Cloud) 项目就算运行成功啦。
访问地址:http://localhost/
七、模块解读
项目解读可以说分为2部分: 1>项目结构解读(静态解读) 2>业务模块解读(动态解读)
前面种种都属于项目结构范畴,解下来就是业务模块解读,这里选择鉴权,网关,代码生成器3个核心模块讲解若依(RuoYi-Cloud)脚手架的使用。
7.1 解读技巧
只要是Java Web项目,流程主干线永远不变: 发起请求---处理请求----响应请求
找一个业务逻辑相对简单的模块,围绕主线去追踪即可。根据以往的经验,请求到响应大体流程图如下:
把上面流程具体化之后就是:
客户端如何发起请求(关注路径/方式/参数),一般使用浏览器的F12查看
接口接收请求,处理请求,响应请求。(关注:单个服务操作/多服务协同操作),一般追踪请求流程。
简单流程:网关---过滤器---拦截器--微服务(请求处理)
复杂流程:网关---上游过滤器---上游拦截器--上游微服务(处理)-----下游过滤器---下游拦截器--下游微服务(处理)
7.2 请求闭环
这里结合上面主线,走一个完整的请求闭环,以获取登录验证码获取为例子。
7.2.1 前端
项目进入第一个页面是登录页面
浏览器F12,查看到第一个接口:http://localhost/dev-api/code
localhost:前端项目启动默认访问ip, 为本地
端口:没有端口,默认端口为80
dev-api:若依框架约定的开发环境使用上下文,可以在.env.development中配置
code:请求路径
完整的:http://locallhost:80/dev-api/code 是由下面异步请求发起的
看若依(RuoYi-Cloud)框架的架构图,请求最先进入的网关。当然也可以通过前端代理服务配置定位到请求会转发到网关服务。
当确认请求入口之后,只需快速定位ruoyi-gateway即可。
此时还存在一个问题,网关哪个类处理/code请求?
如果有WebFlux编程经验,大体能猜到RouterFunctionConfiguration类
@Configuration
public class RouterFunctionConfiguration
@Autowired
private ValidateCodeHandler validateCodeHandler;
@SuppressWarnings("rawtypes")
@Bean
public RouterFunction routerFunction()
return RouterFunctions.route(
RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
validateCodeHandler);
RouterFunction 是 Spring WebFlux 中定义路由的函数式编程方式。
在 Spring WebFlux 中,我们可以使用 RouterFunction
来定义路由规则,将请求映射到相应的处理器函数上。
上面代码意思是前端发过来/code由validateCodeHandler 来处理
@Component
public class ValidateCodeHandler implements HandlerFunction<ServerResponse>
@Autowired
private ValidateCodeService validateCodeService;
@Override
public Mono<ServerResponse> handle(ServerRequest serverRequest)
AjaxResult ajax;
//构建验证码,封装成AjaxResult对象
ajax = validateCodeService.createCaptcha();
catch (CaptchaException | IOException e)
return Mono.error(e);
return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax));
@Override
public AjaxResult createCaptcha() throws IOException, CaptchaException
AjaxResult ajax = AjaxResult.success();
boolean captchaEnabled = captchaProperties.getEnabled();
ajax.put("captchaEnabled", captchaEnabled);
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
最终将生成的验证码封装在AjaxResult 响应回客户端。
那如果说没有Spring WebFlux经验怎么办?很简单,使用idea 搜/code 字符,然后借助ChatGPT猜测代码啥意思
到这,一个完整请求闭环就完成了,后续其他请求便是同理可得。
7.3 网关模块
网关为项目入口,若依(RuoYi-Cloud)项目入口结构可以简化为这样
在ruoyi-gateway网关微服务中配置了所有微服务路由映射
spring:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 认证中心
- id: ruoyi-auth
uri: lb://ruoyi-auth
predicates:
- Path=/auth/**
filters:
# 验证码处理
- CacheRequestFilter
- ValidateCodeFilter
- StripPrefix=1
# 代码生成
- id: ruoyi-gen
uri: lb://ruoyi-gen
predicates:
- Path=/code/**
filters:
- StripPrefix=1
# 定时任务
- id: ruoyi-job
uri: lb://ruoyi-job
predicates:
- Path=/schedule/**
filters:
- StripPrefix=1
# 系统模块
- id: ruoyi-system
uri: lb://ruoyi-system
predicates:
- Path=/system/**
filters:
- StripPrefix=1
# 文件服务
- id: ruoyi-file
uri: lb://ruoyi-file
predicates:
- Path=/file/**
filters:
- StripPrefix=1
前端发起所有请都统一经过网关,再借由网关路由统一转发。
访问ruoyi-file微服务路径规则: http://localhost:80/dev-api/file/**
访问ruoyi-gen微服务路径规则: http://localhost:80/dev-api/code/**
访问ruoyi-job微服务路径规则: http://localhost:80/dev-api/schedule/**
访问ruoyi-system微服务路径规则: http://localhost:80/dev-api/system/**
访问ruoyi-auth微服务路径规则: http://localhost:80/dev-api/auth/**
7.4 鉴权模块
若依(RuoYi-Cloud)鉴权模块涉及到这几个组件
若依(RuoYi-Cloud)框架登录鉴权有2种模式:
外部鉴权:客户端访问服务端(微服务),以JWT令牌为判断依据,有且合法放行,没有或不合法`
内部鉴权:上游微服务访问下游微服务,以请求头标记:from-source=inner有无位依据,有放行,没有拒绝
7.4.1 外部鉴权
7.4.1.1 步骤1:JWT获取
登录页面点击登录发起:http://localhost/dev-api/auth/login 请求
请求中auth前缀为网关配置ry-auth服务路由映射
借助网格路由,进入ry-auth微服务,TokenController请求处理类
@RestController
public class TokenController
@PostMapping("login")
public R<?> login(@RequestBody LoginBody form)
// 用户登录
LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
// 获取登录token
return R.ok(tokenService.createToken(userInfo));
* 创建令牌
public Map<String, Object> createToken(LoginUser loginUser)
String token = IdUtils.fastUUID();
Long userId = loginUser.getSysUser().getUserId();
String userName = loginUser.getSysUser().getUserName();
loginUser.setToken(token);
loginUser.setUserid(userId);
loginUser.setUsername(userName);
loginUser.setIpaddr(IpUtils.getIpAddr());
refreshToken(loginUser); //redis缓存令牌
// Jwt存储信息
Map<String, Object> claimsMap = new HashMap<String, Object>();
claimsMap.put(SecurityConstants.USER_KEY, token);
claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);
// 接口返回信息
Map<String, Object> rspMap = new HashMap<String, Object>();
rspMap.put("access_token", JwtUtils.createToken(claimsMap));
rspMap.put("expires_in", expireTime);
return rspMap;
响应返回:
7.4.1.2 步骤2:JWT鉴权
当客户登录成功之后,后续请求进入网关,网关转发到对应的微服务,该微服务会引用:ruoyi-common-security 鉴权模块,请求进来后,切面判断请求持有的JWT令牌,有且合法放行,没有或不合法拒绝
7.4.1.3 步骤3:JWT鉴权实现
@Aspect
@Component
public class PreAuthorizeAspect
public PreAuthorizeAspect()
* 定义AOP签名 (切入所有使用鉴权注解的方法)
public static final String POINTCUT_SIGN = " @annotation(com.ruoyi.common.security.annotation.RequiresLogin) || "
+ "@annotation(com.ruoyi.common.security.annotation.RequiresPermissions) || "
+ "@annotation(com.ruoyi.common.security.annotation.RequiresRoles)";
* 声明AOP签名
@Pointcut(POINTCUT_SIGN)
public void pointcut()
* 环绕切入
* @param joinPoint 切面对象
* @return 底层方法执行后的返回值
* @throws Throwable 底层方法抛出的异常
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable
// 注解鉴权
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
checkMethodAnnotation(signature.getMethod());
// 执行原有逻辑
Object obj = joinPoint.proceed();
return obj;
catch (Throwable e)
throw e;
* 对一个Method对象进行注解检查
public void checkMethodAnnotation(Method method)
// 校验 @RequiresLogin 注解
RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
if (requiresLogin != null)
AuthUtil.checkLogin();
// 校验 @RequiresRoles 注解
RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
if (requiresRoles != null)
AuthUtil.checkRole(requiresRoles);
// 校验 @RequiresPermissions 注解
RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
if (requiresPermissions != null)
AuthUtil.checkPermi(requiresPermissions);
阅读上面的切面代码,最终得出
只要接口方法只要贴有上面3个注解,都需要进行外部鉴权。比如:
* 获取用户列表
@RequiresPermissions("system:user:list")
@GetMapping("/list")
public TableDataInfo list(SysUser user)
startPage();
List<SysUser> list = userService.selectUserList(user);
return getDataTable(list);
7.4.2 内部鉴权
内部鉴权是微服务与微服务间鉴权,发生在上游微服务访问下游微服务,以请求头标记:from-source=inner有无位依据,有放行,没有拒绝,其目的是实现网络隔离。
来自网关请求,以JWT进行鉴权,来自内部远程调用请求,以from-source=inner进行鉴权,其他非法请求直接拒绝。
还是以ry-auth微服务的TokenController类的登录方法为案例
7.4.2.1 步骤1:远程调用过程
登录请求,先到TokenController类login方法
@RestController
public class TokenController
@Autowired
private TokenService tokenService;
@Autowired
private SysLoginService sysLoginService;
@PostMapping("login")
public R<?> login(@RequestBody LoginBody form)
// 用户登录
LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
// 获取登录token
return R.ok(tokenService.createToken(userInfo));
login方法中用到:SysLoginService接口login逻辑
private SysLoginService sysLoginService;
sysLoginService.login(form.getUsername(), form.getPassword());
观察SysLoginService类,里面引用了接口:RemoteUserService
@Component
public class SysLoginService
@Autowired
private RemoteUserService remoteUserService;
public LoginUser login(String username, String password)
......
// 查询用户信息
R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
......
观察RemoteUserService接口所在位置,明显与TokenController/SysLoginService不在同一微服务。
RemoteUserService 属于ry-system微服务,而TokenController类属于ry-auth微服务,那么此处的remoteUserService.getUserInfo为远程调用。
一个问题:既然为远程调用,跟普通接口调用有啥区别?
定义区别:
使用feign组件,定义远程接口
@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteUserFallbackFactory.class)
public interface RemoteUserService
* 通过用户名查询用户信息
* @param username 用户名
* @param source 请求来源
* @return 结果
@GetMapping("/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
接口定义,除了正常的username参数之外,多了一个请求头
@RequestHeader(SecurityConstants.FROM_SOURCE) String source
调用区别:
使用Feign组件发起远程接口调用,需要多传一个SecurityConstants.INNER 参数
remoteUserService.getUserInfo(username, SecurityConstants.INNER);
这是为何?
SecurityConstants.INNER为内部微服务间远程调用请求头标识,上游微服务调用时,添加该标识的请求头,下游微服务接收请求前,进行请求头校验。
定义是指定请求头key:SecurityConstants.FROM_SOURCE
getUserInfo(
@PathVariable("username") String username,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
调用时,指定请求头value:SecurityConstants.INNER
remoteUserService.getUserInfo(username, SecurityConstants.INNER);
SecurityConstants.FROM_SOURCE = SecurityConstants.INNER
from-source = inner
7.4.2.2 步骤2:远程正式调用
上面定义的远程接口,最终通过feign方式调用到ruoyi-system微服务SysUserController类的info接口
SysUserController类的info接口
@RestController
@RequestMapping("/user")
public class SysUserController extends BaseController
{ /**
* 获取当前用户信息
@InnerAuth
@GetMapping("/info/{username}")
public R<LoginUser> info(@PathVariable("username") String username)
SysUser sysUser = userService.selectUserByUserName(username);
if (StringUtils.isNull(sysUser))
return R.fail("用户名或密码错误");
// 角色集合
Set<String> roles = permissionService.getRolePermission(sysUser);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(sysUser);
LoginUser sysUserVo = new LoginUser();
sysUserVo.setSysUser(sysUser);
sysUserVo.setRoles(roles);
sysUserVo.setPermissions(permissions);
return R.ok(sysUserVo);
这里留意,info方法上面有个@InnerAuth 注解,表示当前info方法为远程调用方法,需要进行远程校验,也即:内部鉴权。
7.4.2.3 步骤3:内部鉴权
@Aspect
@Component
public class InnerAuthAspect implements Ordered
@Around("@annotation(innerAuth)")
public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable
String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);
// 内部请求验证
if (!StringUtils.equals(SecurityConstants.INNER, source))
throw new InnerAuthException("没有内部访问权限,不允许访问");
String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);
String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
// 用户信息验证
if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)))
throw new InnerAuthException("没有设置用户信息,不允许访问 ");
return point.proceed();
* 确保在权限认证aop执行前执行
@Override
public int getOrder()
return Ordered.HIGHEST_PRECEDENCE + 1;
阅读上面的切面代码,发起切面是切入点事贴有@InnerAuth接口方法。
7.5 代码生成模块
若依(RuoYi-Cloud)代码生成模块是一个简单模块,功能顾名思义,用于构建若依体系的代码。
启动后,访问页面:
这里演示一下代码生成器使用过程。
7.5.1 步骤1:创建表
在ry-cloud库中创建一个Employee表
CREATE TABLE `sys_employee` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '名称',
`age` int DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='员工表';
7.5.2 步骤2:导入新建表
在代码生成界面导入新建表
7.5.3 步骤3:定制代码信息
7.5.3.1 基本信息
7.5.3.2 字段信息
字段列名:表中列的名称
字段描述:生成实体类属性注释
物理类型:表中列的类型
Java类型:生成实体类属性变量类型
Java属性:生成实体类属性名
插入:定义添加操作模态框表单控件,选中表示模态框有这个输入空间
编辑:定义编辑操作模态框表单控件,选中表示模态框有这个输入空间
列表:表中数据在列表时,该列数据是否显示,选中为要显示
查询:该列是否作为列表的查询条件,选中为需要作为查询条件
查询方式:作为查询条件时,使用匹配方式
显示类型:查询条件输入类型
字典类型:查询条件显示类型如果是下拉框,使用字典类型,实现下拉选择
7.5.3.3 生成信息
生成模板:一般不动,以单表操作为主
生成包路径:指定当前代码生成根包路径
生成模块名:该功能所属模块,落地到代码就是所在微服务名称,ruoyi-gateway配置的路由映射路径名
生成业务名:该功能英文名,落地到代码就是controller中操作资源名, 比如:/employee
生成功能名:该功能中文名称
上级菜单:该功能页面展示菜单连接挂载在哪个上级菜单。
7.5.3.4 预览
配置上面各种信息成功之后,可以点击预览,根据需要微调
7.5.3.5 生成代码
确定无误之后,直接点生成代码
7.5.4 步骤4:使用代码
压缩包解压得到几个文件:
涉及要添加的菜单,菜单权限,在navicat中执行即可。
-- 菜单 SQL
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('员工', '1', '1', 'employee', 'system/employee/index', 1, 0, 'C', '0', '0', 'system:employee:list', '#', 'admin', sysdate(), '', null, '员工菜单');
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('员工查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', 'system:employee:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('员工新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', 'system:employee:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('员工修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', 'system:employee:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('员工删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', 'system:employee:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
values('员工导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', 'system:employee:export', '#', 'admin', sysdate(), '', null, '');
执行成功后,刷新前端项目,可以看到菜单
7.5.4.2 vue
ruoyi-ui前端employee这个模块设计到vue页面,与js
分别拷贝到项目api目录,跟views目录
刷新之后,点击员工菜单,缺少后端接口
7.5.4.3 main
后端接口代码,将main代码拷贝到ruoyi-system main目录中即可
然后,重启ruoyi-system服务,再访问。
八、定制微服务
8.1 需求
定制微服务-ruoyi-modules-test
8.2 代码结构
对外api:ruoyi-api-test
微服务:ruoyi-modules-test
8.3 实现步骤
8.3.1 步骤1:定义ruoyi-api-test
导入依赖参考ruoyi-api-system
创建包结构参考ruoyi-api-system
这里注意包命名规则:
com.ruoyi-----公司域名倒写
system------微服务名
api------模块名
8.3.2 步骤2:等级ruoyi-api-test依赖
去父项目ruoyi pom.xml文件中登记新建的api
<!-- test接口 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-api-test</artifactId>
<version>${ruoyi.version}</version>
</dependency>
8.3.3 步骤3:定义ruoyi-modules-test
导入依赖参考,ruoyi-system 注意要导入ruoyi-api-test依赖
创建包结构参考ruoyi-system
8.3.4 步骤4:定制微服务配置文件
nacos克隆一份配置文件--克隆自ruoyi-system-dev.yml
8.3.5 步骤5:定制网关微服务路由
到这,定制微服务就结束了,剩下操作就是常规crud了。
将整篇教程总结一下:若依(Ruoyi-Cloud)版本使用
1>官网克隆项目到idea
2>按照文档,建库,建表,配置nacos
3>准备各种环境,修改本地配置
4>前后端启动,测试。
1>网关模块,所有请求入口
2>鉴权模块,外部鉴权,内部鉴权
3>代码生成模块,先建表,再定制,后覆盖
微服务定制
1>拷贝模仿现有api/modules
2>配置中心加配置文件
3>网关配置转发路由