Conflicts:
	ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/domain/SysUser.java
	ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/factory/RemoteLogFallbackFactory.java
	ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/factory/RemoteUserFallbackFactory.java
	ruoyi-common/ruoyi-common-security/src/main/java/com/ruoyi/common/security/handler/GlobalExceptionHandler.java
	ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/properties/IgnoreWhiteProperties.java
	ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java
This commit is contained in:
疯狂的狮子li
2021-08-02 13:18:22 +08:00
115 changed files with 3478 additions and 1502 deletions

22
pom.xml
View File

@@ -6,37 +6,37 @@
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
<name>ruoyi</name>
<url>http://www.ruoyi.vip</url>
<description>若依微服务系统</description>
<properties>
<ruoyi.version>3.0.0</ruoyi.version>
<ruoyi.version>3.1.0</ruoyi.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-boot.version>2.5.1</spring-boot.version>
<spring-boot.version>2.5.3</spring-boot.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<alibaba.nacos.version>2.0.2</alibaba.nacos.version>
<spring-boot-admin.version>2.4.1</spring-boot-admin.version>
<spring-boot.mybatis>2.1.4</spring-boot.mybatis>
<alibaba.nacos.version>2.0.3</alibaba.nacos.version>
<spring-boot-admin.version>2.4.3</spring-boot-admin.version>
<spring-boot.mybatis>2.2.0</spring-boot.mybatis>
<swagger.fox.version>3.0.0</swagger.fox.version>
<swagger.core.version>1.6.2</swagger.core.version>
<tobato.version>1.26.5</tobato.version>
<tobato.version>1.27.2</tobato.version>
<kaptcha.version>2.3.2</kaptcha.version>
<pagehelper.boot.version>1.3.1</pagehelper.boot.version>
<druid.version>1.2.6</druid.version>
<dynamic-ds.version>3.4.0</dynamic-ds.version>
<commons.io.version>2.10.0</commons.io.version>
<dynamic-ds.version>3.4.1</dynamic-ds.version>
<commons.io.version>2.11.0</commons.io.version>
<commons.fileupload.version>1.4</commons.fileupload.version>
<velocity.version>1.7</velocity.version>
<fastjson.version>1.2.76</fastjson.version>
<minio.version>8.2.1</minio.version>
<minio.version>8.2.2</minio.version>
<poi.version>4.1.2</poi.version>
<common-pool.version>2.6.2</common-pool.version>
<common-pool.version>2.10.0</common-pool.version>
<commons-collections.version>3.2.2</commons-collections.version>
<hutool.version>5.7.1</hutool.version>
</properties>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-api</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,9 +3,11 @@ package com.ruoyi.system.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestHeader;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.constant.ServiceNameConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.api.domain.SysLogininfor;
import com.ruoyi.system.api.domain.SysOperLog;
import com.ruoyi.system.api.factory.RemoteLogFallbackFactory;
@@ -21,20 +23,19 @@ public interface RemoteLogService
* 保存系统日志
*
* @param sysOperLog 日志实体
* @param source 请求来源
* @return 结果
*/
@PostMapping("/operlog")
R<Boolean> saveLog(@RequestBody SysOperLog sysOperLog);
public R<Boolean> saveLog(@RequestBody SysOperLog sysOperLog, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
/**
* 保存访问记录
*
* @param username 用户名称
* @param status 状态
* @param message 消息
* @param sysLogininfor 访问实体
* @param source 请求来源
* @return 结果
*/
@PostMapping("/logininfor")
R<Boolean> saveLogininfor(@RequestParam("username") String username, @RequestParam("status") String status,
@RequestParam("message") String message);
public R<Boolean> saveLogininfor(@RequestBody SysLogininfor sysLogininfor, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}

View File

@@ -3,8 +3,13 @@ package com.ruoyi.system.api;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.constant.ServiceNameConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.factory.RemoteUserFallbackFactory;
import com.ruoyi.system.api.model.LoginUser;
@@ -20,8 +25,19 @@ public interface RemoteUserService
* 通过用户名查询用户信息
*
* @param username 用户名
* @param source 请求来源
* @return 结果
*/
@GetMapping(value = "/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username);
@GetMapping("/user/info/{username}")
public R<LoginUser> getUserInfo(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
/**
* 注册用户信息
*
* @param sysUser 用户信息
* @param source 请求来源
* @return 结果
*/
@PostMapping("/user/register")
public R<Boolean> registerUserInfo(@RequestBody SysUser sysUser, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}

View File

@@ -1,4 +1,4 @@
package com.ruoyi.system.domain;
package com.ruoyi.system.api.domain;
import java.util.Date;

View File

@@ -87,14 +87,7 @@ public class SysUser extends BaseEntity {
@JsonProperty
private String password;
/**
* 盐加密
*/
private String salt;
/**
* 帐号状态0正常 1停用
*/
/** 帐号状态0正常 1停用 */
@Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
private String status;

View File

@@ -7,6 +7,7 @@ import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.api.RemoteLogService;
import com.ruoyi.system.api.domain.SysLogininfor;
import com.ruoyi.system.api.domain.SysOperLog;
/**
@@ -23,12 +24,12 @@ public class RemoteLogFallbackFactory implements FallbackFactory<RemoteLogServic
log.error("日志服务调用失败:{}", throwable.getMessage());
return new RemoteLogService() {
@Override
public R<Boolean> saveLog(SysOperLog sysOperLog) {
public R<Boolean> saveLog(SysOperLog sysOperLog, String source) {
return null;
}
@Override
public R<Boolean> saveLogininfor(String username, String status, String message) {
public R<Boolean> saveLogininfor(SysLogininfor sysLogininfor, String source) {
return null;
}
};

View File

@@ -7,6 +7,7 @@ import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.api.RemoteUserService;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.model.LoginUser;
/**
@@ -23,9 +24,14 @@ public class RemoteUserFallbackFactory implements FallbackFactory<RemoteUserServ
log.error("用户服务调用失败:{}", throwable.getMessage());
return new RemoteUserService() {
@Override
public R<LoginUser> getUserInfo(String username) {
public R<LoginUser> getUserInfo(String username, String source) {
return R.fail("获取用户失败:" + throwable.getMessage());
}
@Override
public R<Boolean> registerUserInfo(SysUser sysUser, String source) {
return R.fail("注册用户失败:" + throwable.getMessage());
}
};
}
}

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.auth.form.LoginBody;
import com.ruoyi.auth.form.RegisterBody;
import com.ruoyi.auth.service.SysLoginService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.StringUtils;
@@ -63,4 +64,12 @@ public class TokenController
}
return R.ok();
}
@PostMapping("register")
public R<?> register(@RequestBody RegisterBody registerBody)
{
// 用户注册
sysLoginService.register(registerBody.getUsername(), registerBody.getPassword());
return R.ok();
}
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.auth.form;
/**
* 用户注册对象
*
* @author ruoyi
*/
public class RegisterBody extends LoginBody
{
}

View File

@@ -3,14 +3,18 @@ package com.ruoyi.auth.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.enums.UserStatus;
import com.ruoyi.common.core.exception.BaseException;
import com.ruoyi.common.core.utils.SecurityUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.ip.IpUtils;
import com.ruoyi.system.api.RemoteLogService;
import com.ruoyi.system.api.RemoteUserService;
import com.ruoyi.system.api.domain.SysLogininfor;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.model.LoginUser;
@@ -36,25 +40,25 @@ public class SysLoginService
// 用户名或密码为空 错误
if (StringUtils.isAnyBlank(username, password))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写");
throw new BaseException("用户/密码必须填写");
}
// 密码如果不在指定范围内 错误
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围");
throw new BaseException("用户密码不在指定范围");
}
// 用户名不在指定范围内 错误
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
throw new BaseException("用户名不在指定范围");
}
// 查询用户信息
R<LoginUser> userResult = remoteUserService.getUserInfo(username);
R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
if (R.FAIL == userResult.getCode())
{
@@ -63,33 +67,93 @@ public class SysLoginService
if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData()))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
throw new BaseException("登录用户:" + username + " 不存在");
}
LoginUser userInfo = userResult.getData();
SysUser user = userResult.getData().getSysUser();
if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");
recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");
throw new BaseException("对不起,您的账号:" + username + " 已被删除");
}
if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
throw new BaseException("对不起,您的账号:" + username + " 已停用");
}
if (!SecurityUtils.matchesPassword(password, user.getPassword()))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户密码错误");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码错误");
throw new BaseException("用户不存在/密码错误");
}
remoteLogService.saveLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
return userInfo;
}
public void logout(String loginName)
{
remoteLogService.saveLogininfor(loginName, Constants.LOGOUT, "退出成功");
recordLogininfor(loginName, Constants.LOGOUT, "退出成功");
}
/**
* 注册
*/
public void register(String username, String password)
{
// 用户名或密码为空 错误
if (StringUtils.isAnyBlank(username, password))
{
throw new BaseException("用户/密码必须填写");
}
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
{
throw new BaseException("账户长度必须在2到20个字符之间");
}
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
{
throw new BaseException("密码长度必须在5到20个字符之间");
}
// 注册用户信息
SysUser sysUser = new SysUser();
sysUser.setUserName(username);
sysUser.setNickName(username);
sysUser.setPassword(SecurityUtils.encryptPassword(password));
R<?> registerResult = remoteUserService.registerUserInfo(sysUser, SecurityConstants.INNER);
if (R.FAIL == registerResult.getCode())
{
throw new BaseException(registerResult.getMsg());
}
recordLogininfor(username, Constants.REGISTER, "注册成功");
}
/**
* 记录登录信息
*
* @param username 用户名
* @param status 状态
* @param message 消息内容
* @return
*/
public void recordLogininfor(String username, String status, String message)
{
SysLogininfor logininfor = new SysLogininfor();
logininfor.setUserName(username);
logininfor.setIpaddr(IpUtils.getIpAddr(ServletUtils.getRequest()));
logininfor.setMsg(message);
// 日志状态
if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
{
logininfor.setStatus("0");
}
else if (Constants.LOGIN_FAIL.equals(status))
{
logininfor.setStatus("1");
}
remoteLogService.saveLogininfor(logininfor, SecurityConstants.INNER);
}
}

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -7,33 +7,8 @@ package com.ruoyi.common.core.constant;
*/
public class CacheConstants
{
/**
* 令牌自定义标识
*/
public static final String HEADER = "Authorization";
/**
* 令牌前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 权限缓存前缀
*/
public final static String LOGIN_TOKEN_KEY = "login_tokens:";
/**
* 用户ID字段
*/
public static final String DETAILS_USER_ID = "user_id";
/**
* 用户名字段
*/
public static final String DETAILS_USERNAME = "username";
/**
* 授权信息字段
*/
public static final String AUTHORIZATION_HEADER = "authorization";
}

View File

@@ -17,6 +17,11 @@ public class Constants
*/
public static final String GBK = "GBK";
/**
* RMI 远程方法调用
*/
public static final String LOOKUP_RMI = "rmi://";
/**
* http请求
*/

View File

