Merge remote-tracking branch 'origin/master'

This commit is contained in:
lingdu 2021-10-19 18:18:21 +08:00
commit 1d9931e75f
247 changed files with 9865 additions and 3464 deletions

View File

@ -115,4 +115,4 @@ com.ruoyi
## 若依微服务交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-42799195-blue.svg)](https://jq.qq.com/?_wv=1027&k=yqInfq0S) [![加入QQ群](https://img.shields.io/badge/已满-170157040-blue.svg)](https://jq.qq.com/?_wv=1027&k=Oy1mb3p8) [![加入QQ群](https://img.shields.io/badge/已满-130643120-blue.svg)](https://jq.qq.com/?_wv=1027&k=rvxkJtXK) [![加入QQ群](https://img.shields.io/badge/225920371-blue.svg)](https://jq.qq.com/?_wv=1027&k=0Ck3PvTe) 点击按钮入群。
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-42799195-blue.svg)](https://jq.qq.com/?_wv=1027&k=yqInfq0S) [![加入QQ群](https://img.shields.io/badge/已满-170157040-blue.svg)](https://jq.qq.com/?_wv=1027&k=Oy1mb3p8) [![加入QQ群](https://img.shields.io/badge/已满-130643120-blue.svg)](https://jq.qq.com/?_wv=1027&k=rvxkJtXK) [![加入QQ群](https://img.shields.io/badge/已满-225920371-blue.svg)](https://jq.qq.com/?_wv=1027&k=0Ck3PvTe) [![加入QQ群](https://img.shields.io/badge/201705537-blue.svg)](https://jq.qq.com/?_wv=1027&k=FnHHP4TT) 点击按钮入群。

View File

@ -1,6 +1,6 @@
@echo off
echo.
echo [信息] 清理生成路径。
echo [信息] 清理工程target生成路径。
echo.
%~d0

View File

@ -1,6 +1,6 @@
@echo off
echo.
echo [信息] 运行auth工程。
echo [信息] 使用Jar命令运行Auth工程。
echo.
cd %~dp0

View File

@ -1,6 +1,6 @@
@echo off
echo.
echo [信息] 运行gateway工程。
echo [信息] 使用Jar命令运行Gateway工程。
echo.
cd %~dp0

View File

@ -1,6 +1,6 @@
@echo off
echo.
echo [信息] 运行modules-file工程。
echo [信息] 使用Jar命令运行Modules-File工程。
echo.
cd %~dp0

View File

@ -1,6 +1,6 @@
@echo off
echo.
echo [信息] 运行modules-gen工程。
echo [信息] 使用Jar命令运行Modules-Gen工程。
echo.
cd %~dp0

View File

@ -1,6 +1,6 @@
@echo off
echo.
echo [信息] 运行modules-job工程。
echo [信息] 使用Jar命令运行Modules-Job工程。
echo.
cd %~dp0

View File

@ -1,6 +1,6 @@
@echo off
echo.
echo [信息] 运行modules-system工程。
echo [信息] 使用Jar命令运行Modules-System工程。
echo.
cd %~dp0

View File

@ -1,6 +1,6 @@
@echo off
echo.
echo [信息] 运行monitor工程。
echo [信息] 使用Jar命令运行Monitor工程。
echo.
cd %~dp0

46
pom.xml
View File

@ -6,38 +6,40 @@
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.0.0</version>
<version>3.2.0</version>
<name>ruoyi</name>
<url>http://www.ruoyi.vip</url>
<description>若依微服务系统</description>
<properties>
<ruoyi.version>3.0.0</ruoyi.version>
<ruoyi.version>3.2.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-cloud.version>2020.0.3</spring-cloud.version>
<spring-boot.version>2.5.5</spring-boot.version>
<spring-cloud.version>2020.0.4</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.5.2</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>
<pagehelper.boot.version>1.4.0</pagehelper.boot.version>
<druid.version>1.2.8</druid.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>
<fastjson.version>1.2.78</fastjson.version>
<jjwt.version>0.9.1</jjwt.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>
<transmittable-thread-local.version>2.12.2</transmittable-thread-local.version>
</properties>
<!-- 依赖声明 -->
@ -173,6 +175,20 @@
<version>${fastjson.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- 线程传递值 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${transmittable-thread-local.version}</version>
</dependency>
<!-- 公共资源池 -->
<dependency>
<groupId>org.apache.commons</groupId>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.0.0</version>
<version>3.2.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.2.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 javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

View File

@ -1,4 +1,4 @@
package com.ruoyi.system.domain;
package com.ruoyi.system.api.domain;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

View File

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

View File

@ -204,6 +204,7 @@ public class SysRole extends BaseEntity
this.deptIds = deptIds;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("roleId", getRoleId())

View File

@ -57,9 +57,6 @@ public class SysUser extends BaseEntity
/** 密码 */
private String password;
/** 盐加密 */
private String salt;
/** 帐号状态0正常 1停用 */
@Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
private String status;
@ -91,6 +88,9 @@ public class SysUser extends BaseEntity
/** 岗位组 */
private Long[] postIds;
/** 角色ID */
private Long roleId;
public SysUser()
{
@ -208,16 +208,6 @@ public class SysUser extends BaseEntity
this.password = password;
}
public String getSalt()
{
return salt;
}
public void setSalt(String salt)
{
this.salt = salt;
}
public String getStatus()
{
return status;
@ -298,6 +288,15 @@ public class SysUser extends BaseEntity
this.postIds = postIds;
}
public Long getRoleId()
{
return roleId;
}
public void setRoleId(Long roleId)
{
this.roleId = roleId;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@ -310,7 +309,6 @@ public class SysUser extends BaseEntity
.append("sex", getSex())
.append("avatar", getAvatar())
.append("password", getPassword())
.append("salt", getSalt())
.append("status", getStatus())
.append("delFlag", getDelFlag())
.append("loginIp", getLoginIp())

View File

@ -6,6 +6,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;
/**
@ -25,13 +26,13 @@ public class RemoteLogFallbackFactory implements FallbackFactory<RemoteLogServic
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

@ -6,6 +6,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;
/**
@ -25,10 +26,16 @@ public class RemoteUserFallbackFactory implements FallbackFactory<RemoteUserServ
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.2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -7,10 +7,14 @@ 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.JwtUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.security.auth.AuthUtil;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.api.model.LoginUser;
/**
@ -39,12 +43,12 @@ public class TokenController
@DeleteMapping("logout")
public R<?> logout(HttpServletRequest request)
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser))
String token = SecurityUtils.getToken(request);
if (StringUtils.isNotEmpty(token))
{
String username = loginUser.getUsername();
String username = JwtUtils.getUserName(token);
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
AuthUtil.logoutByToken(token);
// 记录用户退出日志
sysLoginService.logout(username);
}
@ -63,4 +67,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.exception.ServiceException;
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.common.security.utils.SecurityUtils;
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,60 +40,120 @@ public class SysLoginService
// 用户名或密码为空 错误
if (StringUtils.isAnyBlank(username, password))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写");
throw new BaseException("用户/密码必须填写");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写");
throw new ServiceException("用户/密码必须填写");
}
// 密码如果不在指定范围内 错误
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围");
throw new BaseException("用户密码不在指定范围");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围");
throw new ServiceException("用户密码不在指定范围");
}
// 用户名不在指定范围内 错误
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
throw new BaseException("用户名不在指定范围");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
throw new ServiceException("用户名不在指定范围");
}
// 查询用户信息
R<LoginUser> userResult = remoteUserService.getUserInfo(username);
R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
if (R.FAIL == userResult.getCode())
{
throw new BaseException(userResult.getMsg());
throw new ServiceException(userResult.getMsg());
}
if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData()))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
throw new BaseException("登录用户:" + username + " 不存在");
recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
throw new ServiceException("登录用户:" + username + " 不存在");
}
LoginUser userInfo = userResult.getData();
SysUser user = userResult.getData().getSysUser();
if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");
throw new BaseException("对不起,您的账号:" + username + " 已被删除");
recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
}
if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
throw new BaseException("对不起,您的账号:" + username + " 已停用");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
}
if (!SecurityUtils.matchesPassword(password, user.getPassword()))
{
remoteLogService.saveLogininfor(username, Constants.LOGIN_FAIL, "用户密码错误");
throw new BaseException("用户不存在/密码错误");
recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码错误");
throw new ServiceException("用户不存在/密码错误");
}
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 ServiceException("用户/密码必须填写");
}
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
{
throw new ServiceException("账户长度必须在2到20个字符之间");
}
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
{
throw new ServiceException("密码长度必须在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 ServiceException(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.2.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.2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -41,6 +41,12 @@
<artifactId>spring-web</artifactId>
</dependency>
<!-- Transmittable ThreadLocal -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<!-- Apache Commons Pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
@ -71,6 +77,18 @@
<artifactId>fastjson</artifactId>
</dependency>
<!-- Jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!-- Jaxb -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<!-- Apache Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>

View File

@ -5,6 +5,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.math.BigDecimal;
import com.ruoyi.common.core.utils.poi.ExcelHandlerAdapter;
/**
* 自定义导出Excel数据注解
@ -103,7 +104,17 @@ public @interface Excel
/**
* 导出字段对齐方式0默认1靠左2居中3靠右
*/
Align align() default Align.AUTO;
public Align align() default Align.AUTO;
/**
* 自定义数据处理器
*/
public Class<?> handler() default ExcelHandlerAdapter.class;
/**
* 自定义数据处理器参数
*/
public String[] args() default {};
public enum Align
{

View File

@ -8,32 +8,17 @@ package com.ruoyi.common.core.constant;
public class CacheConstants
{
/**
* 令牌自定义标识
* 缓存有效期默认720分钟
*/
public static final String HEADER = "Authorization";
public final static long EXPIRATION = 720;
/**
* 令牌前缀
* 缓存刷新时间默认120分钟
*/
public static final String TOKEN_PREFIX = "Bearer ";
public final static long REFRESH_TIME = 120;
/**
* 权限缓存前缀
*/
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,16 @@ public class Constants
*/
public static final String GBK = "GBK";
/**
* RMI 远程方法调用
*/
public static final String LOOKUP_RMI = "rmi://";
/**
* LDAP 远程方法调用
*/
public static final String LOOKUP_LDAP = "ldap://";
/**
* http请求
*/
@ -87,10 +97,6 @@ public class Constants
*/
public static final long CAPTCHA_EXPIRATION = 2;
/**
* 令牌有效期分钟
*/
public final static long TOKEN_EXPIRE = 720;
/**
* 参数管理 cache key

View File

@ -0,0 +1,44 @@
package com.ruoyi.common.core.constant;
/**
* 权限相关通用常量
*
* @author ruoyi
*/
public class SecurityConstants
{
/**
* 用户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";
/**
* 用户标识
*/
public static final String USER_KEY = "user_key";
/**
* 登录用户
*/
public static final String LOGIN_USER = "login_user";
}

View File

@ -0,0 +1,25 @@
package com.ruoyi.common.core.constant;
/**
* Token的Key常量
*
* @author ruoyi
*/
public class TokenConstants
{
/**
* 令牌自定义标识
*/
public static final String AUTHENTICATION = "Authorization";
/**
* 令牌前缀
*/
public static final String PREFIX = "Bearer ";
/**
* 令牌秘钥
*/
public final static String SECRET = "abcdefghijklmnopqrstuvwxyz";
}

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,88 @@
package com.ruoyi.common.core.context;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.core.utils.StringUtils;
/**
* 获取当前线程变量中的 用户id用户名称Token等信息
* 注意 必须在网关通过请求头的方法传入同时在HeaderInterceptor拦截器设置值 否则这里无法获取
*
* @author ruoyi
*/
public class SecurityContextHolder
{
private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();
public static void set(String key, Object value)
{
Map<String, Object> map = getLocalMap();
map.put(key, value == null ? StringUtils.EMPTY : value);
}
public static String get(String key)
{
Map<String, Object> map = getLocalMap();
return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY));
}
public static <T> T get(String key, Class<T> clazz)
{
Map<String, Object> map = getLocalMap();
return StringUtils.cast(map.getOrDefault(key, null));
}
public static Map<String, Object> getLocalMap()
{
Map<String, Object> map = THREAD_LOCAL.get();
if (map == null)
{
map = new ConcurrentHashMap<String, Object>();
THREAD_LOCAL.set(map);
}
return map;
}
public static void setLocalMap(Map<String, Object> threadLocalMap)
{
THREAD_LOCAL.set(threadLocalMap);
}
public static Long getUserId()
{
return Convert.toLong(get(SecurityConstants.DETAILS_USER_ID), 0L);
}
public static void setUserId(String account)
{
set(SecurityConstants.DETAILS_USER_ID, account);
}
public static String getUserName()
{
return get(SecurityConstants.DETAILS_USERNAME);
}
public static void setUserName(String username)
{
set(SecurityConstants.DETAILS_USERNAME, username);
}
public static String getUserKey()
{
return get(SecurityConstants.USER_KEY);
}
public static void setUserKey(String userKey)
{
set(SecurityConstants.USER_KEY, userKey);
}
public static void remove()
{
THREAD_LOCAL.remove();
}
}

View File

@ -1,43 +0,0 @@
package com.ruoyi.common.core.exception;
/**
* 自定义异常
*
* @author ruoyi
*/
public class CustomException extends RuntimeException
{
private static final long serialVersionUID = 1L;
private Integer code;
private String message;
public CustomException(String message)
{
this.message = message;
}
public CustomException(String message, Integer code)
{
this.message = message;
this.code = code;
}
public CustomException(String message, Throwable e)
{
super(message, e);
this.message = message;
}
@Override
public String getMessage()
{
return message;
}
public Integer getCode()
{
return code;
}
}

View File

@ -0,0 +1,58 @@
package com.ruoyi.common.core.exception;
/**
* 全局异常
*
* @author ruoyi
*/
public class GlobalException extends RuntimeException
{
private static final long serialVersionUID = 1L;
/**
* 错误提示
*/
private String message;
/**
* 错误明细内部调试错误
*
* {@link CommonResult#getDetailMessage()} 一致的设计
*/
private String detailMessage;
/**
* 空构造方法避免反序列化问题
*/
public GlobalException()
{
}
public GlobalException(String message)
{
this.message = message;
}
public String getDetailMessage()
{
return detailMessage;
}
public GlobalException setDetailMessage(String detailMessage)
{
this.detailMessage = detailMessage;
return this;
}
public String getMessage()
{
return message;
}
public GlobalException setMessage(String message)
{
this.message = message;
return this;
}
}

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

@ -0,0 +1,73 @@
package com.ruoyi.common.core.exception;
/**
* 业务异常
*
* @author ruoyi
*/
public final class ServiceException extends RuntimeException
{
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 错误明细内部调试错误
*
* {@link CommonResult#getDetailMessage()} 一致的设计
*/
private String detailMessage;
/**
* 空构造方法避免反序列化问题
*/
public ServiceException()
{
}
public ServiceException(String message)
{
this.message = message;
}
public ServiceException(String message, Integer code)
{
this.message = message;
this.code = code;
}
public String getDetailMessage()
{
return detailMessage;
}
public String getMessage()
{
return message;
}
public Integer getCode()
{
return code;
}
public ServiceException setMessage(String message)
{
this.message = message;
return this;
}
public ServiceException setDetailMessage(String detailMessage)
{
this.detailMessage = detailMessage;
return this;
}
}

View File

@ -0,0 +1,16 @@
package com.ruoyi.common.core.exception.auth;
/**
* 未能通过的登录认证异常
*
* @author ruoyi
*/
public class NotLoginException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public NotLoginException(String message)
{
super(message);
}
}

View File

@ -0,0 +1,23 @@
package com.ruoyi.common.core.exception.auth;
import org.apache.commons.lang3.StringUtils;
/**
* 未能通过的权限认证异常
*
* @author ruoyi
*/
public class NotPermissionException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public NotPermissionException(String permission)
{
super(permission);
}
public NotPermissionException(String[] permissions)
{
super(StringUtils.join(permissions, ","));
}
}

View File

@ -0,0 +1,23 @@
package com.ruoyi.common.core.exception.auth;
import org.apache.commons.lang3.StringUtils;
/**
* 未能通过的角色认证异常
*
* @author ruoyi
*/
public class NotRoleException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public NotRoleException(String role)
{
super(role);
}
public NotRoleException(String[] roles)
{
super(StringUtils.join(roles, ","));
}
}

View File

@ -1,4 +1,4 @@
package com.ruoyi.common.core.exception;
package com.ruoyi.common.core.exception.base;
/**
* 基础异常

View File

@ -1,6 +1,6 @@
package com.ruoyi.common.core.exception.file;
import com.ruoyi.common.core.exception.BaseException;
import com.ruoyi.common.core.exception.base.BaseException;
/**
* 文件信息异常类

View File

@ -1,6 +1,6 @@
package com.ruoyi.common.core.exception.user;
import com.ruoyi.common.core.exception.BaseException;
import com.ruoyi.common.core.exception.base.BaseException;
/**
* 用户信息异常类

View File

@ -0,0 +1,123 @@
package com.ruoyi.common.core.utils;
import java.util.Map;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.constant.TokenConstants;
import com.ruoyi.common.core.text.Convert;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
/**
* Jwt工具类
*
* @author ruoyi
*/
public class JwtUtils
{
public static String secret = TokenConstants.SECRET;
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
public static String createToken(Map<String, Object> claims)
{
String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public static Claims parseToken(String token)
{
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/**
* 根据令牌获取用户标识
*
* @param token 令牌
* @return 用户ID
*/
public static String getUserKey(String token)
{
Claims claims = parseToken(token);
return getValue(claims, SecurityConstants.USER_KEY);
}
/**
* 根据令牌获取用户标识
*
* @param claims 身份信息
* @return 用户ID
*/
public static String getUserKey(Claims claims)
{
return getValue(claims, SecurityConstants.USER_KEY);
}
/**
* 根据令牌获取用户ID
*
* @param token 令牌
* @return 用户ID
*/
public static String getUserId(String token)
{
Claims claims = parseToken(token);
return getValue(claims, SecurityConstants.DETAILS_USER_ID);
}
/**
* 根据身份信息获取用户ID
*
* @param claims 身份信息
* @return 用户ID
*/
public static String getUserId(Claims claims)
{
return getValue(claims, SecurityConstants.DETAILS_USER_ID);
}
/**
* 根据令牌获取用户名
*
* @param token 令牌
* @return 用户名
*/
public static String getUserName(String token)
{
Claims claims = parseToken(token);
return getValue(claims, SecurityConstants.DETAILS_USERNAME);
}
/**
* 根据身份信息获取用户名
*
* @param claims 身份信息
* @return 用户名
*/
public static String getUserName(Claims claims)
{
return getValue(claims, SecurityConstants.DETAILS_USERNAME);
}
/**
* 根据身份信息获取键值
*
* @param claims 身份信息
* @param key
* @return
*/
public static String getValue(Claims claims, String key)
{
return Convert.toStr(claims.get(key), "");
}
}

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;
/**
* 客户端工具类
@ -55,6 +63,22 @@ public class ServletUtils
return Convert.toInt(getRequest().getParameter(name), defaultValue);
}
/**
* 获取Boolean参数
*/
public static Boolean getParameterToBool(String name)
{
return Convert.toBool(getRequest().getParameter(name));
}
/**
* 获取Boolean参数
*/
public static Boolean getParameterToBool(String name, Boolean defaultValue)
{
return Convert.toBool(getRequest().getParameter(name), defaultValue);
}
/**
* 获取request
*/
@ -106,6 +130,16 @@ public class ServletUtils
}
}
public static String getHeader(HttpServletRequest request, String name)
{
String value = request.getHeader(name);
if (StringUtils.isEmpty(value))
{
return StringUtils.EMPTY;
}
return urlDecode(value);
}
public static Map<String, String> getHeaders(HttpServletRequest request)
{
Map<String, String> map = new LinkedHashMap<>();
@ -192,7 +226,7 @@ public class ServletUtils
}
catch (UnsupportedEncodingException e)
{
return "";
return StringUtils.EMPTY;
}
}
@ -210,7 +244,65 @@ public class ServletUtils
}
catch (UnsupportedEncodingException e)
{
return "";
return StringUtils.EMPTY;
}
}
/**
* 设置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

@ -244,6 +244,7 @@ public class FileUtils
.append(percentEncodedFileName);
response.setHeader("Content-disposition", contentDispositionValue.toString());
response.setHeader("download-filename", percentEncodedFileName);
}
/**

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

@ -0,0 +1,19 @@
package com.ruoyi.common.core.utils.poi;
/**
* Excel数据格式处理适配器
*
* @author ruoyi
*/
public interface ExcelHandlerAdapter
{
/**
* 格式化
*
* @param value 单元格数据值
* @param args excel注解args参数组
*
* @return 处理后的值
*/
Object format(Object value, String[] args);
}

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.ArrayList;
@ -35,6 +36,7 @@ import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
@ -102,6 +104,16 @@ public class ExcelUtil<T>
*/
private List<Object[]> fields;
/**
* 当前行号
*/
private int rownum;
/**
* 标题
*/
private String title;
/**
* 最大高度
*/
@ -127,7 +139,7 @@ public class ExcelUtil<T>
this.clazz = clazz;
}
public void init(List<T> list, String sheetName, Type type)
public void init(List<T> list, String sheetName, String title, Type type)
{
if (list == null)
{
@ -136,8 +148,27 @@ public class ExcelUtil<T>
this.list = list;
this.sheetName = sheetName;
this.type = type;
this.title = title;
createExcelField();
createWorkbook();
createTitle();
}
/**
* 创建excel第一行标题
*/
public void createTitle()
{
if (StringUtils.isNotEmpty(title))
{
Row titleRow = sheet.createRow(rownum == 0 ? rownum++ : 0);
titleRow.setHeightInPoints(30);
Cell titleCell = titleRow.createCell(0);
titleCell.setCellStyle(styles.get("title"));
titleCell.setCellValue(title);
sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(),
this.fields.size() - 1));
}
}
/**
@ -148,33 +179,36 @@ public class ExcelUtil<T>
*/
public List<T> importExcel(InputStream is) throws Exception
{
return importExcel(StringUtils.EMPTY, is);
return importExcel(is, 0);
}
/**
* 对excel表单默认第一个索引名转换成list
*
* @param is 输入流
* @param titleNum 标题占用行数
* @return 转换后集合
*/
public List<T> importExcel(InputStream is, int titleNum) throws Exception
{
return importExcel(StringUtils.EMPTY, is, titleNum);
}
/**
* 对excel表单指定表格索引名转换成list
*
* @param sheetName 表格索引名
* @param titleNum 标题占用行数
* @param is 输入流
* @return 转换后集合
*/
public List<T> importExcel(String sheetName, InputStream is) throws Exception
public List<T> importExcel(String sheetName, InputStream is, int titleNum) throws Exception
{
this.type = Type.IMPORT;
this.wb = WorkbookFactory.create(is);
List<T> list = new ArrayList<T>();
Sheet sheet = null;
if (StringUtils.isNotEmpty(sheetName))
{
// 如果指定sheet名,则取指定sheet中的内容.
sheet = wb.getSheet(sheetName);
}
else
{
// 如果传入的sheet名不存在则默认指向第1个sheet.
sheet = wb.getSheetAt(0);
}
// 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
if (sheet == null)
{
throw new IOException("文件sheet不存在");
@ -188,7 +222,7 @@ public class ExcelUtil<T>
// 定义一个map用于存放excel列的序号和field.
Map<String, Integer> cellMap = new HashMap<String, Integer>();
// 获取表头
Row heard = sheet.getRow(0);
Row heard = sheet.getRow(titleNum);
for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++)
{
Cell cell = heard.getCell(i);
@ -203,25 +237,18 @@ public class ExcelUtil<T>
}
}
// 有数据时才处理 得到类的所有field.
Field[] allFields = clazz.getDeclaredFields();
// 定义一个map用于存放列的序号和field.
Map<Integer, Field> fieldsMap = new HashMap<Integer, Field>();
for (int col = 0; col < allFields.length; col++)
List<Object[]> fields = this.getFields();
Map<Integer, Object[]> fieldsMap = new HashMap<Integer, Object[]>();
for (Object[] objects : fields)
{
Field field = allFields[col];
Excel attr = field.getAnnotation(Excel.class);
if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
{
// 设置类的私有字段属性可访问.
field.setAccessible(true);
Excel attr = (Excel) objects[1];
Integer column = cellMap.get(attr.name());
if (column != null)
{
fieldsMap.put(column, field);
fieldsMap.put(column, objects);
}
}
}
for (int i = 1; i <= rows; i++)
for (int i = titleNum + 1; i <= rows; i++)
{
// 从第2行开始取数据,默认第一行是表头.
Row row = sheet.getRow(i);
@ -231,14 +258,15 @@ public class ExcelUtil<T>
continue;
}
T entity = null;
for (Map.Entry<Integer, Field> entry : fieldsMap.entrySet())
for (Map.Entry<Integer, Object[]> entry : fieldsMap.entrySet())
{
Object val = this.getCellValue(row, entry.getKey());
// 如果不存在实例则新建.
entity = (entity == null ? clazz.newInstance() : entity);
// 从map中得到对应列的field.
Field field = fieldsMap.get(entry.getKey());
Field field = (Field) entry.getValue()[0];
Excel attr = (Excel) entry.getValue()[1];
// 取得类型,并根据对象类型设置值.
Class<?> fieldType = field.getType();
if (String.class == fieldType)
@ -298,7 +326,6 @@ public class ExcelUtil<T>
}
if (StringUtils.isNotNull(fieldType))
{
Excel attr = field.getAnnotation(Excel.class);
String propertyName = field.getName();
if (StringUtils.isNotEmpty(attr.targetAttr()))
{
@ -308,6 +335,10 @@ public class ExcelUtil<T>
{
val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
}
else if (!attr.handler().equals(ExcelHandlerAdapter.class))
{
val = dataFormatHandlerAdapter(val, attr);
}
ReflectUtils.invokeSetter(entity, propertyName, val);
}
}
@ -326,14 +357,35 @@ public class ExcelUtil<T>
* @return 结果
* @throws IOException
*/
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName) throws IOException
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName)throws IOException
{
exportExcel(response, list, sheetName, StringUtils.EMPTY);
}
/**
* 对list数据源将其里面的数据导入到excel表单
*
* @param response 返回数据
* @param list 导出数据集合
* @param sheetName 工作表的名称
* @param title 标题
* @return 结果
* @throws IOException
*/
public void exportExcel(HttpServletResponse response, List<T> list, String sheetName, String title) throws IOException
{
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
this.init(list, sheetName, Type.EXPORT);
this.init(list, sheetName, title, Type.EXPORT);
exportExcel(response.getOutputStream());
}
/**
* 对list数据源将其里面的数据导入到excel表单
*
* @param sheetName 工作表的名称
* @return 结果
*/
/**
* 对list数据源将其里面的数据导入到excel表单
*
@ -341,10 +393,22 @@ public class ExcelUtil<T>
* @return 结果
*/
public void importTemplateExcel(HttpServletResponse response, String sheetName) throws IOException
{
importTemplateExcel(response, sheetName, StringUtils.EMPTY);
}
/**
* 对list数据源将其里面的数据导入到excel表单
*
* @param sheetName 工作表的名称
* @param title 标题
* @return 结果
*/
public void importTemplateExcel(HttpServletResponse response, String sheetName, String title) throws IOException
{
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
this.init(null, sheetName, Type.IMPORT);
this.init(null, sheetName, title, Type.IMPORT);
exportExcel(response.getOutputStream());
}
@ -377,13 +441,13 @@ public class ExcelUtil<T>
public void writeSheet()
{
// 取出一共有多少个sheet.
double sheetNo = Math.ceil(list.size() / sheetSize);
for (int index = 0; index <= sheetNo; index++)
int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
for (int index = 0; index < sheetNo; index++)
{
createSheet(sheetNo, index);
// 产生一行
Row row = sheet.createRow(0);
Row row = sheet.createRow(rownum);
int column = 0;
// 写入各个字段的列头名称
for (Object[] os : fields)
@ -411,7 +475,7 @@ public class ExcelUtil<T>
int endNo = Math.min(startNo + sheetSize, list.size());
for (int i = startNo; i < endNo; i++)
{
row = sheet.createRow(i + 1 - startNo);
row = sheet.createRow(i + 1 + rownum - startNo);
// 得到导出对象.
T vo = (T) list.get(i);
int column = 0;
@ -419,8 +483,6 @@ public class ExcelUtil<T>
{
Field field = (Field) os[0];
Excel excel = (Excel) os[1];
// 设置实体类私有属性可访问
field.setAccessible(true);
this.addCell(excel, row, vo, field, column++);
}
}
@ -439,6 +501,16 @@ public class ExcelUtil<T>
CellStyle style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
Font titleFont = wb.createFont();
titleFont.setFontName("Arial");
titleFont.setFontHeightInPoints((short) 16);
titleFont.setBold(true);
style.setFont(titleFont);
styles.put("title", style);
style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderRight(BorderStyle.THIN);
style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderLeft(BorderStyle.THIN);
@ -633,6 +705,10 @@ public class ExcelUtil<T>
{
cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).toString());
}
else if (!attr.handler().equals(ExcelHandlerAdapter.class))
{
cell.setCellValue(dataFormatHandlerAdapter(value, attr));
}
else
{
// 设置列类型
@ -779,6 +855,28 @@ public class ExcelUtil<T>
return StringUtils.stripEnd(propertyString.toString(), separator);
}
/**
* 数据处理器
*
* @param value 数据值
* @param excel 数据注解
* @return
*/
public String dataFormatHandlerAdapter(Object value, Excel excel)
{
try
{
Object instance = excel.handler().newInstance();
Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class });
value = formatMethod.invoke(instance, value, excel.args());
}
catch (Exception e)
{
log.error("不能格式化数据 " + excel.handler(), e.getMessage());
}
return Convert.toStr(value);
}
/**
* 合计统计信息
*/
@ -809,10 +907,9 @@ public class ExcelUtil<T>
{
if (statistics.size() > 0)
{
Cell cell = null;
Row row = sheet.createRow(sheet.getLastRowNum() + 1);
Set<Integer> keys = statistics.keySet();
cell = row.createCell(0);
Cell cell = row.createCell(0);
cell.setCellStyle(styles.get("total"));
cell.setCellValue("合计");
@ -882,7 +979,17 @@ public class ExcelUtil<T>
*/
private void createExcelField()
{
this.fields = new ArrayList<Object[]>();
this.fields = getFields();
this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
this.maxHeight = getRowHeight();
}
/**
* 获取字段注解信息
*/
public List<Object[]> getFields()
{
List<Object[]> fields = new ArrayList<Object[]>();
List<Field> tempFields = new ArrayList<>();
tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
@ -891,7 +998,12 @@ public class ExcelUtil<T>
// 单注解
if (field.isAnnotationPresent(Excel.class))
{
putToField(field, field.getAnnotation(Excel.class));
Excel attr = field.getAnnotation(Excel.class);
if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
{
field.setAccessible(true);
fields.add(new Object[] { field, attr });
}
}
// 多注解
@ -899,14 +1011,17 @@ public class ExcelUtil<T>
{
Excels attrs = field.getAnnotation(Excels.class);
Excel[] excels = attrs.value();
for (Excel excel : excels)
for (Excel attr : excels)
{
putToField(field, excel);
if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
{
field.setAccessible(true);
fields.add(new Object[] { field, attr });
}
}
}
this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
this.maxHeight = getRowHeight();
}
return fields;
}
/**
@ -923,23 +1038,15 @@ public class ExcelUtil<T>
return (short) (maxHeight * 20);
}
/**
* 放到字段集合中
*/
private void putToField(Field field, Excel attr)
{
if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
{
this.fields.add(new Object[] { field, attr });
}
}
/**
* 创建一个工作簿
*/
public void createWorkbook()
{
this.wb = new SXSSFWorkbook(500);
this.sheet = wb.createSheet();
wb.setSheetName(0, sheetName);
this.styles = createStyles(wb);
}
/**
@ -948,17 +1055,13 @@ public class ExcelUtil<T>
* @param sheetNo sheet数量
* @param index 序号
*/
public void createSheet(double sheetNo, int index)
public void createSheet(int sheetNo, int index)
{
// 设置工作表的名称.
if (sheetNo > 1 && index > 0)
{
this.sheet = wb.createSheet();
this.styles = createStyles(wb);
// 设置工作表的名称.
if (sheetNo == 0)
{
wb.setSheetName(index, sheetName);
}
else
{
this.createTitle();
wb.setSheetName(index, sheetName + index);
}
}

View File

@ -1,6 +1,6 @@
package com.ruoyi.common.core.utils.sql;
import com.ruoyi.common.core.exception.BaseException;
import com.ruoyi.common.core.exception.UtilException;
import com.ruoyi.common.core.utils.StringUtils;
/**
@ -22,7 +22,7 @@ public class SqlUtil
{
if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value))
{
throw new BaseException("参数不符合规范,不能进行查询");
throw new UtilException("参数不符合规范,不能进行查询");
}
return value;
}

View File

@ -55,7 +55,8 @@ public class BaseController
if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
{
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
PageHelper.startPage(pageNum, pageSize, orderBy);
Boolean reasonable = pageDomain.getReasonable();
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}
}

View File

@ -21,6 +21,9 @@ public class PageDomain
/** 排序的方向desc或者asc */
private String isAsc = "asc";
/** 分页参数合理化 */
private Boolean reasonable = true;
public String getOrderBy()
{
if (StringUtils.isEmpty(orderByColumn))
@ -81,4 +84,18 @@ public class PageDomain
this.isAsc = isAsc;
}
}
public Boolean getReasonable()
{
if (StringUtils.isNull(reasonable))
{
return Boolean.TRUE;
}
return reasonable;
}
public void setReasonable(Boolean reasonable)
{
this.reasonable = reasonable;
}
}

