一.若依是什么
1.1 什么是框架/架构
我理解的架构/框架应该有以下功能:
满足日常开发功能,如单点登陆、消息队列、监控等;
规范开发者的开发,指定代码格式、注释等;
提高开发效率,提供一系列的封装方法,并减少 bug 的产生率。
例如,开发企业级项目时,除了用户提供的特定业务外还有一些通用的功能,如权限、部门、公告、登陆用户等功能,同时也需要提供中间件的使用与配置,灵活的方法封装,(如获取用户,获取部门)与开发规范,如果没有使用框架的话,以上都需要自行扩展和开发。
1.2 若依做了什么
若依提供了一系列的基础业务模块、常用的封装方法,灵活可配置的中间件等开发基础。让开发者专注于业务开发,做到上手即用。
1.3 若依适合什么场景使用
适合小企业小体量、基础功能无定制化需求的项目,同时对于毕设、学习更是难得的好项目。
项目地址:http://www.ruoyi.vip
1.4 若依提供了那些基础功能
1.4.1 系统管理
用户管理:用户是系统操作者,该功能主要完成系统用户配置。
部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
岗位管理:配置系统用户所属担任职务。
菜单管理:配置系统菜单,操作权限,按钮权限标识等。
角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
字典管理:对系统中经常使用的一些较为固定的数据进行维护。
参数管理:对系统动态配置常用参数。
通知公告:系统通知公告信息发布维护。
1.4.2 日志管理
操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
登录日志:系统登录日志记录查询包含登录异常。
1.4.3 系统监控
在线用户:当前系统中活跃用户状态监控。
服务监控:监视当前系统 CPU、内存、磁盘、堆栈等相关信息。
缓存监控:对系统的缓存查询,查看、清理等操作。
在线构建器:拖动表单元素生成相应的 HTML 代码。
连接池监视:监视当期系统数据库连接池状态,可进行分析 SQL 找出系统性能瓶颈。
1.4.4 系统工具
定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
代码生成:前后端代码的生成(java、html、xml、sql)支持 CRUD 下载 。
系统接口:根据业务代码自动生成相关的 api 接口文档。
二.快速开始若依
2.1 前端搭建
前端直接安装 npm,然后启动即可,与开源 vue 使用相同。
2.2 后端搭建
修改数据库与 redis 的地址即可完成初始配置。
出现下图表示,运行成功!
三.代码介绍
ruoyi 为聚合工程项目分为以上 6 个功能包。下文就详细的介绍各个包的功能。请各位看管跟着博主思路慢慢了解 ruoyi 的全部核心代码。
3.1 ruoyi-quartz 包
使用 quartz 调度框架开发,此处篇幅较长,请参考博主其他文章。
3.2 ruoyi-generator 包
该包为代码生成器,通过前端页面指定业务表、模块功能等信息,就可以生成前后端代码,且生成的代码风格统一,极大提高了开发效率,主要的流程如下。
编写 vm 文件,提前定义模板;
通过页面输入业务表、模块相关信息;
首先通过 sql 语句查询指定表的字段信息,并将查询出的字段信息存入表中;
遍历 vm 文件,将表信息与输入功能信息填入 vm 文件预留变量中;
将文件转为流,并存放于指定位置。
3.2.1 GenTableServiceImpl
GenTableServiceImpl/generatorCode 类为该包核心,velocity 框架将收集到的数据库字段与输入的模块信息填入模板变量中,并生成代码。
@Override
public void generatorCode(String tableName)
{
// 查询表信息
GenTable table = genTableMapper.selectGenTableByName(tableName);
// 设置主子表信息
setSubTable(table);
// 设置主键列信息
setPkColumn(table);
VelocityInitializer.initVelocity();
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表 也就是vm文件
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
// 遍历模板文件
for (String template : templates)
{
if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm"))
{
// 渲染模板 将收集到的数据填入vm文件中
StringWriter sw = new StringWriter();
Template tpl = Velocity.getTemplate(template, Constants.UTF8);
tpl.merge(context, sw);
try
{
//将生成的文件写入固定位置
String path = getGenPath(table, template);
FileUtils.writeStringToFile(new File(path), sw.toString(), CharsetKit.UTF_8);
}
catch (IOException e)
{
throw new ServiceException("渲染模板失败,表名:" + table.getTableName());
}
}
}
}
复制代码
3.3 ruoyi-system 包
ruoyi 项目将 controller 与 service 部分隔离开来。该包为业务代码的 service 层,比较简单,代码请自行了解。
3.4 ruoyi-common 包
该包为项目的通用包,包括通用封装方法、自定义注解、枚举类等。统一将这些抽取出来更加方便开发者的使用,同时使写出的代码更加的统一,方便迭代与修改。
3.4.1 annotation 包
自定义注解,类使用注解后即可完成功能,使用 aop 作为自定义注解的逻辑。逻辑实现在 ruoyi-framework 中。
3.4.2 config 包
获取 application.yml 中的配置信息,并注入项目 bean 中,修改 yml 文件即可修改通用配置,实现配置统一管理。
3.4.3 constant 包
为项目提供常量池,统一管理常量,避免魔法值。
3.4.4 core 包
3.4.4.1 BaseController
所有接口层的基类。分页、排序、组装参数对于每一个 controller 都是必要的,所以为避免重复编写,封装在一基类中,业务 controller 继承该类即可调用。
public class BaseController
{
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 设置请求分页数据
*/
protected void startPage()
{
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
{
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}
}
/**
* 设置请求排序数据
*/
protected void startOrderBy()
{
PageDomain pageDomain = TableSupport.buildPageRequest();
if (StringUtils.isNotEmpty(pageDomain.getOrderBy()))
{
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
PageHelper.orderBy(orderBy);
}
}
/**
* 响应请求分页数据
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
protected TableDataInfo getDataTable(List<?> list)
{
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(HttpStatus.SUCCESS);
rspData.setMsg("查询成功");
rspData.setRows(list);
rspData.setTotal(new PageInfo(list).getTotal());
return rspData;
}
.
.
.
复制代码
3.4.4.2 domain
日常开发中使用实体类方式与前端对接,为了避免接口有多种返回类型,造成沟通成本的增加,使用 BaseEntity、AjaxResult 两种实体基类作为返回类型规范。
BaseEntity 规定了页码、总数等通用字段,需要其他实体类继承,避免减少重复代码与同义多名。
AjaxResult 是统一返回的实体类,能够与前台约定好固定的返回格式。如:{code: message: data}
格式。
public class AjaxResult extends HashMap<String, Object>
{
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public AjaxResult()
{
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(int code, String msg, Object data)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (StringUtils.isNotNull(data))
{
super.put(DATA_TAG, data);
}
}
.
.
.
public class BaseEntity implements Serializable
{
private static final long serialVersionUID = 1L;
/** 搜索值 */
private String searchValue;
/** 创建者 */
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** 备注 */
private String remark;
/** 请求参数 */
private Map<String, Object> params;
.
.
.
复制代码
3.4.5 enums 包
对枚举的统一管理,防止枚举值的重复定义。
3.4.6 exception 包
封装了一系列的异常,在特定场景抛出,方便错误的排查与定位。
3.4.7 filter 包
过滤器包,通用写法,如需添加过滤器,按照规则复写即可。
public class RepeatableFilter implements Filter
{
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
ServletRequest requestWrapper = null;
//判断是否为application/json类型 如果是重新构建请求体
if (request instanceof HttpServletRequest
&& StringUtils.equalsAnyIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE))
{
requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
}
// 否则通过
if (null == requestWrapper)
{
chain.doFilter(request, response);
}
else
{
chain.doFilter(requestWrapper, response);
}
}
@Override
public void destroy()
{
}
}
复制代码
3.4.8 utils 包
提供了非常多常见封装工具类,更加方便调用,如其他项目需要,也可直接复制使用。
3.5 ruoyi-framework 包
3.5.1 aspectj 包
此包为上文 ruoyi-common 中 annotation 包内自定义注解的实现。使用了 AOP,指定注释的逻辑类。
3.5.1.1 DataScopeAspect
该类为数据权限注解实现,在执行接口时,判断当前用户配置的数据权限(在角色页面处配置,如部门可见、本人可见),并将 sql 拼接到 mybatis 的 xml 中。核心代码如下:
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
{
//拼接sql
StringBuilder sqlString = new StringBuilder();
for (SysRole role : user.getRoles())
{
String dataScope = role.getDataScope();
// 判断角色中的数据权限
if (DATA_SCOPE_ALL.equals(dataScope))
{
sqlString = new StringBuilder();
break;
}
//自定义权限拼接
else if (DATA_SCOPE_CUSTOM.equals(dataScope))
{
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
}
//部门权限拼接
else if (DATA_SCOPE_DEPT.equals(dataScope))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
}
//部门及以下权限拼接
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
{
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
}
.
.
.
复制代码
3.5.1.2 DataSourceAspect
DataSource 注解动态功能为切换数据源,固定写法。
3.5.1.3 LogAspect
LogAspect 为日志收集注解,在接口调用时记录用户姓名、接口方法、调用 ip 等,并插入数据库留存。
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
{
try
{
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
operLog.setOperIp(ip);
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
//获取用户姓名
if (loginUser != null)
{
operLog.setOperName(loginUser.getUsername());
}
if (e != null)
{
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 保存数据库
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
}
catch (Exception exp)
{
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
复制代码
3.5.1.4 RateLimiterAspect
此为限流功能,接口调用时将客户端 ip 存放在 redis 中,并判断本次调用和上次调用的相隔时间。如频繁调用会拦截本次。
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
{
//redis固定的参数
String key = rateLimiter.key();
int time = rateLimiter.time();
int count = rateLimiter.count();
//获取ip+调用的方法
String combineKey = getCombineKey(rateLimiter, point);
List<Object> keys = Collections.singletonList(combineKey);
try
{
//获取一定时间内的调用次数
Long number = redisTemplate.execute(limitScript, keys, count, time);
if (StringUtils.isNull(number) || number.intValue() > count)
{
throw new ServiceException("访问过于频繁,请稍候再试");
}
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
}
catch (ServiceException e)
{
throw e;
}
catch (Exception e)
{
throw new RuntimeException("服务器限流异常,请稍候再试");
}
}
复制代码
3.5.2 config 包
该包为项目的核心配置类,当 application.yml 满足固定写法即可生效。配置写法固定统一,可直接复制使用,下文不做详细解释。
3.5.2.1 DruidProperties
从 application.yml 中获取数据源配置。
3.5.2.2 ApplicationConfig
时区信息配置。
3.5.2.3 CaptchaConfig
验证码使用,文字文本框格式等配置。
3.5.2.4 DruidConfig
多数据源配置。
3.5.2.5 FastJson2JsonRedisSerializer
redis 序列化配置,如缺少可能会生成乱码。
3.5.2.6 FilterConfig
过滤器配置,@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
含义为:根据 application.yml 是否配置 xss.enabled 值决定是否加载该类,也就是是否开启 xss 拦截器。
3.5.2.7 KaptchaTextCreator
计算验证码规则配置。
3.5.2.8 MyBatisConfig
Mybatis 配置类,从 application.yml 动态获取 mybatis 包的地址。并重新封装 SqlSessionFactory。实现 mybatis 路径的配置化。
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
{
//从application.yml获取配置
String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
String mapperLocations = env.getProperty("mybatis.mapperLocations");
String configLocation = env.getProperty("mybatis.configLocation");
//获取实体类的包
typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
VFS.addImplClass(SpringBootVFS.class);
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
//加入数据源
sessionFactory.setDataSource(dataSource);
//加入实体类地址
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
//加入mapper
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
//加入配置文件地址
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
return sessionFactory.getObject();
}
复制代码
3.5.2.9 RedisConfig
redis 配置固定写法。
3.5.2.10 ResourcesConfig
配置拦截器、跨域是否生效。
public class ResourcesConfig implements WebMvcConfigurer
{
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry)
{
/** 本地文件上传路径 */
registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**")
.addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");
/** swagger配置 */
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
}
//配置拦截器生效
@Override
public void addInterceptors(InterceptorRegistry registry)
{ //此处配置了上文点击重复的拦截器
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
//跨域配置
@Bean
public CorsFilter corsFilter()
{
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}
}
复制代码
3.5.2.11 SecurityConfig
Spring Security 的配置,该处篇幅较多,请参考博主其他 Spring Security 文章。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败返回json
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求 验证那些接口需要鉴权
.authorizeRequests()
// 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/captchaImage").anonymous()
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js"
).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT过滤器
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS过滤器
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现 该加密算法为spring security提供
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口 配置查询用户的service
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
复制代码
3.5.2.12 ServerConfig
获取请求信息,包括:域名,端口,上下文访问路径等。
3.5.2.13 ThreadPoolConfig
线程池配置,manager 为异步线程池实例。
3.5.3 datasource 包
多数据源固定配置,与 DataSourceAspect 配合使用。
3.5.4 interceptor 包
提供不允许重复点击功能,实现原理:调用信息组装为 key 值存放到 redis 中,调用时从 redis 获取该 key 数据并验证相隔时间,如过于频繁,抛弃本次调用。
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
{
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper)
{
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
}
// body参数为空,获取Parameter的数据
if (StringUtils.isEmpty(nowParams))
{
nowParams = JSONObject.toJSONString(request.getParameterMap());
}
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 唯一值(没有消息头则使用请求地址)
String submitKey = request.getHeader(header);
if (StringUtils.isEmpty(submitKey))
{
submitKey = url;
}
// 组装成加入redis的key值
String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
//根据key值查询redsi
Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
//如果能够查询到
if (sessionObj != null)
{
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
//比对参数,同时比对时间
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
{
return true;
}
}
}
Map<String, Object> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
return false;
}
复制代码
3.5.5 manager 包
AsyncManager、ShutdownManager
为异步工厂配置,AsyncFactory 为使用实例。
3.5.6 security 包
该包为 token 相关方法集合。
3.5.6.1 JwtAuthenticationTokenFilter
主要为验证 token 是否正确。
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
//验证用户信息
LoginUser loginUser = tokenService.getLoginUser(request);
// 验证用户是否有权限
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
//刷新token
tokenService.verifyToken(loginUser);
//获取用户权限对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将用户权限等信息存放在SecurityContext中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
复制代码
3.5.6.2 AuthenticationEntryPointImpl、LogoutSuccessHandlerImpl
将登陆成功、失败等返回信息转换为 json 方式。
3.5.7 web 包
3.5.7.1 server
获取服务器信息,如 cpu 等信息。
private void setCpuInfo(CentralProcessor processor)
{
// CPU信息
long[] prevTicks = processor.getSystemCpuLoadTicks();
Util.sleep(OSHI_WAIT_SECOND);
long[] ticks = processor.getSystemCpuLoadTicks();
long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
cpu.setCpuNum(processor.getLogicalProcessorCount());
cpu.setTotal(totalCpu);
cpu.setSys(cSys);
cpu.setUsed(user);
cpu.setWait(iowait);
cpu.setFree(idle);
}
/**
* 设置内存信息
*/
private void setMemInfo(GlobalMemory memory)
{
mem.setTotal(memory.getTotal());
mem.setUsed(memory.getTotal() - memory.getAvailable());
mem.setFree(memory.getAvailable());
}
.
.
.
复制代码
3.5.7.2 exception
@RestControllerAdvice、@ExceptionHandler
为全局异常拦截配置,当系统中有异常时,会直接调用该类,并根据异常类型抛出指定输出,减少业务代码中的 try/catch。
@RestControllerAdvice
public class GlobalExceptionHandler
{
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 权限校验异常
*/
@ExceptionHandler(AccessDeniedException.class)
public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request)
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
}
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request)
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
return AjaxResult.error(e.getMessage());
}
复制代码
3.5.7.3 service
权限相关的 service。大部分都是 curd 的业务,这里详细说 TokenService。
3.5.7.3.1 TokenService
使用 jwt 实现 token 生成、token 获取数据等方法。
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token)
{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
复制代码
3.6 ruoyi-admin 包
ruoyi-admin 包提供了一系列的通用接口(controller 接口),如需使用直接调用即可,防止开发者未经过沟通相同功能接口开发多次,造成不统一。
3.6.1 common 包
3.6.1.1 CaptchaController
通过谷歌验证码方法生成验证码,详细请看注释。
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
// 通过uuid生成验证码
String uuid = IdUtils.simpleUUID();
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null;
BufferedImage image = null;
// 根据类型生成不同的验证码
//数字验证码
if ("math".equals(captchaType))
{
//使用谷歌验证码方法
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
//使用谷歌验证码方法生成图片
image = captchaProducerMath.createImage(capStr);
}
//文字验证码
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
AjaxResult ajax = AjaxResult.success();
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
复制代码
3.6.1.2 CommonController
通用的上传下载接口,防止重复编写造成一个需求需要修改多个类方法。
3.6.2 monitor 包
3.6.2.1 CacheController
通过 redis 对外接口监控 redis 信息,如 rediskey 数量、key 的详细信息等。
public class CacheController
{
@Autowired
private RedisTemplate<String, String> redisTemplate;
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception
{
//redis的常用信息
Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
//rediskey数量
Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
Map<String, Object> result = new HashMap<>(3);
result.put("info", info);
result.put("dbSize", dbSize);
//key的详细信息
List<Map<String, String>> pieList = new ArrayList<>();
commandStats.stringPropertyNames().forEach(key -> {
Map<String, String> data = new HashMap<>(2);
String property = commandStats.getProperty(key);
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
pieList.add(data);
});
result.put("commandStats", pieList);
return AjaxResult.success(result);
}
}
复制代码
3.6.2.2 ServerController
主要监控正在运行服务器的信息,如 cpu、内存、磁盘等信息。
public void copyTo() throws Exception
{
SystemInfo si = new SystemInfo();
HardwareAbstractionLayer hal = si.getHardware();
setCpuInfo(hal.getProcessor());
setMemInfo(hal.getMemory());
setSysInfo();
setJvmInfo();
setSysFiles(si.getOperatingSystem());
}
/**
* 设置CPU信息
*/
private void setCpuInfo(CentralProcessor processor)
{
// CPU信息
long[] prevTicks = processor.getSystemCpuLoadTicks();
Util.sleep(OSHI_WAIT_SECOND);
long[] ticks = processor.getSystemCpuLoadTicks();
long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
cpu.setCpuNum(processor.getLogicalProcessorCount());
cpu.setTotal(totalCpu);
cpu.setSys(cSys);
cpu.setUsed(user);
cpu.setWait(iowait);
cpu.setFree(idle);
}
/**
* 设置内存信息
*/
private void setMemInfo(GlobalMemory memory)
{
mem.setTotal(memory.getTotal());
mem.setUsed(memory.getTotal() - memory.getAvailable());
mem.setFree(memory.getAvailable());
}
.
.
.
}
复制代码
3.SysLogininforController,SysOperlogController 登录日志、操作日志
主要查询前文 AOP 生成的日志表,普通的增删改查。
4.SysUserOnlineController 在线用户管理
主要功能为在线用户监控与强踢下线。通过查询和删除 redis 缓存即可实现。
3.6.3 system 包
上文说到 ruoyi 业务代码的 service 与 controller 层分在两个包中,该处为业务代码的 controller 层。
3.6.3.1 @PreAuthorize
@PreAuthorize("@ss.hasPermi('system:dict:list')")
此处为 shiro 框架提供权限注解,配合权限表使用,当菜单表中 perms 字段与 @PreAuthorize 注解内容system:dict:list
匹配后才有权限访问接口。
3.6.3.2 AjaxResult
提供了固定的结果编码/调用信息/数据的返回格式,为前台提供了统一的返回格式,这样防止过多种类的返回类型,从而增大沟通成本。该方案为架构的基础,绝大部分架构均为该种处理方案。
public class AjaxResult extends HashMap<String, Object>
{
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
*/
public AjaxResult()
{
}
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
*/
public AjaxResult(int code, String msg)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static AjaxResult success()
{
return AjaxResult.success("操作成功");
}
.
.
.
复制代码
三.ruoyi 的优势
ruoyi 框架对比市面上其他产品有以下优势:
代码为作者一个编写,格式统一,注释完备,整体结构干净舒服;
搭建与启动快捷,依赖组件较少;
基础功能完备,足够支撑小体量业务;
社区活跃,提供了工作流、单点登陆、多数据库版本,可按需使用。
四.总结
如果您有初识架构、自我提升、私活项目架构等需求,ruoyi 是您不二选择,如果其他问题,可留言沟通。
评论