@@ -0,0 +1,44 @@
package com.ruoyi.common.core.constant;
/**
* 权限相关通用常量
*
* @author ruoyi
*/
public class SecurityConstants
{
/**
* 令牌自定义标识
*/
public static final String TOKEN_AUTHENTICATION = "Authorization";
/**
* 令牌前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 用户ID字段
*/
public static final String DETAILS_USER_ID = "user_id";
/**
* 用户名字段
*/
public static final String DETAILS_USERNAME = "username";
/**
* 授权信息字段
*/
public static final String AUTHORIZATION_HEADER = "authorization";
/**
* 请求来源
*/
public static final String FROM_SOURCE = "from-source";
/**
* 内部请求
*/
public static final String INNER = "inner";
}

View File

@@ -57,6 +57,9 @@ public class UserConstants
/** ParentView组件标识 */
public final static String PARENT_VIEW = "ParentView";
/** InnerLink组件标识 */
public final static String INNER_LINK = "InnerLink";
/** 校验返回结果码 */
public final static String UNIQUE = "0";

View File

@@ -0,0 +1,16 @@
package com.ruoyi.common.core.exception;
/**
* 内部认证异常
*
* @author ruoyi
*/
public class InnerAuthException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public InnerAuthException(String message)
{
super(message);
}
}

View File

@@ -2,7 +2,7 @@ package com.ruoyi.common.core.utils;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.text.Convert;
/**
@@ -17,7 +17,7 @@ public class SecurityUtils
*/
public static String getUsername()
{
String username = ServletUtils.getRequest().getHeader(CacheConstants.DETAILS_USERNAME);
String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
return ServletUtils.urlDecode(username);
}
@@ -26,7 +26,7 @@ public class SecurityUtils
*/
public static Long getUserId()
{
return Convert.toLong(ServletUtils.getRequest().getHeader(CacheConstants.DETAILS_USER_ID));
return Convert.toLong(ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID));
}
/**
@@ -42,10 +42,18 @@ public class SecurityUtils
*/
public static String getToken(HttpServletRequest request)
{
String token = ServletUtils.getRequest().getHeader(CacheConstants.HEADER);
if (StringUtils.isNotEmpty(token) && token.startsWith(CacheConstants.TOKEN_PREFIX))
String token = request.getHeader(SecurityConstants.TOKEN_AUTHENTICATION);
return replaceTokenPrefix(token);
}
/**
* 替换token前缀
*/
public static String replaceTokenPrefix(String token)
{
token = token.replace(CacheConstants.TOKEN_PREFIX, "");
if (StringUtils.isNotEmpty(token) && token.startsWith(SecurityConstants.TOKEN_PREFIX))
{
token = token.replace(SecurityConstants.TOKEN_PREFIX, "");
}
return token;
}

View File

@@ -10,11 +10,19 @@ import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.text.Convert;
import reactor.core.publisher.Mono;
/**
* 客户端工具类
@@ -213,4 +221,62 @@ public class ServletUtils
return "";
}
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value)
{
return webFluxResponseWriter(response, HttpStatus.OK, value, R.FAIL);
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param code 响应状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value, int code)
{
return webFluxResponseWriter(response, HttpStatus.OK, value, code);
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param status http状态码
* @param code 响应状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, HttpStatus status, Object value, int code)
{
return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code);
}
/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param contentType content-type
* @param status http状态码
* @param code 响应状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code)
{
response.setStatusCode(status);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
R<?> result = R.fail(code, value.toString());
DataBuffer dataBuffer = response.bufferFactory().wrap(JSONObject.toJSONString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
}

View File

@@ -4,6 +4,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.util.AntPathMatcher;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.text.StrFormatter;
/**
@@ -282,6 +283,17 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
return StrFormatter.format(template, params);
}
/**
* 是否为http(s)://开头
*
* @param link 链接
* @return 结果
*/
public static boolean ishttp(String link)
{
return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS);
}
/**
* 驼峰转下划线命名
*/

View File

@@ -0,0 +1,155 @@
package com.ruoyi.common.core.utils.html;
import com.ruoyi.common.core.utils.StringUtils;
/**
* 转义和反转义工具类
*
* @author ruoyi
*/
public class EscapeUtil
{
public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
private static final char[][] TEXT = new char[64][];
static
{
for (int i = 0; i < 64; i++)
{
TEXT[i] = new char[] { (char) i };
}
// special HTML characters
TEXT['\''] = "&#039;".toCharArray(); // 单引号
TEXT['"'] = "&#34;".toCharArray(); // 双引号
TEXT['&'] = "&#38;".toCharArray(); // &符
TEXT['<'] = "&#60;".toCharArray(); // 小于号
TEXT['>'] = "&#62;".toCharArray(); // 大于号
}
/**
* 转义文本中的HTML字符为安全的字符
*
* @param text 被转义的文本
* @return 转义后的文本
*/
public static String escape(String text)
{
return encode(text);
}
/**
* 还原被转义的HTML特殊字符
*
* @param content 包含转义符的HTML内容
* @return 转换后的字符串
*/
public static String unescape(String content)
{
return decode(content);
}
/**
* 清除所有HTML标签但是不删除标签内的内容
*
* @param content 文本
* @return 清除标签后的文本
*/
public static String clean(String content)
{
return new HTMLFilter().filter(content);
}
/**
* Escape编码
*
* @param text 被编码的文本
* @return 编码后的字符
*/
private static String encode(String text)
{
int len;
if ((text == null) || ((len = text.length()) == 0))
{
return StringUtils.EMPTY;
}
StringBuilder buffer = new StringBuilder(len + (len >> 2));
char c;
for (int i = 0; i < len; i++)
{
c = text.charAt(i);
if (c < 64)
{
buffer.append(TEXT[c]);
}
else
{
buffer.append(c);
}
}
return buffer.toString();
}
/**
* Escape解码
*
* @param content 被转义的内容
* @return 解码后的字符串
*/
public static String decode(String content)
{
if (StringUtils.isEmpty(content))
{
return content;
}
StringBuilder tmp = new StringBuilder(content.length());
int lastPos = 0, pos = 0;
char ch;
while (lastPos < content.length())
{
pos = content.indexOf("%", lastPos);
if (pos == lastPos)
{
if (content.charAt(pos + 1) == 'u')
{
ch = (char) Integer.parseInt(content.substring(pos + 2, pos + 6), 16);
tmp.append(ch);
lastPos = pos + 6;
}
else
{
ch = (char) Integer.parseInt(content.substring(pos + 1, pos + 3), 16);
tmp.append(ch);
lastPos = pos + 3;
}
}
else
{
if (pos == -1)
{
tmp.append(content.substring(lastPos));
lastPos = content.length();
}
else
{
tmp.append(content.substring(lastPos, pos));
lastPos = pos;
}
}
}
return tmp.toString();
}
public static void main(String[] args)
{
String html = "<script>alert(1);</script>";
// String html = "<scr<script>ipt>alert(\"XSS\")</scr<script>ipt>";
// String html = "<123";
// String html = "123>";
System.out.println(EscapeUtil.clean(html));
System.out.println(EscapeUtil.escape(html));
System.out.println(EscapeUtil.unescape(html));
}
}

View File