View File

@ -29,6 +29,11 @@ public class TableSupport
*/
public static final String IS_ASC = "isAsc";
/**
* 分页参数合理化
*/
public static final String REASONABLE = "reasonable";
/**
* 封装分页对象
*/
@ -39,6 +44,7 @@ public class TableSupport
pageDomain.setPageSize(ServletUtils.getParameterToInt(PAGE_SIZE));
pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
return pageDomain;
}

View File

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

View File

@ -1,18 +1,13 @@
package com.ruoyi.common.datascope.aspect;
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.domain.BaseEntity;
import com.ruoyi.common.datascope.annotation.DataScope;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.api.domain.SysRole;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.model.LoginUser;
@ -56,32 +51,17 @@ public class DataScopeAspect
*/
public static final String DATA_SCOPE = "dataScope";
@Autowired
private TokenService tokenService;
// 配置织入点
@Pointcut("@annotation(com.ruoyi.common.datascope.annotation.DataScope)")
public void dataScopePointCut()
{
}
@Before("dataScopePointCut()")
public void doBefore(JoinPoint point) throws Throwable
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
{
clearDataScope(point);
handleDataScope(point);
handleDataScope(point, controllerDataScope);
}
protected void handleDataScope(final JoinPoint joinPoint)
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
{
// 获得注解
DataScope controllerDataScope = getAnnotationLog(joinPoint);
if (controllerDataScope == null)
{
return;
}
// 获取当前的用户
LoginUser loginUser = tokenService.getLoginUser();
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNotNull(loginUser))
{
SysUser currentUser = loginUser.getSysUser();
@ -155,22 +135,6 @@ public class DataScopeAspect
}
}
/**
* 是否存在注解如果存在就获取
*/
private DataScope getAnnotationLog(JoinPoint joinPoint)
{
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
return method.getAnnotation(DataScope.class);
}
return null;
}
/**
* 拼接权限sql前先清空params.dataScope参数防止注入
*/

