# 前后端分离仓库管理系统 **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的部署 #### 演示截图 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0430/223237_848b4460_5655643.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2020/0430/223442_5208134a_5655643.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2020/0430/223457_c574a046_5655643.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2020/0430/223504_98e7bef2_5655643.png "屏幕截图.png") ![输入图片说明](https://images.gitee.com/uploads/images/2020/0430/224718_31c32dc7_5655643.png "屏幕截图.png") #### 安装教程(详细看部署文档) 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