@@ -0,0 +1,570 @@
package com.ruoyi.common.core.utils.html;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* HTML过滤器用于去除XSS漏洞隐患。
*
* @author ruoyi
*/
public final class HTMLFilter
{
/**
* regex flag union representing /si modifiers in php
**/
private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
private static final Pattern P_COMMENTS = Pattern.compile("<!--(.*?)-->", Pattern.DOTALL);
private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?");
private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?");
private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
private static final Pattern P_END_ARROW = Pattern.compile("^>");
private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
private static final Pattern P_AMP = Pattern.compile("&");
private static final Pattern P_QUOTE = Pattern.compile("\"");
private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
// @xxx could grow large... maybe use sesat's ReferenceMap
private static final ConcurrentMap<String, Pattern> P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap<>();
private static final ConcurrentMap<String, Pattern> P_REMOVE_SELF_BLANKS = new ConcurrentHashMap<>();
/**
* set of allowed html elements, along with allowed attributes for each element
**/
private final Map<String, List<String>> vAllowed;
/**
* counts of open tags for each (allowable) html element
**/
private final Map<String, Integer> vTagCounts = new HashMap<>();
/**
* html elements which must always be self-closing (e.g. "<img />")
**/
private final String[] vSelfClosingTags;
/**
* html elements which must always have separate opening and closing tags (e.g. "<b></b>")
**/
private final String[] vNeedClosingTags;
/**
* set of disallowed html elements
**/
private final String[] vDisallowed;
/**
* attributes which should be checked for valid protocols
**/
private final String[] vProtocolAtts;
/**
* allowed protocols
**/
private final String[] vAllowedProtocols;
/**
* tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />")
**/
private final String[] vRemoveBlanks;
/**
* entities allowed within html markup
**/
private final String[] vAllowedEntities;
/**
* flag determining whether comments are allowed in input String.
*/
private final boolean stripComment;
private final boolean encodeQuotes;
/**
* flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "<b text </b>"
* becomes "<b> text </b>"). If set to false, unbalanced angle brackets will be html escaped.
*/
private final boolean alwaysMakeTags;
/**
* Default constructor.
*/
public HTMLFilter()
{
vAllowed = new HashMap<>();
final ArrayList<String> a_atts = new ArrayList<>();
a_atts.add("href");
a_atts.add("target");
vAllowed.put("a", a_atts);
final ArrayList<String> img_atts = new ArrayList<>();
img_atts.add("src");
img_atts.add("width");
img_atts.add("height");
img_atts.add("alt");
vAllowed.put("img", img_atts);
final ArrayList<String> no_atts = new ArrayList<>();
vAllowed.put("b", no_atts);
vAllowed.put("strong", no_atts);
vAllowed.put("i", no_atts);
vAllowed.put("em", no_atts);
vSelfClosingTags = new String[] { "img" };
vNeedClosingTags = new String[] { "a", "b", "strong", "i", "em" };
vDisallowed = new String[] {};
vAllowedProtocols = new String[] { "http", "mailto", "https" }; // no ftp.
vProtocolAtts = new String[] { "src", "href" };
vRemoveBlanks = new String[] { "a", "b", "strong", "i", "em" };
vAllowedEntities = new String[] { "amp", "gt", "lt", "quot" };
stripComment = true;
encodeQuotes = true;
alwaysMakeTags = false;
}
/**
* Map-parameter configurable constructor.
*
* @param conf map containing configuration. keys match field names.
*/
@SuppressWarnings("unchecked")
public HTMLFilter(final Map<String, Object> conf)
{
assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags";
assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed";
assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols";
assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts";
assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks";
assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities";
vAllowed = Collections.unmodifiableMap((HashMap<String, List<String>>) conf.get("vAllowed"));
vSelfClosingTags = (String[]) conf.get("vSelfClosingTags");
vNeedClosingTags = (String[]) conf.get("vNeedClosingTags");
vDisallowed = (String[]) conf.get("vDisallowed");
vAllowedProtocols = (String[]) conf.get("vAllowedProtocols");
vProtocolAtts = (String[]) conf.get("vProtocolAtts");
vRemoveBlanks = (String[]) conf.get("vRemoveBlanks");
vAllowedEntities = (String[]) conf.get("vAllowedEntities");
stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true;
encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true;
alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true;
}
private void reset()
{
vTagCounts.clear();
}
// ---------------------------------------------------------------
// my versions of some PHP library functions
public static String chr(final int decimal)
{
return String.valueOf((char) decimal);
}
public static String htmlSpecialChars(final String s)
{
String result = s;
result = regexReplace(P_AMP, "&amp;", result);
result = regexReplace(P_QUOTE, "&quot;", result);
result = regexReplace(P_LEFT_ARROW, "&lt;", result);
result = regexReplace(P_RIGHT_ARROW, "&gt;", result);
return result;
}
// ---------------------------------------------------------------
/**
* given a user submitted input String, filter out any invalid or restricted html.
*
* @param input text (i.e. submitted by a user) than may contain html
* @return "clean" version of input, with only valid, whitelisted html elements allowed
*/
public String filter(final String input)
{
reset();
String s = input;
s = escapeComments(s);
s = balanceHTML(s);
s = checkTags(s);
s = processRemoveBlanks(s);
// s = validateEntities(s);
return s;
}
public boolean isAlwaysMakeTags()
{
return alwaysMakeTags;
}
public boolean isStripComments()
{
return stripComment;
}
private String escapeComments(final String s)
{
final Matcher m = P_COMMENTS.matcher(s);
final StringBuffer buf = new StringBuffer();
if (m.find())
{
final String match = m.group(1); // (.*?)
m.appendReplacement(buf, Matcher.quoteReplacement("<!--" + htmlSpecialChars(match) + "-->"));
}
m.appendTail(buf);
return buf.toString();
}
private String balanceHTML(String s)
{
if (alwaysMakeTags)
{
//
// try and form html
//
s = regexReplace(P_END_ARROW, "", s);
// 不追加结束标签
s = regexReplace(P_BODY_TO_END, "<$1>", s);
s = regexReplace(P_XML_CONTENT, "$1<$2", s);
}
else
{
//
// escape stray brackets
//
s = regexReplace(P_STRAY_LEFT_ARROW, "&lt;$1", s);
s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2&gt;<", s);
//
// the last regexp causes '<>' entities to appear
// (we need to do a lookahead assertion so that the last bracket can
// be used in the next pass of the regexp)
//
s = regexReplace(P_BOTH_ARROWS, "", s);
}
return s;
}
private String checkTags(String s)
{
Matcher m = P_TAGS.matcher(s);
final StringBuffer buf = new StringBuffer();
while (m.find())
{
String replaceStr = m.group(1);
replaceStr = processTag(replaceStr);
m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));
}
m.appendTail(buf);
// these get tallied in processTag
// (remember to reset before subsequent calls to filter method)
final StringBuilder sBuilder = new StringBuilder(buf.toString());
for (String key : vTagCounts.keySet())
{
for (int ii = 0; ii < vTagCounts.get(key); ii++)
{
sBuilder.append("</").append(key).append(">");
}
}
s = sBuilder.toString();
return s;
}
private String processRemoveBlanks(final String s)
{
String result = s;
for (String tag : vRemoveBlanks)
{
if (!P_REMOVE_PAIR_BLANKS.containsKey(tag))
{
P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?></" + tag + ">"));
}
result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result);
if (!P_REMOVE_SELF_BLANKS.containsKey(tag))
{
P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>"));
}
result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result);
}
return result;
}
private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s)
{
Matcher m = regex_pattern.matcher(s);
return m.replaceAll(replacement);
}
private String processTag(final String s)
{
// ending tags
Matcher m = P_END_TAG.matcher(s);
if (m.find())
{
final String name = m.group(1).toLowerCase();
if (allowed(name))
{
if (false == inArray(name, vSelfClosingTags))
{
if (vTagCounts.containsKey(name))
{
vTagCounts.put(name, vTagCounts.get(name) - 1);
return "</" + name + ">";
}
}
}
}
// starting tags
m = P_START_TAG.matcher(s);
if (m.find())
{
final String name = m.group(1).toLowerCase();
final String body = m.group(2);
String ending = m.group(3);
// debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
if (allowed(name))
{
final StringBuilder params = new StringBuilder();
final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);
final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);
final List<String> paramNames = new ArrayList<>();
final List<String> paramValues = new ArrayList<>();
while (m2.find())
{
paramNames.add(m2.group(1)); // ([a-z0-9]+)
paramValues.add(m2.group(3)); // (.*?)
}
while (m3.find())
{
paramNames.add(m3.group(1)); // ([a-z0-9]+)
paramValues.add(m3.group(3)); // ([^\"\\s']+)
}
String paramName, paramValue;
for (int ii = 0; ii < paramNames.size(); ii++)
{
paramName = paramNames.get(ii).toLowerCase();
paramValue = paramValues.get(ii);
// debug( "paramName='" + paramName + "'" );
// debug( "paramValue='" + paramValue + "'" );
// debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
if (allowedAttribute(name, paramName))
{
if (inArray(paramName, vProtocolAtts))
{
paramValue = processParamProtocol(paramValue);
}
params.append(' ').append(paramName).append("=\"").append(paramValue).append("\"");
}
}
if (inArray(name, vSelfClosingTags))
{
ending = " /";
}
if (inArray(name, vNeedClosingTags))
{
ending = "";
}
if (ending == null || ending.length() < 1)
{
if (vTagCounts.containsKey(name))
{
vTagCounts.put(name, vTagCounts.get(name) + 1);
}
else
{
vTagCounts.put(name, 1);
}
}
else
{
ending = " /";
}
return "<" + name + params + ending + ">";
}
else
{
return "";
}
}
// comments
m = P_COMMENT.matcher(s);
if (!stripComment && m.find())
{
return "<" + m.group() + ">";
}
return "";
}
private String processParamProtocol(String s)
{
s = decodeEntities(s);
final Matcher m = P_PROTOCOL.matcher(s);
if (m.find())
{
final String protocol = m.group(1);
if (!inArray(protocol, vAllowedProtocols))
{
// bad protocol, turn into local anchor link instead
s = "#" + s.substring(protocol.length() + 1);
if (s.startsWith("#//"))
{
s = "#" + s.substring(3);
}
}
}
return s;
}
private String decodeEntities(String s)
{
StringBuffer buf = new StringBuffer();
Matcher m = P_ENTITY.matcher(s);
while (m.find())
{
final String match = m.group(1);
final int decimal = Integer.decode(match).intValue();
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
}
m.appendTail(buf);
s = buf.toString();
buf = new StringBuffer();
m = P_ENTITY_UNICODE.matcher(s);
while (m.find())
{
final String match = m.group(1);
final int decimal = Integer.valueOf(match, 16).intValue();
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
}
m.appendTail(buf);
s = buf.toString();
buf = new StringBuffer();
m = P_ENCODE.matcher(s);
while (m.find())
{
final String match = m.group(1);
final int decimal = Integer.valueOf(match, 16).intValue();
m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
}
m.appendTail(buf);
s = buf.toString();
s = validateEntities(s);
return s;
}
private String validateEntities(final String s)
{
StringBuffer buf = new StringBuffer();
// validate entities throughout the string
Matcher m = P_VALID_ENTITIES.matcher(s);
while (m.find())
{
final String one = m.group(1); // ([^&;]*)
final String two = m.group(2); // (?=(;|&|$))
m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));
}
m.appendTail(buf);
return encodeQuotes(buf.toString());
}
private String encodeQuotes(final String s)
{
if (encodeQuotes)
{
StringBuffer buf = new StringBuffer();
Matcher m = P_VALID_QUOTES.matcher(s);
while (m.find())
{
final String one = m.group(1); // (>|^)
final String two = m.group(2); // ([^<]+?)
final String three = m.group(3); // (<|$)
// 不替换双引号为&quot;防止json格式无效 regexReplace(P_QUOTE, "&quot;", two)
m.appendReplacement(buf, Matcher.quoteReplacement(one + two + three));
}
m.appendTail(buf);
return buf.toString();
}
else
{
return s;
}
}
private String checkEntity(final String preamble, final String term)
{
return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&amp;" + preamble;
}
private boolean isValidEntity(final String entity)
{
return inArray(entity, vAllowedEntities);
}
private static boolean inArray(final String s, final String[] array)
{
for (String item : array)
{
if (item != null && item.equals(s))
{
return true;
}
}
return false;
}
private boolean allowed(final String name)
{
return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);
}
private boolean allowedAttribute(final String name, final String paramName)
{
return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,6 +3,7 @@ package com.ruoyi.common.log.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.system.api.RemoteLogService;
import com.ruoyi.system.api.domain.SysOperLog;
@@ -23,6 +24,6 @@ public class AsyncLogService
@Async
public void saveSysLog(SysOperLog sysOperLog)
{
remoteLogService.saveLog(sysOperLog);
remoteLogService.saveLog(sysOperLog, SecurityConstants.INNER);
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -74,6 +74,17 @@ public class RedisService
return redisTemplate.expire(key, timeout, unit);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -0,0 +1,19 @@
package com.ruoyi.common.security.annotation;
import java.lang.annotation.*;
/**
* 内部认证注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InnerAuth
{
/**
* 是否校验用户信息
*/
boolean isUser() default false;
}

View File

@@ -0,0 +1,51 @@
package com.ruoyi.common.security.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.exception.InnerAuthException;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.security.annotation.InnerAuth;
/**
* 内部服务调用验证处理
*
* @author ruoyi
*/
@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;
}
}

View File

@@ -2,11 +2,11 @@ package com.ruoyi.common.security.feign;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.ruoyi.common.core.utils.ip.IpUtils;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.ip.IpUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
@@ -26,20 +26,20 @@ public class FeignRequestInterceptor implements RequestInterceptor
{
Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
// 传递用户信息请求头,防止丢失
String userId = headers.get(CacheConstants.DETAILS_USER_ID);
String userId = headers.get(SecurityConstants.DETAILS_USER_ID);
if (StringUtils.isNotEmpty(userId))
{
requestTemplate.header(CacheConstants.DETAILS_USER_ID, userId);
requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);
}
String userName = headers.get(CacheConstants.DETAILS_USERNAME);
String userName = headers.get(SecurityConstants.DETAILS_USERNAME);
if (StringUtils.isNotEmpty(userName))
{
requestTemplate.header(CacheConstants.DETAILS_USERNAME, userName);
requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);
}
String authentication = headers.get(CacheConstants.AUTHORIZATION_HEADER);
String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
if (StringUtils.isNotEmpty(authentication))
{
requestTemplate.header(CacheConstants.AUTHORIZATION_HEADER, authentication);
requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
}
// 配置客户端IP