View File

@ -5,7 +5,7 @@
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>3.0.0</version>
<version>3.2.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.2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -38,4 +38,9 @@ public @interface Log
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
public boolean isSaveResponseData() default true;
}

View File

@ -1,18 +1,13 @@
package com.ruoyi.common.log.aspect;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -21,13 +16,13 @@ import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.JSON;
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.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessStatus;
import com.ruoyi.common.log.service.AsyncLogService;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.api.domain.SysOperLog;
/**
@ -44,21 +39,15 @@ public class LogAspect
@Autowired
private AsyncLogService asyncLogService;
// 配置织入点
@Pointcut("@annotation(com.ruoyi.common.log.annotation.Log)")
public void logPointCut()
{
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
{
handleLog(joinPoint, null, jsonResult);
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
@ -67,32 +56,22 @@ public class LogAspect
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e)
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
{
handleLog(joinPoint, e, null);
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult)
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
{
try
{
// 获得注解
Log controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null)
{
return;
}
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
operLog.setOperIp(ip);
// 返回参数
operLog.setJsonResult(JSON.toJSONString(jsonResult));
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
String username = SecurityUtils.getUsername();
if (StringUtils.isNotBlank(username))
@ -112,7 +91,7 @@ public class LogAspect
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog);
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 保存数据库
asyncLogService.saveSysLog(operLog);
}
@ -132,7 +111,7 @@ public class LogAspect
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) throws Exception
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception
{
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
@ -146,6 +125,11 @@ public class LogAspect
// 获取参数的信息传入到数据库中
setRequestValue(joinPoint, operLog);
}
// 是否需要保存response参数和值
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult))
{
operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
}
}
/**
@ -164,22 +148,6 @@ public class LogAspect
}
}
/**
* 是否存在注解如果存在就获取
*/
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
{
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
return method.getAnnotation(Log.class);
}
return null;
}
/**
* 参数拼装
*/
@ -188,13 +156,13 @@ public class LogAspect
String params = "";
if (paramsArray != null && paramsArray.length > 0)
{
for (int i = 0; i < paramsArray.length; i++)
for (Object o : paramsArray)
{
if (StringUtils.isNotNull(paramsArray[i]) && !isFilterObject(paramsArray[i]))
if (StringUtils.isNotNull(o) && !isFilterObject(o))
{
try
{
Object jsonObj = JSON.toJSON(paramsArray[i]);
Object jsonObj = JSON.toJSON(o);
params += jsonObj.toString() + " ";
}
catch (Exception e)
@ -223,17 +191,17 @@ public class LogAspect
else if (Collection.class.isAssignableFrom(clazz))
{
Collection collection = (Collection) o;
for (Iterator iter = collection.iterator(); iter.hasNext();)
for (Object value : collection)
{
return iter.next() instanceof MultipartFile;
return value instanceof MultipartFile;
}
}
else if (Map.class.isAssignableFrom(clazz))
{
Map map = (Map) o;
for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
for (Object value : map.entrySet())
{
Map.Entry entry = (Map.Entry) iter.next();
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}

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.2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -74,6 +74,28 @@ public class RedisService
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}
/**
* 判断 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.2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -16,6 +16,12 @@
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<!-- RuoYi Api System -->
<dependency>
<groupId>com.ruoyi</groupId>

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,20 @@
package com.ruoyi.common.security.annotation;
/**
* 权限注解的验证模式
*
* @author ruoyi
*
*/
public enum Logical
{
/**
* 必须具有所有的元素
*/
AND,
/**
* 只需具有其中一个元素
*/
OR
}

View File

@ -1,46 +0,0 @@
package com.ruoyi.common.security.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 权限注解
*
* @author ruoyi
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize
{
/**
* 验证用户是否具备某权限
*/
public String hasPermi() default "";
/**
* 验证用户是否不具备某权限 hasPermi逻辑相反
*/
public String lacksPermi() default "";
/**
* 验证用户是否具有以下任意一个权限
*/
public String[] hasAnyPermi() default {};
/**
* 判断用户是否拥有某个角色
*/
public String hasRole() default "";
/**
* 验证用户是否不具备某角色 isRole逻辑相反
*/
public String lacksRole() default "";
/**
* 验证用户是否具有以下任意一个角色
*/
public String[] hasAnyRoles() default {};
}

View File

@ -0,0 +1,18 @@
package com.ruoyi.common.security.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 登录认证只有登录之后才能进入该方法
*
* @author ruoyi
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresLogin
{
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.common.security.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 权限认证必须具有指定权限才能进入该方法
*
* @author ruoyi
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresPermissions
{
/**
* 需要校验的权限码
*/
String[] value() default {};
/**
* 验证模式AND | OR默认AND
*/
Logical logical() default Logical.AND;
}

View File

@ -0,0 +1,26 @@
package com.ruoyi.common.security.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 角色认证必须具有指定角色标识才能进入该方法
*
* @author ruoyi
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface RequiresRoles
{
/**
* 需要校验的角色标识
*/
String[] value() default {};
/**
* 验证逻辑AND | OR默认AND
*/
Logical logical() default Logical.AND;
}

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

@ -1,225 +1,97 @@
package com.ruoyi.common.security.aspect;
import java.lang.reflect.Method;
import java.util.Collection;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PatternMatchUtils;
import com.ruoyi.common.core.exception.PreAuthorizeException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.security.annotation.PreAuthorize;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.system.api.model.LoginUser;
import com.ruoyi.common.security.annotation.RequiresLogin;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.common.security.annotation.RequiresRoles;
import com.ruoyi.common.security.auth.AuthUtil;
/**
* 自定义权限实现
* 基于 Spring Aop 的注解鉴权
*
* @author ruoyi
* @author kong
*/
@Aspect
@Component
public class PreAuthorizeAspect
{
@Autowired
private TokenService tokenService;
/** 所有权限标识 */
private static final String ALL_PERMISSION = "*:*:*";
/** 管理员角色权限标识 */
private static final String SUPER_ADMIN = "admin";
/** 数组为0时 */
private static final Integer ARRAY_EMPTY = 0;
@Around("@annotation(com.ruoyi.common.security.annotation.PreAuthorize)")
public Object around(ProceedingJoinPoint point) throws Throwable
/**
* 构建
*/
public PreAuthorizeAspect()
{
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
PreAuthorize annotation = method.getAnnotation(PreAuthorize.class);
if (annotation == null)
{
return point.proceed();
}
if (StringUtils.isNotEmpty(annotation.hasPermi()))
{
if (hasPermi(annotation.hasPermi()))
{
return point.proceed();
}
throw new PreAuthorizeException();
}
else if (StringUtils.isNotEmpty(annotation.lacksPermi()))
{
if (lacksPermi(annotation.lacksPermi()))
{
return point.proceed();
}
throw new PreAuthorizeException();
}
else if (ARRAY_EMPTY < annotation.hasAnyPermi().length)
{
if (hasAnyPermi(annotation.hasAnyPermi()))
{
return point.proceed();
}
throw new PreAuthorizeException();
}
else if (StringUtils.isNotEmpty(annotation.hasRole()))
{
if (hasRole(annotation.hasRole()))
{
return point.proceed();
}
throw new PreAuthorizeException();
}
else if (StringUtils.isNotEmpty(annotation.lacksRole()))
{
if (lacksRole(annotation.lacksRole()))
{
return point.proceed();
}
throw new PreAuthorizeException();
}
else if (ARRAY_EMPTY < annotation.hasAnyRoles().length)
{
if (hasAnyRoles(annotation.hasAnyRoles()))
{
return point.proceed();
}
throw new PreAuthorizeException();
}
return point.proceed();
}
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
* 定义AOP签名 (切入所有使用鉴权注解的方法)
*/
public boolean hasPermi(String permission)
public static final String POINTCUT_SIGN = " @annotation(com.ruoyi.common.security.annotation.RequiresLogin) || "
+ "@annotation(com.ruoyi.common.security.annotation.RequiresPermissions) || "
+ "@annotation(com.ruoyi.common.security.annotation.RequiresRoles)";
/**
* 声明AOP签名
*/
@Pointcut(POINTCUT_SIGN)
public void pointcut()
{
LoginUser userInfo = tokenService.getLoginUser();
if (StringUtils.isNull(userInfo) || CollectionUtils.isEmpty(userInfo.getPermissions()))
{
return false;
}
return hasPermissions(userInfo.getPermissions(), permission);
}
/**
* 验证用户是否不具备某权限 hasPermi逻辑相反
* 环绕切入
*
* @param permission 权限字符串
* @return 用户是否不具备某权限
* @param joinPoint 切面对象
* @return 底层方法执行后的返回值
* @throws Throwable 底层方法抛出的异常
*/
public boolean lacksPermi(String permission)
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable
{
return hasPermi(permission) != true;
// 注解鉴权
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
checkMethodAnnotation(signature.getMethod());
try
{
// 执行原有逻辑
Object obj = joinPoint.proceed();
return obj;
}
catch (Throwable e)
{
throw e;
}
}
/**
* 验证用户是否具有以下任意一个权限
*
* @param permissions 权限列表
* @return 用户是否具有以下任意一个权限
* 对一个Method对象进行注解检查
*/
public boolean hasAnyPermi(String[] permissions)
public void checkMethodAnnotation(Method method)
{
LoginUser userInfo = tokenService.getLoginUser();
if (StringUtils.isNull(userInfo) || CollectionUtils.isEmpty(userInfo.getPermissions()))
// 校验 @RequiresLogin 注解
RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
if (requiresLogin != null)
{
return false;
}
Collection<String> authorities = userInfo.getPermissions();
for (String permission : permissions)
{
if (permission != null && hasPermissions(authorities, permission))
{
return true;
}
}
return false;
AuthUtil.checkLogin();
}
/**
* 判断用户是否拥有某个角色
*
* @param role 角色字符串
* @return 用户是否具备某角色
*/
public boolean hasRole(String role)
// 校验 @RequiresRoles 注解
RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
if (requiresRoles != null)
{
LoginUser userInfo = tokenService.getLoginUser();
if (StringUtils.isNull(userInfo) || CollectionUtils.isEmpty(userInfo.getRoles()))
{
return false;
}
for (String roleKey : userInfo.getRoles())
{
if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(role))
{
return true;
}
}
return false;
AuthUtil.checkRole(requiresRoles);
}
/**
* 验证用户是否不具备某角色 isRole逻辑相反
*
* @param role 角色名称
* @return 用户是否不具备某角色
*/
public boolean lacksRole(String role)
// 校验 @RequiresPermissions 注解
RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
if (requiresPermissions != null)
{
return hasRole(role) != true;
AuthUtil.checkPermi(requiresPermissions);
}
/**
* 验证用户是否具有以下任意一个角色
*
* @param roles 角色列表
* @return 用户是否具有以下任意一个角色
*/
public boolean hasAnyRoles(String[] roles)
{
LoginUser userInfo = tokenService.getLoginUser();
if (StringUtils.isNull(userInfo) || CollectionUtils.isEmpty(userInfo.getRoles()))
{
return false;
}
for (String role : roles)
{
if (hasRole(role))
{
return true;
}
}
return false;
}
/**
* 判断是否包含权限
*
* @param authorities 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
private boolean hasPermissions(Collection<String> authorities, String permission)
{
return authorities.stream().filter(StringUtils::hasText)
.anyMatch(x -> ALL_PERMISSION.contains(x) || PatternMatchUtils.simpleMatch(x, permission));
}
}

View File

@ -0,0 +1,371 @@
package com.ruoyi.common.security.auth;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.springframework.util.PatternMatchUtils;
import com.ruoyi.common.core.exception.auth.NotLoginException;
import com.ruoyi.common.core.exception.auth.NotPermissionException;
import com.ruoyi.common.core.exception.auth.NotRoleException;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.security.annotation.Logical;
import com.ruoyi.common.security.annotation.RequiresLogin;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.common.security.annotation.RequiresRoles;
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.api.model.LoginUser;
/**
* Token 权限验证逻辑实现类
*
* @author ruoyi
*/
public class AuthLogic
{
/** 所有权限标识 */
private static final String ALL_PERMISSION = "*:*:*";
/** 管理员角色权限标识 */
private static final String SUPER_ADMIN = "admin";
public TokenService tokenService = SpringUtils.getBean(TokenService.class);
/**
* 会话注销
*/
public void logout()
{
String token = SecurityUtils.getToken();
if (token == null)
{
return;
}
logoutByToken(token);
}
/**
* 会话注销根据指定Token
*/
public void logoutByToken(String token)
{
tokenService.delLoginUser(token);
}
/**
* 检验用户是否已经登录如未登录则抛出异常
*/
public void checkLogin()
{
getLoginUser();
}
/**
* 获取当前用户缓存信息, 如果未登录则抛出异常
*
* @return 用户缓存信息
*/
public LoginUser getLoginUser()
{
String token = SecurityUtils.getToken();
if (token == null)
{
throw new NotLoginException("未提供token");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null)
{
throw new NotLoginException("无效的token");
}
return loginUser;
}
/**
* 获取当前用户缓存信息, 如果未登录则抛出异常
*
* @param token 前端传递的认证信息
* @return 用户缓存信息
*/
public LoginUser getLoginUser(String token)
{
return tokenService.getLoginUser(token);
}
/**
* 验证当前用户有效期, 如果相差不足360分钟自动刷新缓存
*
* @param loginUser 当前用户信息
*/
public void verifyLoginUserExpire(LoginUser loginUser)
{
tokenService.verifyToken(loginUser);
}
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(String permission)
{
return hasPermi(getPermiList(), permission);
}
/**
* 验证用户是否具备某权限, 如果验证未通过则抛出异常: NotPermissionException
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public void checkPermi(String permission)
{
if (!hasPermi(getPermiList(), permission))
{
throw new NotPermissionException(permission);
}
}
/**
* 根据注解(@RequiresPermissions)鉴权, 如果验证未通过则抛出异常: NotPermissionException
*
* @param requiresPermissions 注解对象
*/
public void checkPermi(RequiresPermissions requiresPermissions)
{
if (requiresPermissions.logical() == Logical.AND)
{
checkPermiAnd(requiresPermissions.value());
}
else
{
checkPermiOr(requiresPermissions.value());
}
}
/**
* 验证用户是否含有指定权限必须全部拥有
*
* @param permissions 权限列表
*/
public void checkPermiAnd(String... permissions)
{
Set<String> permissionList = getPermiList();
for (String permission : permissions)
{
if (!hasPermi(permissionList, permission))
{
throw new NotPermissionException(permission);
}
}
}
/**
* 验证用户是否含有指定权限只需包含其中一个
*
* @param permissions 权限码数组
*/
public void checkPermiOr(String... permissions)
{
Set<String> permissionList = getPermiList();
for (String permission : permissions)
{
if (hasPermi(permissionList, permission))
{
return;
}
}
if (permissions.length > 0)
{
throw new NotPermissionException(permissions);
}
}
/**
* 判断用户是否拥有某个角色
*
* @param role 角色标识
* @return 用户是否具备某角色
*/
public boolean hasRole(String role)
{
return hasRole(getRoleList(), role);
}
/**
* 判断用户是否拥有某个角色, 如果验证未通过则抛出异常: NotRoleException
*
* @param role 角色标识
*/
public void checkRole(String role)
{
if (!hasRole(role))
{
throw new NotRoleException(role);
}
}
/**
* 根据注解(@RequiresRoles)鉴权
*
* @param requiresRoles 注解对象
*/
public void checkRole(RequiresRoles requiresRoles)
{
if (requiresRoles.logical() == Logical.AND)
{
checkRoleAnd(requiresRoles.value());
}
else
{
checkRoleOr(requiresRoles.value());
}
}
/**
* 验证用户是否含有指定角色必须全部拥有
*
* @param roles 角色标识数组
*/
public void checkRoleAnd(String... roles)
{
Set<String> roleList = getRoleList();
for (String role : roles)
{
if (!hasRole(roleList, role))
{
throw new NotRoleException(role);
}
}
}
/**
* 验证用户是否含有指定角色只需包含其中一个
*
* @param roles 角色标识数组
*/
public void checkRoleOr(String... roles)
{
Set<String> roleList = getRoleList();
for (String role : roles)
{
if (hasRole(roleList, role))
{
return;
}
}
if (roles.length > 0)
{
throw new NotRoleException(roles);
}
}
/**
* 根据注解(@RequiresLogin)鉴权
*
* @param at 注解对象
*/
public void checkByAnnotation(RequiresLogin at)
{
this.checkLogin();
}
/**
* 根据注解(@RequiresRoles)鉴权
*
* @param at 注解对象
*/
public void checkByAnnotation(RequiresRoles at)
{
String[] roleArray = at.value();
if (at.logical() == Logical.AND)
{
this.checkRoleAnd(roleArray);
}
else
{
this.checkRoleOr(roleArray);
}
}
/**
* 根据注解(@RequiresPermissions)鉴权
*
* @param at 注解对象
*/
public void checkByAnnotation(RequiresPermissions at)
{
String[] permissionArray = at.value();
if (at.logical() == Logical.AND)
{
this.checkPermiAnd(permissionArray);
}
else
{
this.checkPermiOr(permissionArray);
}
}
/**
* 获取当前账号的角色列表
*
* @return 角色列表
*/
public Set<String> getRoleList()
{
try
{
LoginUser loginUser = getLoginUser();
return loginUser.getRoles();
}
catch (Exception e)
{
return new HashSet<>();
}
}
/**
* 获取当前账号的权限列表
*
* @return 权限列表
*/
public Set<String> getPermiList()
{
try
{
LoginUser loginUser = getLoginUser();
return loginUser.getPermissions();
}
catch (Exception e)
{
return new HashSet<>();
}
}
/**
* 判断是否包含权限
*
* @param authorities 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(Collection<String> authorities, String permission)
{
return authorities.stream().filter(StringUtils::hasText)
.anyMatch(x -> ALL_PERMISSION.contains(x) || PatternMatchUtils.simpleMatch(x, permission));
}
/**
* 判断是否包含角色
*
* @param roles 角色列表
* @param role 角色
* @return 用户是否具备某角色权限
*/
public boolean hasRole(Collection<String> roles, String role)
{
return roles.stream().filter(StringUtils::hasText)
.anyMatch(x -> SUPER_ADMIN.contains(x) || PatternMatchUtils.simpleMatch(x, role));
}
}

View File

@ -0,0 +1,162 @@
package com.ruoyi.common.security.auth;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.common.security.annotation.RequiresRoles;
import com.ruoyi.system.api.model.LoginUser;
/**
* Token 权限验证工具类
*
* @author ruoyi
*/
public class AuthUtil
{
/**
* 底层的 AuthLogic 对象
*/
public static AuthLogic authLogic = new AuthLogic();
/**
* 会话注销
*/
public static void logout()
{
authLogic.logout();
}
/**
* 会话注销根据指定Token
*
* @param tokenValue 指定token
*/
public static void logoutByToken(String token)
{
authLogic.logoutByToken(token);
}
/**
* 检验当前会话是否已经登录如未登录则抛出异常
*/
public static void checkLogin()
{
authLogic.checkLogin();
}
/**
* 获取当前登录用户信息
*/
public static LoginUser getLoginUser(String token)
{
return authLogic.getLoginUser(token);
}
/**
* 验证当前用户有效期
*/
public static void verifyLoginUserExpire(LoginUser loginUser)
{
authLogic.verifyLoginUserExpire(loginUser);
}
/**
* 当前账号是否含有指定角色标识, 返回true或false
*
* @param role 角色标识
* @return 是否含有指定角色标识
*/
public static boolean hasRole(String role)
{
return authLogic.hasRole(role);
}
/**
* 当前账号是否含有指定角色标识, 如果验证未通过则抛出异常: NotRoleException
*
* @param role 角色标识
*/
public static void checkRole(String role)
{
authLogic.checkRole(role);
}
/**
* 根据注解传入参数鉴权, 如果验证未通过则抛出异常: NotRoleException
*
* @param requiresRoles 角色权限注解
*/
public static void checkRole(RequiresRoles requiresRoles)
{
authLogic.checkRole(requiresRoles);
}
/**
* 当前账号是否含有指定角色标识 [指定多个必须全部验证通过]
*
* @param roles 角色标识数组
*/
public static void checkRoleAnd(String... roles)
{
authLogic.checkRoleAnd(roles);
}
/**
* 当前账号是否含有指定角色标识 [指定多个只要其一验证通过即可]
*
* @param roles 角色标识数组
*/
public static void checkRoleOr(String... roles)
{
authLogic.checkRoleOr(roles);
}
/**
* 当前账号是否含有指定权限, 返回true或false
*
* @param permission 权限码
* @return 是否含有指定权限
*/
public static boolean hasPermi(String permission)
{
return authLogic.hasPermi(permission);
}
/**
* 当前账号是否含有指定权限, 如果验证未通过则抛出异常: NotPermissionException
*
* @param permission 权限码
*/
public static void checkPermi(String permission)
{
authLogic.checkPermi(permission);
}
/**
* 根据注解传入参数鉴权, 如果验证未通过则抛出异常: NotPermissionException
*
* @param requiresPermissions 权限注解
*/
public static void checkPermi(RequiresPermissions requiresPermissions)
{
authLogic.checkPermi(requiresPermissions);
}
/**
* 当前账号是否含有指定权限 [指定多个必须全部验证通过]
*
* @param permissions 权限码数组
*/
public static void checkPermiAnd(String... permissions)
{
authLogic.checkPermiAnd(permissions);
}
/**
* 当前账号是否含有指定权限 [指定多个只要其一验证通过即可]
*
* @param permissions 权限码数组
*/
public static void checkPermiOr(String... permissions)
{
authLogic.checkPermiOr(permissions);
}
}

View File

@ -0,0 +1,33 @@
package com.ruoyi.common.security.config;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.ruoyi.common.security.interceptor.HeaderInterceptor;
/**
* 拦截器配置
*
* @author ruoyi
*/
public class WebMvcConfig implements WebMvcConfigurer
{
/** 不需要拦截地址 */
public static final String[] excludeUrls = { "/login", "/logout", "/refresh" };
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(getHeaderInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(excludeUrls)
.order(-10);
}
/**
* 自定义请求头拦截器
*/
public HeaderInterceptor getHeaderInterceptor()
{
return new HeaderInterceptor();
}
}

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

@ -1,15 +1,19 @@
package com.ruoyi.common.security.handler;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
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.constant.HttpStatus;
import com.ruoyi.common.core.exception.DemoModeException;
import com.ruoyi.common.core.exception.PreAuthorizeException;
import com.ruoyi.common.core.exception.InnerAuthException;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.exception.auth.NotPermissionException;
import com.ruoyi.common.core.exception.auth.NotRoleException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
@ -24,31 +28,69 @@ public class GlobalExceptionHandler
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 基础异常
* 权限码异常
*/
@ExceptionHandler(BaseException.class)
public AjaxResult baseException(BaseException e)
@ExceptionHandler(NotPermissionException.class)
public AjaxResult handleNotPermissionException(NotPermissionException e, HttpServletRequest request)
{
return AjaxResult.error(e.getDefaultMessage());
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限码校验失败'{}'", requestURI, e.getMessage());
return AjaxResult.error(HttpStatus.FORBIDDEN, "没有访问权限,请联系管理员授权");
}
/**
* 角色权限异常
*/
@ExceptionHandler(NotRoleException.class)
public AjaxResult handleNotRoleException(NotRoleException e, HttpServletRequest request)
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',角色权限校验失败'{}'", requestURI, e.getMessage());
return AjaxResult.error(HttpStatus.FORBIDDEN, "没有访问权限,请联系管理员授权");
}
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request)
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
return AjaxResult.error(e.getMessage());
}
/**
* 业务异常
*/
@ExceptionHandler(CustomException.class)
public AjaxResult businessException(CustomException e)
{
if (StringUtils.isNull(e.getCode()))
{
return AjaxResult.error(e.getMessage());
}
return AjaxResult.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e)
@ExceptionHandler(ServiceException.class)
public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request)
{
log.error(e.getMessage(), e);
Integer code = e.getCode();
return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
}
/**
* 拦截未知的运行时异常
*/
@ExceptionHandler(RuntimeException.class)
public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request)
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
}
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e, HttpServletRequest request)
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
}
@ -56,7 +98,7 @@ public class GlobalExceptionHandler
* 自定义验证异常
*/
@ExceptionHandler(BindException.class)
public AjaxResult validatedBindException(BindException e)
public AjaxResult handleBindException(BindException e)
{
log.error(e.getMessage(), e);
String message = e.getAllErrors().get(0).getDefaultMessage();
@ -67,7 +109,7 @@ public class GlobalExceptionHandler
* 自定义验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object validExceptionHandler(MethodArgumentNotValidException e)
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
{
log.error(e.getMessage(), e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
@ -75,19 +117,19 @@ public class GlobalExceptionHandler
}
/**
* 权限异常
* 内部认证异常
*/
@ExceptionHandler(PreAuthorizeException.class)
public AjaxResult preAuthorizeException(PreAuthorizeException e)
@ExceptionHandler(InnerAuthException.class)
public AjaxResult handleInnerAuthException(InnerAuthException e)
{
return AjaxResult.error("没有权限,请联系管理员授权");
return AjaxResult.error(e.getMessage());
}
/**
* 演示模式异常
*/
@ExceptionHandler(DemoModeException.class)
public AjaxResult demoModeException(DemoModeException e)
public AjaxResult handleDemoModeException(DemoModeException e)
{
return AjaxResult.error("演示模式,不允许操作");
}

View File

@ -0,0 +1,53 @@
package com.ruoyi.common.security.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.context.SecurityContextHolder;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.security.auth.AuthUtil;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.api.model.LoginUser;
/**
* 自定义请求头拦截器将Header数据封装到线程变量中方便获取
*
* @author ruoyi
*/
public class HeaderInterceptor implements AsyncHandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (!(handler instanceof HandlerMethod))
{
return true;
}
SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));
SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));
SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));
String token = SecurityUtils.getToken();
if (StringUtils.isNotEmpty(token))
{
LoginUser loginUser = AuthUtil.getLoginUser(token);
if (StringUtils.isNotNull(loginUser))
{
AuthUtil.verifyLoginUserExpire(loginUser);
SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception
{
SecurityContextHolder.remove();
}
}

View File

@ -7,13 +7,14 @@ import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.constant.CacheConstants;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.utils.IdUtils;
import com.ruoyi.common.core.utils.SecurityUtils;
import com.ruoyi.common.core.utils.JwtUtils;
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.common.redis.service.RedisService;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.system.api.model.LoginUser;
/**
@ -27,31 +28,41 @@ public class TokenService
@Autowired
private RedisService redisService;
private final static long EXPIRE_TIME = Constants.TOKEN_EXPIRE * 60;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private final static long expireTime = CacheConstants.EXPIRATION;
private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY;
protected static final long MILLIS_SECOND = 1000;
private final static Long MILLIS_MINUTE_TEN = CacheConstants.REFRESH_TIME * MILLIS_MINUTE;
/**
* 创建令牌
*/
public Map<String, Object> createToken(LoginUser loginUser)
{
// 生成token
String token = IdUtils.fastUUID();
Long userId = loginUser.getSysUser().getUserId();
String userName = loginUser.getSysUser().getUserName();
loginUser.setToken(token);
loginUser.setUserid(loginUser.getSysUser().getUserId());
loginUser.setUsername(loginUser.getSysUser().getUserName());
loginUser.setUserid(userId);
loginUser.setUsername(userName);
loginUser.setIpaddr(IpUtils.getIpAddr(ServletUtils.getRequest()));
refreshToken(loginUser);
// 保存或更新用户token
Map<String, Object> map = new HashMap<String, Object>();
map.put("access_token", token);
map.put("expires_in", EXPIRE_TIME);
redisService.setCacheObject(ACCESS_TOKEN + token, loginUser, EXPIRE_TIME, TimeUnit.SECONDS);
return map;
// Jwt存储信息
Map<String, Object> claimsMap = new HashMap<String, Object>();
claimsMap.put(SecurityConstants.USER_KEY, token);
claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);
// 接口返回信息
Map<String, Object> rspMap = new HashMap<String, Object>();
rspMap.put("access_token", JwtUtils.createToken(claimsMap));
rspMap.put("expires_in", expireTime);
return rspMap;
}
/**
@ -73,13 +84,30 @@ public class TokenService
{
// 获取请求携带的令牌
String token = SecurityUtils.getToken(request);
return getLoginUser(token);
}
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(String token)
{
LoginUser user = null;
try
{
if (StringUtils.isNotEmpty(token))
{
String userKey = getTokenKey(token);
LoginUser user = redisService.getCacheObject(userKey);
String userkey = JwtUtils.getUserKey(token);
user = redisService.getCacheObject(getTokenKey(userkey));
return user;
}
return null;
}
catch (Exception e)
{
}
return user;
}
/**
@ -93,12 +121,30 @@ public class TokenService
}
}
/**
* 删除用户缓存信息
*/
public void delLoginUser(String token)
{
if (StringUtils.isNotEmpty(token))
{
String userKey = getTokenKey(token);
redisService.deleteObject(userKey);
String userkey = JwtUtils.getUserKey(token);
redisService.deleteObject(getTokenKey(userkey));
}
}
/**
* 验证令牌有效期相差不足120分钟自动刷新缓存
*
* @param loginUser
*/
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
{
refreshToken(loginUser);
}
}
@ -110,10 +156,10 @@ public class TokenService
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + EXPIRE_TIME * MILLIS_SECOND);
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisService.setCacheObject(userKey, loginUser, EXPIRE_TIME, TimeUnit.SECONDS);
redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
private String getTokenKey(String token)

