# 前后端分离仓库管理系统
**Repository Path**: OneSheep123/erp
## Basic Information
- **Project Name**: 前后端分离仓库管理系统
- **Description**: 前后端仓库管理系统 采用Springboot框架 shiro为安全框架 redis做缓存 docker容器进行mysql的部署
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 42
- **Forks**: 5
- **Created**: 2020-04-08
- **Last Updated**: 2024-08-07
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 前后端分离仓库管理系统
网址:http://212.64.58.72:82
接口文档地址:http://212.64.58.72:8081/doc.html
#### 介绍
前后端仓库管理系统 采用Springboot、mybatis-plus框架 shiro为安全框架 layuimini为前端框架 redis做缓存 docker容器进行mysql的部署
#### 演示截图





#### 安装教程(详细看部署文档)
1. 将Maven项目导出为jar包
2. 上传项目到云服务器
3. 将前端文件夹web放到nginx目录下并改名为erpweb
4. 放行端口3个端口,例如8881、8882、8883作为项目端口,进行负载均衡
5. 进入jar包项目,使用'java -jar erp'命令开启三个项目
6. 放行82端口进行网站访问
7. 配置nginx.conf文件,在后面加入
```
upstream www.erp.com {
server 127.0.0.1:8881;
server 127.0.0.1:8882;
server 127.0.0.1:8883;
#ip_hash;这是是用来解决登陆的session的问题
}
server
{
listen 82;
server_name localhost;
root erpweb;
index login.html;
location ^~ /api/ {
proxy_pass http://www.erp.com;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
client_max_body_size 2048m;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
8. 重启nginx,即可访问
#### 注意事项
1.shiro登录session共享问题
修改pom.xml引入shiro-redis
org.crazycake
shiro-redis
3.2.3
修改application.yml(对所有路径放行,以便进行测试)
#shiro的配置
shiro:
hash-algorithm-name: md5
hash-iterations: 2
# login-url: /index.html
# unauthorized-url: /unauthorized.html
anon-urls:
- /**
- /index.html*
- /login.html*
- /login/toLogin*
- /login/login*
logout-url: /login/logout*
authc-urls:
#- /**
1.属性配置类 ShiroProperties
@ConfigurationProperties(prefix = "shiro")
@Data
public class ShiroProperties {
private String hashAlgorithmName = "md5";
private Integer hashIterations = 2;
private String loginUrl;
private String unauthorizedUrl;
private String[] anonUrls;
private String logoutUrl;
private String[] authcUrls;
}
2.shiro配置类 ShiroAutoConfiguration
@Configuration
@EnableConfigurationProperties(value = {ShiroProperties.class})
public class ShiroAutoConfiguration {
@Autowired
private ShiroProperties shiroProperties;
//redis的配置属性类
@Autowired
private RedisProperties redisProperties;
/**
* 凭证匹配器
*/
@Bean
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(shiroProperties.getHashAlgorithmName());
credentialsMatcher.setHashIterations(shiroProperties.getHashIterations());
return credentialsMatcher;
}
/**
* 创建realm
*/
@Bean
public UserRealm userRealm(CredentialsMatcher credentialsMatcher) {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(credentialsMatcher);
return userRealm;
}
/**
* 声明安全管理器
*/
@Bean("securityManager")
public SecurityManager securityManager(DefaultWebSessionManager defaultWebSessionManager, SessionDAO redisSession, UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
defaultWebSessionManager.setSessionDAO(redisSession);
securityManager.setSessionManager(defaultWebSessionManager);
return securityManager;
}
/**
* 配置过滤器 Shiro 的Web过滤器 id必须和web.xml里面的shiroFilter的 targetBeanName的值一样
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//注入安全管理器
bean.setSecurityManager(securityManager);
//注入登陆页面
bean.setLoginUrl(shiroProperties.getLoginUrl());
//注入未授权的页面地址
bean.setUnauthorizedUrl(shiroProperties.getUnauthorizedUrl());
//注入过滤器
Map filterChainDefinition = new HashMap<>();
//注入放行地址
if (shiroProperties.getAnonUrls() != null && shiroProperties.getAnonUrls().length > 0) {
String[] anonUrls = shiroProperties.getAnonUrls();
for (String anonUrl : anonUrls) {
filterChainDefinition.put(anonUrl, "anon");
}
}
//注入登出的地址
if (shiroProperties.getLogoutUrl() != null) {
filterChainDefinition.put(shiroProperties.getLogoutUrl(), "logout");
}
//注拦截的地址
String[] authcUrls = shiroProperties.getAuthcUrls();
if (authcUrls != null && authcUrls.length > 0) {
for (String authcUrl : authcUrls) {
filterChainDefinition.put(authcUrl, "authc");
}
}
bean.setFilterChainDefinitionMap(filterChainDefinition);
return bean;
}
/**
* 注册过滤器
*/
@Bean
public FilterRegistrationBean filterRegistrationBeanDelegatingFilterProxy() {
FilterRegistrationBean bean = new FilterRegistrationBean<>();
//创建过滤器
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
bean.setFilter(proxy);
bean.addInitParameter("targetFilterLifecycle", "true");
bean.addInitParameter("targetBeanName", "shiroFilter");
// bean.addUrlPatterns();
List servletNames = new ArrayList<>();
servletNames.add(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
bean.setServletNames(servletNames);
return bean;
}
/*加入注解的使用,不加入这个注解不生效--开始*/
/**
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/*加入注解的使用,不加入这个注解不生效--结束*/
/**
* 使用Redis 来存储登录的信息
* sessionDao 还需要设置给sessionManager
*/
@Bean
public SessionDAO redisSessionDAO(IRedisManager redisManager) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager); //操作哪个redis
redisSessionDAO.setExpire(7 * 24 * 3600); // 用户的登录信息保存多久? 7 天
// redisSessionDAO.setKeySerializer(keySerializer); jdk
// redisSessionDAO.setValueSerializer(valueSerializer);jdk
return redisSessionDAO;
}
@Bean
public IRedisManager redisManager() {
RedisManager redisManager = new RedisManager();
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(redisProperties.getJedis().getPool().getMaxActive()); // 链接池的最量 20 ,并发特别大时,连接池的数据可以最大增加20个
jedisPoolConfig.setMaxIdle(redisProperties.getJedis().getPool().getMaxIdle());// 连接池的最大剩余量15个 :并发不大,池里面的对象用不上,里面对象太多了。浪费空间
jedisPoolConfig.setMinIdle(redisProperties.getJedis().getPool().getMinIdle()); // 连接池初始就有10 个
JedisPool jedisPool = new JedisPool(jedisPoolConfig, redisProperties.getHost(), redisProperties.getPort(), 2000, redisProperties.getPassword());
redisManager.setJedisPool(jedisPool);
return redisManager;
}
}
3.userRealm
还是原来的,不需要修改
public class UserRealm extends AuthenticatingRealm {
@Autowired
private UserService userservice;
public String getName() {
return this.getClass().getName();
}
/**
* @param authenticationToken 存储信息的token
* @return 认证信息
* @throws AuthenticationException 登陆失败会抛出异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String userName = (String) authenticationToken.getPrincipal();
//根据用户名查询用户
User user = userservice.queryUserByLoginName(userName);
if (null != user) {
//创建Activeuser
ActiverUser activierUser = new ActiverUser();
activierUser.setUser(user);
//创建返回值
ByteSource salt = ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(activierUser, user.getPwd(), salt, this.getName());
return info;
} else {
return null;
}
}
}
4.seesion的管理器 TokenWebSessionManager
@Configuration
public class TokenWebSessionManager extends DefaultWebSessionManager {
private static final String TOKEN_HEADER = "TOKEN";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//从头里面得到请求TOKEN 如果不存在就生成一个
String header = WebUtils.toHttp(request).getHeader(TOKEN_HEADER);
if (StringUtils.hasText(header)) {
return header;
}
return UUID.randomUUID().toString();
}
}
6.使用 LoginController
@Controller
@RequestMapping("login")
@CrossOrigin
public class LoginController {
@RequestMapping("doLogin")
@ResponseBody
public ResultObj doLogin(String loginname, String password) {
try {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken loginToken = new UsernamePasswordToken(loginname, password);
subject.login(loginToken);
//得到shiro的sessionid==token
String token = subject.getSession().getId().toString();
return new ResultObj(200, "登陆成功", token);
} catch (AuthenticationException e) {
e.printStackTrace();
return new ResultObj(-1, "登陆失败,用户名或者密码不正确");
}
}
}
2.解决前端请求跨域问题
1.前端修改
创建common.js
var api = 'http://127.0.0.1:8080/'
//下次再发ajax请求把token带到后台
var token = $.cookie('TOKEN');
//如果访问登陆页面这外的页面并且还没有登陆成功之后写入cookie的token就转到登陆页面
if (token == undefined & window.location != 'http://localhost:63342/ERP-WEB/login.html') {
window.top.location = '/ERP-WEB/login.html';
}
//设置全局ajax拦截,发送请求时携带token
$.ajaxSetup({
headers: {
'TOKEN': token
}
})
2.登陆页面修改
登录按钮
3.主页修改
引入js
4.创建CorsAutoConfig
@Configuration
public class CorsAutoConfig {
@Bean
public CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource=new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration=new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",corsConfiguration);
CorsFilter corsFilter=new CorsFilter(urlBasedCorsConfigurationSource);
return corsFilter;
}
}
3.动态验证用户登录
如果cookie里token还在,redis中的数据失效了,用户会处于未登陆的状态,此使需要重定向到登陆页面
如果cookie中的token失效了,需要重定向到登陆页面
1.后端提供接口
修改LoginController
@RequestMapping("checkLogin")
@ResponseBody
public ResultObj checkLogin() {
Subject subject = SecurityUtils.getSubject();
boolean authenticated = subject.isAuthenticated();
if (authenticated) {
return ResultObj.IS_LOGIN;
} else {
return ResultObj.UN_LOGIN;
}
}
2.修改common.js
var api = 'http://127.0.0.1:8080/'
//下次再发ajax请求把token带到后台
var token = $.cookie('TOKEN');
//设置全局ajax拦截,发送请求时携带token
$.ajaxSetup({
headers: {
'TOKEN': token
}
})
//如果访问登陆页面这外的页面并且还没有登陆成功之后写入cookie的token就转到登陆页面
if (window.location != 'http://localhost:63342/ERP-WEB/login.html') {
if (token == undefined) {
window.top.location = '/ERP-WEB/login.html';
} else {
$.ajax({
url: api + "login/checkLogin",
async: true,
type: 'post',
dataType: 'json',
success: function (res) {
if (res.code == -1) {
window.top.location = '/ERP-WEB/login.html';
}
},
error: function (res) {
window.top.location = '/ERP-WEB/login.html';
}
});
}
}
4.转换成json时剔除为空的字段
在属性上加上以下注解:
@JsonInclude(JsonInclude.Include.NON_EMPTY)
5.mybatisPlus ,domain中添加数据库中没有的字段
在属性上添加下面的注解
@TableField(exist=false)
6.生成json串时不序列化
在属性上添加下面的注解
@JsonIgnore
7.@Lazy + @Autowird(不要用@Resourse)
加上原因:
Userserviceimpl中加上了@lazy 是因为原来userservice中配置的aop(redis缓存)不生效,不是代理类
原因是realm比userService先执行,导致它的切面没有被注入,而controller不加是因为contoller等到用户调用url时才有用
8.redis做缓存时,注入service与ioc容器生成代理对象顺序问题解决 //问题:deptServiceimpl中对应的getById的 redis切面不生效 原因是:realm导致的
在依赖注入过后,通过ioc容器获得相应的service对象(此时已经该代理的已经代理完了)
工具类
@Component
public class AppUtils implements ApplicationContextAware {
private static ApplicationContext context;
public static ApplicationContext getContext() {
return context;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}
使用时,从容器取出代理类:
@Override
public DataGridView queryAllUser(UserVo userVo) {
IPage page = new Page<>(userVo.getPage(), userVo.getLimit());
QueryWrapper qw = new QueryWrapper<>();
qw.eq(null != userVo.getAvailable(), "available", userVo.getAvailable());
qw.like(!StringUtils.isBlank(userVo.getDeptid()), "deptid", userVo.getDeptid());
qw.like(!StringUtils.isBlank(userVo.getName()), "name", userVo.getName());
qw.like(!StringUtils.isBlank(userVo.getRemark()), "remark", userVo.getRemark());
qw.eq("type", Constant.USER_TYPE_NORMAL);
userMapper.selectPage(page, qw);
List users = page.getRecords();
//从ioc容器中获取DeptService实例,从而给用户的部门名称字段赋值
ApplicationContext context = AppUtils.getContext();
DeptService deptService = context.getBean(DeptService.class);
for (User user : users) {
Dept dept = deptService.getById(user.getDeptid());
user.setDeptname(dept.getTitle());
}
return new DataGridView(page.getTotal(), users);
}
这样就能保证我们使用的DeptService接口的实例对象,一定是实现redis缓存的代理对象
9.缓存问题
- 问题描述:当使用表格里面是否可用对数据进行更新之后,缓存里面的数据丢失部分 ,原因是因为@CachePut里缓存的是返回的值的对象
- 解决思路:先进行修改,再进行一次查询,将查询到的数据进行返回
10.docker安装redis
- 拉取镜像最新版本
docker pull redis:3.2
- 启动redis容器
docker run -d -p 6379:6379 -v $PWD/redis/data:/data -d --name redis-server redis:3.2 --appendonly yes --requirepass "123456"
- 注释
- - -p 6379:6379 => 映射端口6379
- - -v $PWD/redis/data:/data => 将主机中当前目录下的data挂载到容器的/data
- - --name redis-server =>容器别名
- - --requirepass "root" => 设置密码为root
- - --appendonly yes => 启用AOF持久化方式,设置为no重启数据不会保存
- 进入容器内部测试
进入容器内部 docker exec -it redis-server /bin/bash
连接redis redis-cli
登录redis auth root