View File

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.ruoyi.common.core.exception.BaseException;
import com.ruoyi.common.core.exception.CustomException;
import com.ruoyi.common.core.exception.DemoModeException;
import com.ruoyi.common.core.exception.InnerAuthException;
import com.ruoyi.common.core.exception.PreAuthorizeException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
@@ -76,6 +77,15 @@ public class GlobalExceptionHandler {
return AjaxResult.error("没有权限,请联系管理员授权");
}
/**
* 内部认证异常
*/
@ExceptionHandler(InnerAuthException.class)
public AjaxResult InnerAuthException(InnerAuthException e)
{
return AjaxResult.error(e.getMessage());
}
/**
* 演示模式异常
*/

View File

@@ -73,6 +73,16 @@ public class TokenService
{
// 获取请求携带的令牌
String token = SecurityUtils.getToken(request);
return getLoginUser(token);
}
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(String token)
{
if (StringUtils.isNotEmpty(token))
{
String userKey = getTokenKey(token);

View File

@@ -1,4 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ruoyi.common.security.service.TokenService,\
com.ruoyi.common.security.aspect.PreAuthorizeAspect,\
com.ruoyi.common.security.aspect.InnerAuthAspect,\
com.ruoyi.common.security.handler.GlobalExceptionHandler

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -0,0 +1,46 @@
package com.ruoyi.gateway.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
/**
* 验证码配置
*
* @author ruoyi
*/
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.captcha")
public class CaptchaProperties
{
/**
* 验证码开关
*/
private Boolean enabled;
/**
* 验证码类型math 数组计算 char 字符)
*/
private String type;
public Boolean getEnabled()
{
return enabled;
}
public void setEnabled(Boolean enabled)
{
this.enabled = enabled;
}
public String getType()
{
return type;
}
public void setType(String type)
{
this.type = type;
}
}

View File

@@ -19,9 +19,8 @@ import org.springframework.context.annotation.Configuration;
@Accessors(chain = true)
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "ignore")
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties {
/**
* 放行白名单配置,网关不校验此处的白名单
*/

View File

@@ -0,0 +1,48 @@
package com.ruoyi.gateway.config.properties;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
/**
* XSS跨站脚本配置
*
* @author ruoyi
*/
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.xss")
public class XssProperties
{
/**
* Xss开关
*/
private Boolean enabled;
/**
* 排除路径
*/
private List<String> excludeUrls = new ArrayList<>();
public Boolean getEnabled()
{
return enabled;
}
public void setEnabled(Boolean enabled)
{
this.enabled = enabled;
}
public List<String> getExcludeUrls()
{
return excludeUrls;
}
public void setExcludeUrls(List<String> excludeUrls)
{
this.excludeUrls = excludeUrls;
}
}

View File

@@ -7,19 +7,16 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.constant.HttpStatus;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.utils.SecurityUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.service.RedisService;
@@ -51,54 +48,68 @@ public class AuthFilter implements GlobalFilter, Ordered
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
String url = exchange.getRequest().getURI().getPath();
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites()))
{
return chain.filter(exchange);
}
String token = getToken(exchange.getRequest());
if (StringUtils.isBlank(token))
String token = getToken(request);
if (StringUtils.isEmpty(token))
{
return setUnauthorizedResponse(exchange, "令牌不能为空");
return unauthorizedResponse(exchange, "令牌不能为空");
}
String userStr = sops.get(getTokenKey(token));
if (StringUtils.isNull(userStr))
if (StringUtils.isEmpty(userStr))
{
return setUnauthorizedResponse(exchange, "登录状态已过期");
return unauthorizedResponse(exchange, "登录状态已过期");
}
JSONObject obj = JSONObject.parseObject(userStr);
String userid = obj.getString("userid");
String username = obj.getString("username");
if (StringUtils.isBlank(userid) || StringUtils.isBlank(username))
JSONObject cacheObj = JSONObject.parseObject(userStr);
String userid = cacheObj.getString("userid");
String username = cacheObj.getString("username");
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))
{
return setUnauthorizedResponse(exchange, "令牌验证失败");
return unauthorizedResponse(exchange, "令牌验证失败");
}
// 设置过期时间
redisService.expire(getTokenKey(token), EXPIRE_TIME);
// 设置用户信息到请求
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header(CacheConstants.DETAILS_USER_ID, userid)
.header(CacheConstants.DETAILS_USERNAME, ServletUtils.urlEncode(username)).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private Mono<Void> setUnauthorizedResponse(ServerWebExchange exchange, String msg)
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value)
{
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(HttpStatus.OK);
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
return bufferFactory.wrap(JSON.toJSONBytes(R.fail(msg)));
}));
if (value == null)
{
return;
}
String valueStr = value.toString();
String valueEncode = ServletUtils.urlEncode(valueStr);
mutate.header(name, valueEncode);
}
private void removeHeader(ServerHttpRequest.Builder mutate, String name)
{
mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg)
{
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
}
/**
* 获取缓存key
*/
private String getTokenKey(String token)
{
return CacheConstants.LOGIN_TOKEN_KEY + token;
@@ -109,12 +120,8 @@ public class AuthFilter implements GlobalFilter, Ordered
*/
private String getToken(ServerHttpRequest request)
{
String token = request.getHeaders().getFirst(CacheConstants.HEADER);
if (StringUtils.isNotEmpty(token) && token.startsWith(CacheConstants.TOKEN_PREFIX))
{
token = token.replace(CacheConstants.TOKEN_PREFIX, "");
}
return token;
String token = request.getHeaders().getFirst(SecurityConstants.TOKEN_AUTHENTICATION);
return SecurityUtils.replaceTokenPrefix(token);
}
@Override

View File

@@ -5,11 +5,8 @@ import java.util.List;
import java.util.regex.Pattern;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.ruoyi.common.core.web.domain.AjaxResult;
import reactor.core.publisher.Mono;
import com.ruoyi.common.core.utils.ServletUtils;
/**
* 黑名单过滤器
@@ -27,10 +24,7 @@ public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUr
String url = exchange.getRequest().getURI().getPath();
if (config.matchBlacklist(url))
{
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return exchange.getResponse().writeWith(
Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes(AjaxResult.error("请求地址不允许访问")))));
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
}
return chain.filter(exchange);

View File

@@ -16,6 +16,11 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 获取body请求数据解决流不能重复读取问题
*
* @author ruoyi
*/
@Component
public class CacheRequestFilter extends AbstractGatewayFilterFactory<CacheRequestFilter.Config>
{

View File

@@ -9,15 +9,13 @@ import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFac
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.gateway.config.properties.CaptchaProperties;
import com.ruoyi.gateway.service.ValidateCodeService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 验证码过滤器
@@ -27,11 +25,14 @@ import reactor.core.publisher.Mono;
@Component
public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object>
{
private final static String AUTH_URL = "/auth/login";
private final static String[] VALIDATE_URL = new String[] { "/auth/login", "/auth/register" };
@Autowired
private ValidateCodeService validateCodeService;
@Autowired
private CaptchaProperties captchaProperties;
private static final String CODE = "code";
private static final String UUID = "uuid";
@@ -42,8 +43,8 @@ public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object>
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 非登录请求,不处理
if (!StringUtils.containsIgnoreCase(request.getURI().getPath(), AUTH_URL))
// 非登录/注册请求或验证码关闭,不处理
if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled())
{
return chain.filter(exchange);
}
@@ -56,10 +57,7 @@ public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object>
}
catch (Exception e)
{
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return exchange.getResponse().writeWith(
Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes(AjaxResult.error(e.getMessage())))));
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
}
return chain.filter(exchange);
};

View File

@@ -0,0 +1,120 @@
package com.ruoyi.gateway.filter;
import java.nio.charset.StandardCharsets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.html.EscapeUtil;
import com.ruoyi.gateway.config.properties.XssProperties;
import io.netty.buffer.ByteBufAllocator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 跨站脚本过滤器
*
* @author ruoyi
*/
@Component
@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
public class XssFilter implements GlobalFilter, Ordered
{
// 跨站脚本的 xss 配置nacos自行添加
@Autowired
private XssProperties xss;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
ServerHttpRequest request = exchange.getRequest();
// GET DELETE 不过滤
HttpMethod method = request.getMethod();
if (method == null || method.matches("GET") || method.matches("DELETE"))
{
return chain.filter(exchange);
}
// 非json类型不过滤
if (!isJsonRequest(exchange))
{
return chain.filter(exchange);
}
// excludeUrls 不过滤
String url = request.getURI().getPath();
if (StringUtils.matches(url, xss.getExcludeUrls()))
{
return chain.filter(exchange);
}
ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
return chain.filter(exchange.mutate().request(httpRequestDecorator).build());
}
private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange)
{
ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest())
{
@Override
public Flux<DataBuffer> getBody()
{
Flux<DataBuffer> body = super.getBody();
return body.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
String bodyStr = new String(content, StandardCharsets.UTF_8);
// 防xss攻击过滤
bodyStr = EscapeUtil.clean(bodyStr);
// 转成字节
byte[] bytes = bodyStr.getBytes();
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
});
}
@Override
public HttpHeaders getHeaders()
{
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
// 由于修改了请求体的body导致content-length长度不确定因此需要删除原先的content-length
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
};
return serverHttpRequestDecorator;
}
/**
* 是否是Json请求
*
* @param request
*/
public boolean isJsonRequest(ServerWebExchange exchange)
{
String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
}
@Override
public int getOrder()
{
return -100;
}
}

View File