View File

@ -1,4 +1,4 @@
package com.ruoyi.system.utils;
package com.ruoyi.common.security.utils;
import java.util.Collection;
import java.util.List;
@ -6,7 +6,7 @@ import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.system.domain.SysDictData;
import com.ruoyi.system.api.domain.SysDictData;
/**
* 字典工具类

View File

@ -1,9 +1,13 @@
package com.ruoyi.common.core.utils;
package com.ruoyi.common.security.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.text.Convert;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.constant.TokenConstants;
import com.ruoyi.common.core.context.SecurityContextHolder;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.system.api.model.LoginUser;
/**
* 权限获取工具类
@ -12,21 +16,36 @@ import com.ruoyi.common.core.text.Convert;
*/
public class SecurityUtils
{
/**
* 获取用户
*/
public static String getUsername()
{
String username = ServletUtils.getRequest().getHeader(CacheConstants.DETAILS_USERNAME);
return ServletUtils.urlDecode(username);
}
/**
* 获取用户ID
*/
public static Long getUserId()
{
return Convert.toLong(ServletUtils.getRequest().getHeader(CacheConstants.DETAILS_USER_ID));
return SecurityContextHolder.getUserId();
}
/**
* 获取用户名称
*/
public static String getUsername()
{
return SecurityContextHolder.getUserName();
}
/**
* 获取用户key
*/
public static String getUserKey()
{
return SecurityContextHolder.getUserKey();
}
/**
* 获取登录用户信息
*/
public static LoginUser getLoginUser()
{
return SecurityContextHolder.get(SecurityConstants.LOGIN_USER, LoginUser.class);
}
/**
@ -42,10 +61,20 @@ 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))
// 从header获取token标识
String token = request.getHeader(TokenConstants.AUTHENTICATION);
return replaceTokenPrefix(token);
}
/**
* 裁剪token前缀
*/
public static String replaceTokenPrefix(String token)
{
token = token.replace(CacheConstants.TOKEN_PREFIX, "");
// 如果前端设置了令牌前缀则裁剪掉前缀
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
{
token = token.replaceFirst(TokenConstants.PREFIX, "");
}
return token;
}

View File

@ -1,4 +1,6 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ruoyi.common.security.config.WebMvcConfig,\
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.2.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.2.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

@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration;
*/
@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

@ -1,29 +1,24 @@
package com.ruoyi.gateway.filter;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.constant.TokenConstants;
import com.ruoyi.common.core.utils.JwtUtils;
import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.gateway.config.properties.IgnoreWhiteProperties;
import io.jsonwebtoken.Claims;
import reactor.core.publisher.Mono;
/**
@ -36,69 +31,83 @@ public class AuthFilter implements GlobalFilter, Ordered
{
private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);
private final static long EXPIRE_TIME = Constants.TOKEN_EXPIRE * 60;
// 排除过滤的 uri 地址nacos自行添加
@Autowired
private IgnoreWhiteProperties ignoreWhite;
@Resource(name = "stringRedisTemplate")
private ValueOperations<String, String> sops;
@Autowired
private RedisService redisService;
@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))
Claims claims = JwtUtils.parseToken(token);
if (claims == null)
{
return setUnauthorizedResponse(exchange, "登录状态已过期");
return unauthorizedResponse(exchange, "token已过期或验证不正确");
}
JSONObject obj = JSONObject.parseObject(userStr);
String userid = obj.getString("userid");
String username = obj.getString("username");
if (StringUtils.isBlank(userid) || StringUtils.isBlank(username))
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin)
{
return setUnauthorizedResponse(exchange, "令牌验证失败");
return unauthorizedResponse(exchange, "登录状态已过期");
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))
{
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.USER_KEY, userkey);
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,10 +118,11 @@ 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))
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
// 如果前端设置了令牌前缀则裁剪掉前缀
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
{
token = token.replace(CacheConstants.TOKEN_PREFIX, "");
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
return token;
}

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.2.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.2.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" +

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