@@ -6,14 +6,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import com.alibaba.fastjson.JSON;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.utils.ServletUtils;
import reactor.core.publisher.Mono;
/**
@@ -55,12 +51,6 @@ public class GatewayExceptionHandler implements ErrorWebExceptionHandler
log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(HttpStatus.OK);
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
return bufferFactory.wrap(JSON.toJSONBytes(R.fail(msg)));
}));
return ServletUtils.webFluxResponseWriter(response, msg);
}
}

View File

@@ -1,10 +1,8 @@
package com.ruoyi.gateway.handler;
import java.nio.charset.StandardCharsets;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import com.ruoyi.common.core.utils.ServletUtils;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
@@ -19,11 +17,7 @@ public class SentinelFallbackHandler implements WebExceptionHandler
{
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange)
{
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] datas = "{\"code\":429,\"msg\":\"请求超过最大数,请稍后再试\"}".getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
return serverHttpResponse.writeWith(Mono.just(buffer));
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求超过最大数,请稍后再试");
}
@Override

View File

@@ -16,6 +16,7 @@ import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.sign.Base64;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.gateway.config.properties.CaptchaProperties;
import com.ruoyi.gateway.service.ValidateCodeService;
/**
@@ -35,8 +36,8 @@ public class ValidateCodeServiceImpl implements ValidateCodeService
@Autowired
private RedisService redisService;
// 验证码类型
private String captchaType = "math";
@Autowired
private CaptchaProperties captchaProperties;
/**
* 生成验证码
@@ -44,6 +45,14 @@ public class ValidateCodeServiceImpl implements ValidateCodeService
@Override
public AjaxResult createCapcha() throws IOException, CaptchaException
{
AjaxResult ajax = AjaxResult.success();
boolean captchaOnOff = captchaProperties.getEnabled();
ajax.put("captchaOnOff", captchaOnOff);
if (!captchaOnOff)
{
return ajax;
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
@@ -51,6 +60,7 @@ public class ValidateCodeServiceImpl implements ValidateCodeService
String capStr = null, code = null;
BufferedImage image = null;
String captchaType = captchaProperties.getType();
// 生成验证码
if ("math".equals(captchaType))
{
@@ -77,7 +87,6 @@ public class ValidateCodeServiceImpl implements ValidateCodeService
return AjaxResult.error(e.getMessage());
}
AjaxResult ajax = AjaxResult.success();
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;

View File

@@ -4,7 +4,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -12,11 +12,11 @@ import com.ruoyi.common.swagger.annotation.EnableCustomSwagger2;
*/
@EnableCustomSwagger2
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
public class RuoYFileApplication
public class RuoYiFileApplication
{
public static void main(String[] args)
{
SpringApplication.run(RuoYFileApplication.class, args);
SpringApplication.run(RuoYiFileApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 文件服务模块启动成功 ლ(´ڡ`ლ)゙ \n" +
" .-------. ____ __ \n" +
" | _ _ \\ \\ \\ / / \n" +

View File

@@ -4,6 +4,7 @@ import java.io.File;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -33,4 +34,17 @@ public class ResourcesConfig implements WebMvcConfigurer {
registry.addResourceHandler(localFilePrefix + "/**")
.addResourceLocations("file:" + localFilePath + File.separator);
}
/**
* 开启跨域
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路由
registry.addMapping(localFilePrefix + "/**")
// 设置允许跨域请求的域名
.allowedOrigins("*")
// 设置允许的方法
.allowedMethods("GET");
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -280,7 +280,8 @@ public class VelocityUtils
*/
public static String getParentMenuId(JSONObject paramsObj)
{
if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID))
if (StringUtils.isNotEmpty(paramsObj) && paramsObj.containsKey(GenConstants.PARENT_MENU_ID)
&& StringUtils.isNotEmpty(paramsObj.getString(GenConstants.PARENT_MENU_ID)))
{
return paramsObj.getString(GenConstants.PARENT_MENU_ID);
}

View File

@@ -78,7 +78,7 @@ public class ${ClassName}Controller extends BaseController
@GetMapping(value = "/{${pkColumn.javaField}}")
public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField})
{
return AjaxResult.success(${className}Service.select${ClassName}ById(${pkColumn.javaField}));
return AjaxResult.success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}));
}
/**
@@ -111,6 +111,6 @@ public class ${ClassName}Controller extends BaseController
@DeleteMapping("/{${pkColumn.javaField}s}")
public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s)
{
return toAjax(${className}Service.delete${ClassName}ByIds(${pkColumn.javaField}s));
return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s));
}
}

View File

@@ -17,10 +17,10 @@ public interface ${ClassName}Mapper
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @param ${pkColumn.javaField} ${functionName}主键
* @return ${functionName}
*/
public ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 查询${functionName}列表
@@ -49,27 +49,27 @@ public interface ${ClassName}Mapper
/**
* 删除${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @param ${pkColumn.javaField} ${functionName}主键
* @return 结果
*/
public int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 批量删除${functionName}
*
* @param ${pkColumn.javaField}s 需要删除的数据ID
* @param ${pkColumn.javaField}s 需要删除的数据主键集合
* @return 结果
*/
public int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s);
public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
#if($table.sub)
/**
* 批量删除${subTable.functionName}
*
* @param customerIds 需要删除的数据ID
* @param ${pkColumn.javaField}s 需要删除的数据主键集合
* @return 结果
*/
public int delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
public int delete${subClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
/**
* 批量新增${subTable.functionName}
@@ -81,7 +81,7 @@ public interface ${ClassName}Mapper
/**
* 通过${functionName}ID删除${subTable.functionName}信息
* 通过${functionName}主键删除${subTable.functionName}信息
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return 结果

View File

@@ -14,10 +14,10 @@ public interface I${ClassName}Service
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @param ${pkColumn.javaField} ${functionName}主键
* @return ${functionName}
*/
public ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 查询${functionName}列表
@@ -46,16 +46,16 @@ public interface I${ClassName}Service
/**
* 批量删除${functionName}
*
* @param ${pkColumn.javaField}s 需要删除的${functionName}ID
* @param ${pkColumn.javaField}s 需要删除的${functionName}主键集合
* @return 结果
*/
public int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s);
public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s);
/**
* 删除${functionName}信息
*
* @param ${pkColumn.javaField} ${functionName}ID
* @param ${pkColumn.javaField} ${functionName}主键
* @return 结果
*/
public int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField});
}

View File

@@ -34,13 +34,13 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @param ${pkColumn.javaField} ${functionName}主键
* @return ${functionName}
*/
@Override
public ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField})
public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
{
return ${className}Mapper.select${ClassName}ById(${pkColumn.javaField});
return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField});
}
/**
@@ -108,34 +108,34 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
/**
* 批量删除${functionName}
*
* @param ${pkColumn.javaField}s 需要删除的${functionName}ID
* @param ${pkColumn.javaField}s 需要删除的${functionName}主键
* @return 结果
*/
#if($table.sub)
@Transactional
#end
@Override
public int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s)
public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s)
{
#if($table.sub)
${className}Mapper.delete${subClassName}By${subTableFkClassName}s(${pkColumn.javaField}s);
#end
return ${className}Mapper.delete${ClassName}ByIds(${pkColumn.javaField}s);
return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s);
}
/**
* 删除${functionName}信息
*
* @param ${pkColumn.javaField} ${functionName}ID
* @param ${pkColumn.javaField} ${functionName}主键
* @return 结果
*/
@Override
public int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField})
public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
{
#if($table.sub)
${className}Mapper.delete${subClassName}By${subTableFkClassName}(${pkColumn.javaField});
#end
return ${className}Mapper.delete${ClassName}ById(${pkColumn.javaField});
return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField});
}
#if($table.sub)
@@ -147,7 +147,7 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
public void insert${subClassName}(${ClassName} ${className})
{
List<${subClassName}> ${subclassName}List = ${className}.get${subClassName}List();
Long ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}();
${pkColumn.javaType} ${pkColumn.javaField} = ${className}.get${pkColumn.capJavaField}();
if (StringUtils.isNotNull(${subclassName}List))
{
List<${subClassName}> list = new ArrayList<${subClassName}>();

View File

@@ -258,46 +258,10 @@
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName}, export${BusinessName} } from "@/api/${moduleName}/${businessName}";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "imageUpload")
import ImageUpload from '@/components/ImageUpload';
#break
#end
#end
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "fileUpload")
import FileUpload from '@/components/FileUpload';
#break
#end
#end
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "editor")
import Editor from '@/components/Editor';
#break
#end
#end
export default {
name: "${BusinessName}",
components: {
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "imageUpload")
ImageUpload,
#break
#end
#end
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "fileUpload")
FileUpload,
#break
#end
#end
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "editor")
Editor,
#break
#end
#end
Treeselect
},
data() {

View File

@@ -309,47 +309,9 @@
<script>
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "imageUpload")
import ImageUpload from '@/components/ImageUpload';
#break
#end
#end
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "fileUpload")
import FileUpload from '@/components/FileUpload';
#break
#end
#end
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "editor")
import Editor from '@/components/Editor';
#break
#end
#end
export default {
name: "${BusinessName}",
components: {
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "imageUpload")
ImageUpload,
#break
#end
#end
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "fileUpload")
FileUpload,
#break
#end
#end
#foreach($column in $columns)
#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "editor")
Editor,
#break
#end
#end
},
data() {
return {
// 遮罩层

View File

@@ -58,7 +58,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</where>
</select>
<select id="select${ClassName}ById" parameterType="${pkColumn.javaType}" resultMap="#if($table.sub)${ClassName}${subClassName}Result#else${ClassName}Result#end">
<select id="select${ClassName}By${pkColumn.capJavaField}" parameterType="${pkColumn.javaType}" resultMap="#if($table.sub)${ClassName}${subClassName}Result#else${ClassName}Result#end">
#if($table.crud || $table.tree)
<include refid="select${ClassName}Vo"/>
where ${pkColumn.columnName} = #{${pkColumn.javaField}}
@@ -102,11 +102,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where ${pkColumn.columnName} = #{${pkColumn.javaField}}
</update>
<delete id="delete${ClassName}ById" parameterType="${pkColumn.javaType}">
<delete id="delete${ClassName}By${pkColumn.capJavaField}" parameterType="${pkColumn.javaType}">
delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}}
</delete>
<delete id="delete${ClassName}ByIds" parameterType="String">
<delete id="delete${ClassName}By${pkColumn.capJavaField}s" parameterType="String">
delete from ${tableName} where ${pkColumn.columnName} in
<foreach item="${pkColumn.javaField}" collection="array" open="(" separator="," close=")">
#{${pkColumn.javaField}}
@@ -121,7 +121,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</delete>
<delete id="delete${subClassName}By${subTableFkClassName}" parameterType="Long">
<delete id="delete${subClassName}By${subTableFkClassName}" parameterType="${pkColumn.javaType}">
delete from ${subTableName} where ${subTableFkName} = #{${subTableFkclassName}}
</delete>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -13,8 +13,10 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.exception.job.TaskException;
import com.ruoyi.common.core.utils.SecurityUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.poi.ExcelUtil;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.AjaxResult;
@@ -79,14 +81,22 @@ public class SysJobController extends BaseController
@PreAuthorize(hasPermi = "monitor:job:add")
@Log(title = "定时任务", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysJob sysJob) throws SchedulerException, TaskException
public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException
{
if (!CronUtils.isValid(sysJob.getCronExpression()))
if (!CronUtils.isValid(job.getCronExpression()))
{
return AjaxResult.error("cron表达式不正确");
return error("新增任务'" + job.getJobName() + "'失败Cron表达式不正确");
}
sysJob.setCreateBy(SecurityUtils.getUsername());
return toAjax(jobService.insertJob(sysJob));
else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
{
return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi://'调用");
}
else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
{
return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)//'调用");
}
job.setCreateBy(SecurityUtils.getUsername());
return toAjax(jobService.insertJob(job));
}
/**
@@ -95,14 +105,22 @@ public class SysJobController extends BaseController
@PreAuthorize(hasPermi = "monitor:job:edit")
@Log(title = "定时任务", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysJob sysJob) throws SchedulerException, TaskException
public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException
{
if (!CronUtils.isValid(sysJob.getCronExpression()))
if (!CronUtils.isValid(job.getCronExpression()))
{
return AjaxResult.error("cron表达式不正确");
return error("修改任务'" + job.getJobName() + "'失败Cron表达式不正确");
}
sysJob.setUpdateBy(SecurityUtils.getUsername());
return toAjax(jobService.updateJob(sysJob));
else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
{
return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi://'调用");
}
else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
{
return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)//'调用");
}
job.setUpdateBy(SecurityUtils.getUsername());
return toAjax(jobService.updateJob(job));
}
/**

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>3.0.0</version>
<version>3.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -8,20 +8,18 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.ip.IpUtils;
import com.ruoyi.common.core.utils.poi.ExcelUtil;
import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.TableDataInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.security.annotation.InnerAuth;
import com.ruoyi.common.security.annotation.PreAuthorize;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.api.domain.SysLogininfor;
import com.ruoyi.system.service.ISysLogininforService;
/**
@@ -72,26 +70,10 @@ public class SysLogininforController extends BaseController
return AjaxResult.success();
}
@InnerAuth
@PostMapping
public AjaxResult add(@RequestParam("username") String username, @RequestParam("status") String status,
@RequestParam("message") String message)
public AjaxResult add(@RequestBody SysLogininfor logininfor)
{
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
// 封装对象
SysLogininfor logininfor = new SysLogininfor();
logininfor.setUserName(username);
logininfor.setIpaddr(ip);
logininfor.setMsg(message);
// 日志状态
if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
{
logininfor.setStatus("0");
}
else if (Constants.LOGIN_FAIL.equals(status))
{
logininfor.setStatus("1");
}
return toAjax(logininforService.insertLogininfor(logininfor));
}
}

View File

@@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.utils.SecurityUtils;
import com.ruoyi.common.core.utils.StringUtils;
@@ -94,8 +93,7 @@ public class SysMenuController extends BaseController
{
return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
}
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame())
&& !StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS))
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
{
return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
}
@@ -115,8 +113,7 @@ public class SysMenuController extends BaseController
{
return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
}
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame())
&& !StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS))
else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
{
return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
}

View File

@@ -17,6 +17,7 @@ import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.TableDataInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.security.annotation.InnerAuth;
import com.ruoyi.common.security.annotation.PreAuthorize;
import com.ruoyi.system.api.domain.SysOperLog;
import com.ruoyi.system.service.ISysOperLogService;
@@ -69,6 +70,7 @@ public class SysOperlogController extends BaseController
return AjaxResult.success();
}
@InnerAuth
@PostMapping
public AjaxResult add(@RequestBody SysOperLog operLog)
{

View File

@@ -75,9 +75,12 @@ public class SysProfileController extends BaseController
{
return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
LoginUser loginUser = tokenService.getLoginUser();
SysUser sysUser = loginUser.getSysUser();
user.setUserId(sysUser.getUserId());
user.setPassword(null);
if (userService.updateUserProfile(user) > 0)
{
LoginUser loginUser = tokenService.getLoginUser();
// 更新缓存用户信息
loginUser.getSysUser().setNickName(user.getNickName());
loginUser.getSysUser().setPhonenumber(user.getPhonenumber());

View File

@@ -5,6 +5,7 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -26,10 +27,12 @@ import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.TableDataInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.security.annotation.InnerAuth;
import com.ruoyi.common.security.annotation.PreAuthorize;
import com.ruoyi.system.api.domain.SysRole;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.model.LoginUser;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysPermissionService;
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
@@ -56,6 +59,9 @@ public class SysUserController extends BaseController
@Autowired
private ISysPermissionService permissionService;
@Autowired
private ISysConfigService configService;
/**
* 获取用户列表
*/
@@ -100,6 +106,7 @@ public class SysUserController extends BaseController
/**
* 获取当前用户信息
*/
@InnerAuth
@GetMapping("/info/{username}")
public R<LoginUser> info(@PathVariable("username") String username)
{
@@ -119,6 +126,25 @@ public class SysUserController extends BaseController
return R.ok(sysUserVo);
}
/**
* 注册用户信息
*/
@InnerAuth
@PostMapping("/register")
public R<Boolean> register(@RequestBody SysUser sysUser)
{
String username = sysUser.getUserName();
if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser"))))
{
return R.fail("当前系统没有开启注册功能!");
}
if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(username)))
{
return R.fail("保存用户'" + username + "'失败,注册账号已存在");
}
return R.ok(userService.registerUser(sysUser));
}
/**
* 获取用户信息
*
@@ -217,6 +243,10 @@ public class SysUserController extends BaseController
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds)
{
if (ArrayUtils.contains(userIds, SecurityUtils.getUserId()))
{
return AjaxResult.error("当前用户不能删除");
}
return toAjax(userService.deleteUserByIds(userIds));
}

View File

@@ -3,6 +3,8 @@ package com.ruoyi.system.domain.vo;
import lombok.*;
import lombok.experimental.Accessors;
import com.ruoyi.common.core.utils.StringUtils;
/**
* 路由显示信息
*
@@ -28,6 +30,11 @@ public class MetaVo {
*/
private boolean noCache;
/**
* 内链地址http(s)://开头)
*/
private String link;
public MetaVo(String title, String icon) {
this.title = title;
this.icon = icon;
@@ -39,4 +46,18 @@ public class MetaVo {
this.noCache = noCache;
}
public MetaVo(String title, String icon, String link) {
this.title = title;
this.icon = icon;
this.link = link;
}
public MetaVo(String title, String icon, boolean noCache, String link) {
this.title = title;
this.icon = icon;
this.noCache = noCache;
if (StringUtils.ishttp(link)) {
this.link = link;
}
}
}

View File

@@ -1,7 +1,7 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.api.domain.SysLogininfor;
/**
* 系统访问日志情况信息 数据层

View File

@@ -1,7 +1,7 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.api.domain.SysLogininfor;
/**
* 系统访问日志情况信息 服务层

View File

@@ -105,6 +105,14 @@ public interface ISysUserService
*/
public int insertUser(SysUser user);
/**
* 注册用户信息
*
* @param user 用户信息
* @return 结果
*/
public boolean registerUser(SysUser user);
/**
* 修改用户信息
*

View File

@@ -209,7 +209,8 @@ public class SysDeptServiceImpl implements ISysDeptService
updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors);
}
int result = deptMapper.updateDept(dept);
if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()))
if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors())
&& !StringUtils.equals("0", dept.getAncestors()))
{
// 如果该部门是启用状态,则启用该部门的所有上级部门
updateParentDeptStatusNormal(dept);

View File

@@ -3,7 +3,7 @@ package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.api.domain.SysLogininfor;
import com.ruoyi.system.mapper.SysLogininforMapper;
import com.ruoyi.system.service.ISysLogininforService;

View File

@@ -10,6 +10,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.utils.SecurityUtils;
import com.ruoyi.common.core.utils.StringUtils;
@@ -150,7 +151,7 @@ public class SysMenuServiceImpl implements ISysMenuService
router.setName(getRouteName(menu));
router.setPath(getRouterPath(menu));
router.setComponent(getComponent(menu));
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache())));
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
List<SysMenu> cMenus = menu.getChildren();
if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType()))
{
@@ -166,7 +167,21 @@ public class SysMenuServiceImpl implements ISysMenuService
children.setPath(menu.getPath());
children.setComponent(menu.getComponent());
children.setName(StringUtils.capitalize(menu.getPath()));
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache())));
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
childrenList.add(children);
router.setChildren(childrenList);
}
else if (menu.getParentId().intValue() == 0 && isInnerLink(menu))
{
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
router.setPath("/inner");
List<RouterVo> childrenList = new ArrayList<RouterVo>();
RouterVo children = new RouterVo();
String routerPath = StringUtils.replaceEach(menu.getPath(), new String[] { Constants.HTTP, Constants.HTTPS }, new String[] { "", "" });
children.setPath(routerPath);
children.setComponent(UserConstants.INNER_LINK);
children.setName(StringUtils.capitalize(routerPath));
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
childrenList.add(children);
router.setChildren(childrenList);
}
@@ -338,6 +353,11 @@ public class SysMenuServiceImpl implements ISysMenuService
public String getRouterPath(SysMenu menu)
{
String routerPath = menu.getPath();
// 内链打开外网方式
if (menu.getParentId().intValue() != 0 && isInnerLink(menu))
{
routerPath = StringUtils.replaceEach(routerPath, new String[] { Constants.HTTP, Constants.HTTPS }, new String[] { "", "" });
}
// 非外链并且是一级目录(类型为目录)
if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
&& UserConstants.NO_FRAME.equals(menu.getIsFrame()))
@@ -365,6 +385,10 @@ public class SysMenuServiceImpl implements ISysMenuService
{
component = menu.getComponent();
}
else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0 && isInnerLink(menu))
{
component = UserConstants.INNER_LINK;
}
else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu))
{
component = UserConstants.PARENT_VIEW;
@@ -384,6 +408,17 @@ public class SysMenuServiceImpl implements ISysMenuService
&& menu.getIsFrame().equals(UserConstants.NO_FRAME);
}
/**
* 是否为内链组件
*
* @param menu 菜单信息
* @return 结果
*/
public boolean isInnerLink(SysMenu menu)
{
return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath());
}
/**
* 是否为parent_view组件
*

View File

@@ -246,6 +246,17 @@ public class SysUserServiceImpl implements ISysUserService
return rows;
}
/**
* 注册用户信息
*
* @param user 用户信息
* @return 结果
*/
public boolean registerUser(SysUser user)
{
return userMapper.insertUser(user) > 0;
}
/**
* 修改保存用户信息
*
@@ -275,6 +286,7 @@ public class SysUserServiceImpl implements ISysUserService
* @param roleIds 角色组
*/
@Override
@Transactional
public void insertUserAuth(Long userId, Long[] roleIds)
{
userRoleMapper.deleteUserRoleByUserId(userId);

View File

@@ -1,6 +1,6 @@
{
"name": "ruoyi",
"version": "3.0.0",
"version": "3.1.0",
"description": "若依管理系统",
"author": "若依",
"license": "MIT",
@@ -41,7 +41,7 @@
"clipboard": "2.0.6",
"core-js": "3.8.1",
"echarts": "4.9.0",
"element-ui": "2.15.2",
"element-ui": "2.15.3",
"file-saver": "2.0.4",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",

View File

@@ -9,6 +9,18 @@ export function login(username, password, code, uuid) {
})
}
// 注册方法
export function register(data) {
return request({
url: '/auth/register',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
// 刷新方法
export function refreshToken() {
return request({

View File

@@ -2,6 +2,7 @@
<div>
<el-upload
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
name="file"
@@ -9,7 +10,7 @@
:headers="headers"
style="display: none"
ref="upload"
v-if="this.uploadUrl"
v-if="this.type == 'url'"
>
</el-upload>
<div class="editor" ref="editor" :style="styles"></div>
@@ -46,14 +47,20 @@ export default {
type: Boolean,
default: false,
},
/* 上传地址 */
uploadUrl: {
// 上传文件大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
/* 类型base64格式、url格式 */
type: {
type: String,
default: "",
default: "url",
}
},
data() {
return {
uploadUrl: process.env.VUE_APP_BASE_API + "/file/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken()
},
@@ -119,7 +126,7 @@ export default {
const editor = this.$refs.editor;
this.Quill = new Quill(editor, this.options);
// 如果设置了上传地址则自定义图片上传事件
if (this.uploadUrl) {
if (this.type == 'url') {
let toolbar = this.Quill.getModule("toolbar");
toolbar.addHandler("image", (value) => {
this.uploadType = "image";
@@ -129,14 +136,6 @@ export default {
this.quill.format("image", false);
}
});
toolbar.addHandler("video", (value) => {
this.uploadType = "video";
if (value) {
this.$refs.upload.$children[0].$refs.input.click();
} else {
this.quill.format("video", false);
}
});
}
this.Quill.pasteHTML(this.currentValue);
this.Quill.on("text-change", (delta, oldDelta, source) => {
@@ -157,6 +156,18 @@ export default {
this.$emit("on-editor-change", eventName, ...args);
});
},
// 上传前校检格式和大小
handleBeforeUpload(file) {
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
return true;
},
handleUploadSuccess(res, file) {
// 获取富文本组件实例
let quill = this.Quill;
@@ -165,7 +176,7 @@ export default {
// 获取光标所在位置
let length = quill.getSelection().index;
// 插入图片 res.url为服务器返回的图片地址
quill.insertEmbed(length, "image", res.url);
quill.insertEmbed(length, "image", res.data.url);
// 调整光标到最后
quill.setSelection(length + 1);
} else {

View File

@@ -4,7 +4,7 @@
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:limit="1"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
@@ -26,7 +26,7 @@
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in list">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="file.url" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
@@ -42,9 +42,15 @@
import { getToken } from "@/utils/auth";
export default {
name: "FileUpload",
props: {
// 值
value: [String, Object, Array],
// 数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
@@ -70,19 +76,15 @@ export default {
fileList: [],
};
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
// 列表
list() {
watch: {
value: {
handler(val) {
if (val) {
let temp = 1;
if (this.value) {
// 首先将值转为数组
const list = Array.isArray(this.value) ? this.value : [this.value];
const list = Array.isArray(val) ? val : this.value.split(',');
// 然后将数组转为对象数组
return list.map((item) => {
this.fileList = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
@@ -94,6 +96,15 @@ export default {
return [];
}
},
deep: true,
immediate: true
}
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
methods: {
// 上传前校检格式和大小
@@ -126,7 +137,7 @@ export default {
},
// 文件个数超出
handleExceed() {
this.$message.error(`只允许上传单个文件`);
this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
},
// 上传失败
handleUploadError(err) {
@@ -135,12 +146,13 @@ export default {
// 上传成功回调
handleUploadSuccess(res, file) {
this.$message.success("上传成功");
this.$emit("input", res.data.url);
this.fileList.push({ name: res.data.url, url: res.data.url });
this.$emit("input", this.listToString(this.fileList));
},
// 删除文件
handleDelete(index) {
this.fileList.splice(index, 1);
this.$emit("input", '');
this.$emit("input", this.listToString(this.fileList));
},
// 获取文件名称
getFileName(name) {
@@ -149,11 +161,17 @@ export default {
} else {
return "";
}
},
// 对象转成指定字符串分隔
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url + separator;
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
}
}
},
created() {
this.fileList = this.list;
},
};
</script>

View File

@@ -70,9 +70,11 @@ export default {
this.show = false
},
change(val) {
const path = val.path;
if(this.ishttp(val.path)) {
// http(s):// 路径新窗口打开
window.open(val.path, "_blank");
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
} else {
this.$router.push(val.path)
}

View File

@@ -5,33 +5,38 @@
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
name="file"
:show-file-list="false"
:on-remove="handleRemove"
:show-file-list="true"
:headers="headers"
style="display: inline-block; vertical-align: top"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:class="{hide: this.fileList.length >= this.limit}"
>
<el-image v-if="!value" :src="value">
<div slot="error" class="image-slot">
<i class="el-icon-plus" />
</div>
</el-image>
<div v-else class="image">
<el-image :src="value" :style="`width:150px;height:150px;`" fit="fill"/>
<div class="mask">
<div class="actions">
<span title="预览" @click.stop="dialogVisible = true">
<i class="el-icon-zoom-in" />
</span>
<span title="移除" @click.stop="removeImage">
<i class="el-icon-delete" />
</span>
</div>
</div>
</div>
<i class="el-icon-plus"></i>
</el-upload>
<el-dialog :visible.sync="dialogVisible" title="预览" width="800" append-to-body>
<img :src="value" style="display: block; max-width: 100%; margin: 0 auto;">
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
<el-dialog
:visible.sync="dialogVisible"
title="预览"
width="800"
append-to-body
>
<img
:src="dialogImageUrl"
style="display: block; max-width: 100%; margin: 0 auto"
/>
</el-dialog>
</div>
</template>
@@ -40,36 +45,123 @@
import { getToken } from "@/utils/auth";
export default {
props: {
value: [String, Object, Array],
// 图片数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
}
},
data() {
return {
dialogImageUrl: "",
dialogVisible: false,
hideUpload: false,
uploadImgUrl: process.env.VUE_APP_BASE_API + "/file/upload", // 上传的图片服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: []
};
},
props: {
watch: {
value: {
type: String,
default: "",
handler(val) {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : this.value.split(',');
// 然后将数组转为对象数组
this.fileList = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
return item;
});
} else {
this.fileList = [];
return [];
}
},
deep: true,
immediate: true
}
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
methods: {
removeImage() {
this.$emit("input", "");
// 删除图片
handleRemove(file, fileList) {
const findex = this.fileList.map(f => f.name).indexOf(file.name);
this.fileList.splice(findex, 1);
this.$emit("input", this.listToString(this.fileList));
},
// 上传成功回调
handleUploadSuccess(res) {
this.$emit("input", res.data.url);
this.fileList.push({ name: res.data.url, url: res.data.url });
this.$emit("input", this.listToString(this.fileList));
this.loading.close();
},
handleBeforeUpload() {
// 上传前loading加载
handleBeforeUpload(file) {
let isImg = false;
if (this.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
isImg = this.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
} else {
isImg = file.type.indexOf("image") > -1;
}
if (!isImg) {
this.$message.error(
`文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`
);
return false;
}
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$message.error(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
this.loading = this.$loading({
lock: true,
text: "上传中",
background: "rgba(0, 0, 0, 0.7)",
});
},
// 文件个数超出
handleExceed() {
this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
},
// 上传失败
handleUploadError() {
this.$message({
type: "error",
@@ -77,24 +169,37 @@ export default {
});
this.loading.close();
},
// 预览
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
},
watch: {},
// 对象转成指定字符串分隔
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url + separator;
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
}
}
};
</script>
<style scoped lang="scss">
.image {
position: relative;
.mask {
// .el-upload--picture-card 控制加号部分
::v-deep.hide .el-upload--picture-card {
display: none;
}
// 去掉动画效果
::v-deep .el-list-enter-active,
::v-deep .el-list-leave-active {
transition: all 0s;
}
::v-deep .el-list-enter, .el-list-leave-active {
opacity: 0;
position: absolute;
top: 0;
width: 100%;
background-color: rgba(0, 0, 0, 0.5);
transition: all 0.3s;
}
&:hover .mask {
opacity: 1;
}
transform: translateY(0);
}
</style>

View File

@@ -12,7 +12,7 @@
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-submenu index="more" v-if="topMenus.length > visibleNumber">
<el-submenu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template slot="title">更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
@@ -87,7 +87,7 @@ export default {
// 默认激活的菜单
activeMenu() {
const path = this.$route.path;
let activePath = this.routers[0].path;
let activePath = this.defaultRouter();
if (path.lastIndexOf("/") > 0) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
@@ -100,7 +100,7 @@ export default {
}
var routes = this.activeRoutes(activePath);
if (routes.length === 0) {
activePath = this.currentIndex || this.routers[0].path
activePath = this.currentIndex || this.defaultRouter()
this.activeRoutes(activePath);
}
return activePath;
@@ -121,6 +121,17 @@ export default {
const width = document.body.getBoundingClientRect().width / 3;
this.visibleNumber = parseInt(width / 85);
},
// 默认激活的路由
defaultRouter() {
let router;
Object.keys(this.routers).some((key) => {
if (!this.routers[key].hidden) {
router = this.routers[key].path;
return true;
}
});
return router;
},
// 菜单选择事件
handleSelect(key, keyPath) {
this.currentIndex = key;
@@ -158,25 +169,27 @@ export default {
</script>
<style lang="scss">
.el-menu--horizontal > .el-menu-item {
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px;
line-height: 50px;
margin: 0;
border-bottom: 3px solid transparent;
color: #999093;
padding: 0 5px;
margin: 0 10px;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.el-menu--horizontal > .el-menu-item.is-active {
border-bottom: 3px solid #{'var(--theme)'};
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-submenu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133;
}
/* submenu item */
.el-menu--horizontal > .el-submenu .el-submenu__title {
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
</style>

View File

@@ -0,0 +1,64 @@
/**
* v-dialogDrag 弹窗拖拽
* Copyright (c) 2019 ruoyi
*/
export default {
bind(el, binding, vnode, oldVnode) {
const value = binding.value
if (value == false) return
// 获取拖拽内容头部
const dialogHeaderEl = el.querySelector('.el-dialog__header');
const dragDom = el.querySelector('.el-dialog');
dialogHeaderEl.style.cursor = 'move';
// 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null);
dragDom.style.position = 'absolute';
dragDom.style.marginTop = 0;
let width = dragDom.style.width;
if (width.includes('%')) {
width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100);
} else {
width = +width.replace(/\px/g, '');
}
dragDom.style.left = `${(document.body.clientWidth - width) / 2}px`;
// 鼠标按下事件
dialogHeaderEl.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
const disX = e.clientX - dialogHeaderEl.offsetLeft;
const disY = e.clientY - dialogHeaderEl.offsetTop;
// 获取到的值带px 正则匹配替换
let styL, styT;
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (sty.left.includes('%')) {
styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
} else {
styL = +sty.left.replace(/\px/g, '');
styT = +sty.top.replace(/\px/g, '');
};
// 鼠标拖拽事件
document.onmousemove = function (e) {
// 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
const l = e.clientX - disX;
const t = e.clientY - disY;
let finallyL = l + styL
let finallyT = t + styT
// 移动当前元素
dragDom.style.left = `${finallyL}px`;
dragDom.style.top = `${finallyT}px`;
};
document.onmouseup = function (e) {
document.onmousemove = null;
document.onmouseup = null;
};
}
}
};

View File

@@ -0,0 +1,18 @@
import hasRole from './permission/hasRole'
import hasPermi from './permission/hasPermi'
import dialogDrag from './dialog/drag'
const install = function(Vue) {
Vue.directive('hasRole', hasRole)
Vue.directive('hasPermi', hasPermi)
Vue.directive('dialogDrag', dialogDrag)
}
if (window.Vue) {
window['hasRole'] = hasRole
window['hasPermi'] = hasPermi
window['dialogDrag'] = dialogDrag
Vue.use(install); // eslint-disable-line
}
export default install

View File

@@ -1,5 +1,5 @@
/**
* 操作权限处理
* v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi
*/

View File

@@ -1,5 +1,5 @@
/**
* 角色权限处理
* v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi
*/

View File

@@ -0,0 +1,27 @@
<script>
export default {
data() {
return {};
},
render() {
const { $route: { meta: { link } }, } = this;
if ({ link }.link === "") {
return "404";
}
let url = { link }.link;
const height = document.documentElement.clientHeight - 94.5 + "px";
const style = { height: height };
return (
<div style={style}>
<iframe
src={url}
frameborder="no"
style="width: 100%; height: 100%"
scrolling="auto"
></iframe>
</div>
);
},
};
</script>

View File

@@ -10,7 +10,7 @@ import '@/assets/styles/ruoyi.scss' // ruoyi css
import App from './App'
import store from './store'
import router from './router'
import permission from './directive/permission'
import directive from './directive' //directive
import { download } from '@/utils/request'
import './assets/icons' // icon
@@ -21,6 +21,12 @@ import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels,
import Pagination from "@/components/Pagination";
// 自定义表格工具组件
import RightToolbar from "@/components/RightToolbar"
// 富文本组件
import Editor from "@/components/Editor"
// 文件上传组件
import FileUpload from "@/components/FileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
// 字典标签组件
import DictTag from '@/components/DictTag'
// 头部标签组件
@@ -53,8 +59,11 @@ Vue.prototype.msgInfo = function (msg) {
Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)
Vue.component('Editor', Editor)
Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.use(permission)
Vue.use(directive)
Vue.use(VueMeta)
/**

View File

@@ -5,7 +5,6 @@ Vue.use(Router)
/* Layout */
import Layout from '@/layout'
import ParentView from '@/components/ParentView';
/**
* Note: 路由配置项
@@ -22,6 +21,7 @@ import ParentView from '@/components/ParentView';
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标对应路径src/assets/icons/svg
breadcrumb: false // 如果设置为false则不会在breadcrumb面包屑中显示
activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
}
*/
@@ -43,6 +43,11 @@ export const constantRoutes = [
component: (resolve) => require(['@/views/login'], resolve),
hidden: true
},
{
path: '/register',
component: (resolve) => require(['@/views/register'], resolve),
hidden: true
},
{
path: '/404',
component: (resolve) => require(['@/views/error/404'], resolve),
@@ -81,7 +86,7 @@ export const constantRoutes = [
]
},
{
path: '/auth',
path: '/system/user-auth',
component: Layout,
hidden: true,
children: [
@@ -89,12 +94,12 @@ export const constantRoutes = [
path: 'role/:userId(\\d+)',
component: (resolve) => require(['@/views/system/user/authRole'], resolve),
name: 'AuthRole',
meta: { title: '分配角色'}
meta: { title: '分配角色', activeMenu: '/system/user'}
}
]
},
{
path: '/auth',
path: '/system/role-auth',
component: Layout,
hidden: true,
children: [
@@ -102,46 +107,46 @@ export const constantRoutes = [
path: 'user/:roleId(\\d+)',
component: (resolve) => require(['@/views/system/role/authUser'], resolve),
name: 'AuthUser',
meta: { title: '分配用户'}
meta: { title: '分配用户', activeMenu: '/system/role'}
}
]
},
{
path: '/dict',
path: '/system/dict-data',
component: Layout,
hidden: true,
children: [
{
path: 'type/data/:dictId(\\d+)',
path: 'index/:dictId(\\d+)',
component: (resolve) => require(['@/views/system/dict/data'], resolve),
name: 'Data',
meta: { title: '字典数据', icon: '' }
meta: { title: '字典数据', activeMenu: '/system/dict'}
}
]
},
{
path: '/job',
path: '/monitor/job-log',
component: Layout,
hidden: true,
children: [
{
path: 'log',
path: 'index',
component: (resolve) => require(['@/views/monitor/job/log'], resolve),
name: 'JobLog',
meta: { title: '调度日志' }
meta: { title: '调度日志', activeMenu: '/monitor/job'}
}
]
},
{
path: '/gen',
path: '/tool/gen-edit',
component: Layout,
hidden: true,
children: [
{
path: 'edit/:tableId(\\d+)',
path: 'index/:tableId(\\d+)',
component: (resolve) => require(['@/views/tool/gen/editTable'], resolve),
name: 'GenEdit',
meta: { title: '修改生成配置' }
meta: { title: '修改生成配置', activeMenu: '/tool/gen'}
}
]
}

View File

@@ -2,6 +2,7 @@ import { constantRoutes } from '@/router'
import { getRouters } from '@/api/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView';
import InnerLink from '@/layout/components/InnerLink'
const permission = {
state: {
@@ -65,6 +66,8 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
route.component = Layout
} else if (route.component === 'ParentView') {
route.component = ParentView
} else if (route.component === 'InnerLink') {
route.component = InnerLink
} else {
route.component = loadView(route.component)
}

View File

@@ -23,22 +23,7 @@ service.interceptors.request.use(config => {
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?';
for (const propName of Object.keys(config.params)) {
const value = config.params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && typeof(value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
let params = propName + '[' + key + ']';
var subPart = encodeURIComponent(params) + "=";
url += subPart + encodeURIComponent(value[key]) + "&";
}
} else {
url += part + encodeURIComponent(value) + "&";
}
}
}
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
@@ -66,6 +51,7 @@ service.interceptors.response.use(res => {
location.href = '/index';
})
}).catch(() => {});
return Promise.reject()
} else if (code === 500) {
Message({
message: msg,

View File

@@ -180,10 +180,20 @@ export function handleTree(data, id, parentId, children) {
*/
export function tansParams(params) {
let result = ''
Object.keys(params).forEach((key) => {
if (!Object.is(params[key], undefined) && !Object.is(params[key], null) && !Object.is(JSON.stringify(params[key]), '{}')) {
result += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + '&'
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && typeof(value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
let params = propName + '[' + key + ']';
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
} else {
result += part + encodeURIComponent(value) + "&";
}
}
}
})
return result
}

View File

@@ -32,9 +32,11 @@ export function resolveBlob(res, mimeType) {
var result = patt.exec(contentDisposition)
var fileName = result[1]
fileName = fileName.replace(/\"/g, '')
aLink.style.display = 'none'
aLink.href = URL.createObjectURL(blob)
aLink.setAttribute('download', fileName) // 设置下载文件名称
document.body.appendChild(aLink)
aLink.click()
URL.revokeObjectURL(aLink.href);//清除引用
document.body.removeChild(aLink);
}

View File

@@ -119,9 +119,9 @@
</p>
<p>
<i class="el-icon-user-solid"></i> QQ群<s>满42799195</s>
<s>满170157040</s> <s>满130643120</s>
<a href="https://jq.qq.com/?_wv=1027&k=0Ck3PvTe" target="_blank">
225920371</a
<s>满170157040</s> <s>满130643120</s> <s>满225920371</s>
<a href="https://jq.qq.com/?_wv=1027&k=Kg9CdVdx" target="_blank">
201705537</a
>
</p>
<p>
@@ -146,6 +146,61 @@
<span>更新日志</span>
</div>
<el-collapse accordion>
<el-collapse-item title="v3.1.0 - 2021-08-02">
<ol>
<li>支持配置XSS跨站脚本过滤</li>
<li>支持配置验证码开关&类型</li>
<li>新增是否开启用户注册功能</li>
<li>用户管理新增分配角色功能</li>
<li>角色管理新增分配用户功能</li>
<li>系统布局配置支持动态标题开关</li>
<li>增加字典标签样式回显dict组件</li>
<li>FileUpload组件支持多文件上传</li>
<li>ImageUpload组件支持多图片上传</li>
<li>封装通用iframe组件</li>
<li>菜单路由配置支持内链访问</li>
<li>全局注册通用组件</li>
<li>富文本默认上传返回url类型</li>
<li>富文本新增上传文件大小限制</li>
<li>增加自定义弹窗拖拽指令</li>
<li>顶部菜单排除隐藏的默认路由</li>
<li>跳转路由高亮相对应的菜单栏</li>
<li>日志列表支持排序操作</li>
<li>分页组件新增pagerCount属性</li>
<li>定时任务屏蔽http(s)远程调用</li>
<li>文件服务本地资源允许跨域访问</li>
<li>升级spring-boot到最新版本2.5.3</li>
<li>升级spring-boot-admin到最新版2.4.3</li>
<li>升级spring-boot-mybatis到最新版2.2.0</li>
<li>升级nacos到最新版2.0.3</li>
<li>升级pagehelper到最新版1.3.1</li>
<li>升级minio到最新版本8.2.2</li>
<li>升级tobato到最新版本1.27.2</li>
<li>升级dynamic-ds到最新版本3.4.1</li>
<li>升级commons.io到最新版本v2.11.0</li>
<li>升级common-pool到最新版本2.10.0</li>
<li>升级commons.fileupload到最新版本v1.4</li>
<li>升级element-ui到最新版本2.15.3</li>
<li>优化统一网关错误码响应</li>
<li>修复导出含params属性对象参数问题</li>
<li>修复任意账户越权问题</li>
<li>修复定时任务日志执行状态显示</li>
<li>修改登录失效返回值code401</li>
<li>用户信息长度校验限制</li>
<li>角色&菜单新增字段属性提示信息</li>
<li>修复用户搜索分页变量错误</li>
<li>优化部门父级启用状态</li>
<li>启用部门状态排除顶级节点</li>
<li>定时任务新增更多操作</li>
<li>优化代码生成模板</li>
<li>优化顶部菜单显示样式</li>
<li>优化导入用户显示样式</li>
<li>优化用户不能删除自己</li>
<li>密码框新增显示切换密码图标</li>
<li>BLOB下载时清除URL对象引用</li>
<li>其他细节优化</li>
</ol>
</el-collapse-item>
<el-collapse-item title="v3.0.0 - 2021-06-10">
<ol>
<li>新增菜单导航显示风格TopNavfalse为左侧导航菜单true为顶部导航菜单</li>
@@ -505,7 +560,7 @@ export default {
data() {
return {
// 版本号
version: "3.0.0",
version: "3.1.0",
};
},
methods: {

View File

@@ -18,7 +18,7 @@
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-form-item prop="code" v-if="captchaOnOff">
<el-input
v-model="loginForm.code"
auto-complete="off"
@@ -44,6 +44,9 @@
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
</el-form>
<!-- 底部 -->
@@ -73,14 +76,18 @@ export default {
},
loginRules: {
username: [
{ required: true, trigger: "blur", message: "用户名不能为空" }
{ required: true, trigger: "blur", message: "请输入您的账号" }
],
password: [
{ required: true, trigger: "blur", message: "密码不能为空" }
{ required: true, trigger: "blur", message: "请输入您的密码" }
],
code: [{ required: true, trigger: "change", message: "验证码不能为空" }]
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
},
loading: false,
// 验证码开关
captchaOnOff: true,
// 注册开关
register: false,
redirect: undefined
};
},
@@ -99,8 +106,11 @@ export default {
methods: {
getCode() {
getCodeImg().then(res => {
this.captchaOnOff = res.captchaOnOff === undefined ? true : res.captchaOnOff;
if (this.captchaOnOff) {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
}
});
},
getCookie() {
@@ -130,7 +140,9 @@ export default {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
this.loading = false;
if (this.captchaOnOff) {
this.getCode();
}
});
}
});

Some files were not shown because too many files have changed in this diff Show More