此项目是我之前发布的 项目实战2: 基于SpringBoot的购物商城系统 的改良版,之前的项目定位是购物商城,但是缺了很多模块,比如没有评论、购物车、收藏等购物商城的基本功能,顶多只算是一个增删改查的案例,其中对于一些技术的应用只是表面层次,例如 Redis,仅仅用来保存有期限的验证码。
本次改良将项目定位成一套超市账单后台管理系统,该管理系统的主要特点是可以动态地修改不同角色的权限。结合现实生活中,超市里不同身份的人具有不同的职责与权限。这次的项目更有专一性,虽然仍然缺少许多内容,但是可以作为 SpringBoot 增删改查的练手项目。
目前这个项目存在的不足(部分):
采用技术栈:

用到的技术栈包括工具:


首先确定系统的对象,然后分为不同的模块,根据对象的不同场景继续划分模块。
以最基本的系统用户出发,目前我设计的用户对象有以下 5 种:
1. 供应商管理员
用于管理超市中供应商的角色,主要是对于供应商信息进行管理,比如查询、添加、删除、修改供应商。
2. 销售员
根据商品来生成账单的角色,类似超市的收银员,比如查询商品、生成账单,修改账单状态。
3. 商品管理员
主要管理商品的角色,类似超市的货物管理员,负责管理每个商品的信息(位置、价格等),我们这里不涉及位置的管理,比如查询商品、修改商品信息、删除商品。
4. 网站管理员
具有网站所有权限的角色,是管理其他所有角色的关键角色,比如管理用户信息、用户角色、角色权限等。
5. 经理
查询系统的营收情况,后台统计报表信息,不用编辑任何信息,只需要查看可视化的图表,同时还可以查看用户信息,用户角色等。
综上,以上 5 种角色是结合之前的系统设计出来的,之前的项目确实太乱了,定位不清晰,现在这么简单的列出五种角色后,就清楚了许多,而且他们各司其职,我们可以更好的利用 Shrio 框架来进行权限管理。
详细设计这一块不知道如何描述,我在写这个 “项目” 的时候,基本上就是想到什么写什么,只知道大概的流程,比如先从注册、登录的页面开始写起,写完页面,然后再写 Controller、Service、DAO。
现在发现,之前真的浪费太多时间了,页面能复制的就直接复制了,需要改的就改一下,目前接触了 Vue2,发现前端工程化的编码加上 组件库,非常容易上手而且写出来很清晰,像我在这个项目写的,尽管文件区分比较明确,但是里面的逻辑依然比较繁琐。
这里不一 一描述我这拙劣的设计方法了(太一般啦),就挑几个我自认为需要记录的内容说一下吧。
参考资料:Shiro 教程
Apache Shiro 是 Java 的一个安全框架。对比 Spring Security,功能没有那么强大,但是比较灵活。
既支持 普通的 Java 程序,又支持 Web开发的 JavaEE应用程序。
Shiro 解决的问题:

Shiro 架构图:

Subject:主体,代表了当前 “用户”,属于抽象概念,所有 Subject 都会绑定到 SecurityManager。
SecurityManager:安全管理器,管理所有的 Subject;是 Shiro 的核心,类似 SpringMVC 的 DispatcherServlet 。
Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),可以把 Realm 看成 DataSource,即数据源。
当用户登录时需进行授权,则需提前指定权限,然后再给指定的用户赋予相应的权限, Shiro 支持使用 ini 配置文件来授权,同样支持 DB 数据库来授权,这里则使用 MySQL 来存储 权限的内容,之后通过硬编码的方式,将 MySQL的权限注册到 Shiro 的会话中心。
参考:RBAC模型 资料
Shiro 是用于授权和鉴权的框架,除此之外还需要使用 RBAC 模型来形成完整的一套支持权限管理的系统。 RBAC 即 Role-Base-Access-Controle ,基于角色的访问控制。
RBAC 模型分为四种,分别是 RBAC0、RBAC1、RBAC2 和 RBAC3, 安全等级依次提升。
目前了解 RBAC0 就行,只有更复杂的业务系统才需要更高权限模型。
RBAC0 是其他三个模型的基础,提出的是用户、角色、权限之间的关系,如下图:

根据这个模型来建立 五张数据表,如下图:

这里使用 Spring 整合后的 Shiro 依赖 shiro-spring ,当引入依赖后经过一些配置,将 Shiro 相关的组件注入到 Spring 的容器中。
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.6.0version>
dependency>
Shiro 的 Realm 层负责和数据库交互,定义认证与授权这两个过程,接下来是 Java 代码实现的流程:
package com.star.ms.admin.shiro;
import org.apache.shiro.realm.AuthorizingRealm;
// 实现 Realm 需要继承 Shiro 提供的 AuthorizingRealm 类
public class CustomRealm extends AuthorizingRealm {
// 从 Spring 容器获取 业务逻辑层中 用户、角色、权限的实现
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermService permService;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
String principal = (String) principalCollection.getPrimaryPrincipal();
// 获取当前的用户信息
User user = userService.getByCode(principal);
if(user != null){
// 获取用户的角色
Role role = roleService.getByUserCode(user.getUsercode());
user.setRole(role);
// 赋予当前用户对应的角色信息(相同角色的权限是一致的)
if(role != null) {
info.addRole(role.getName());
// 添加权限
List<Perm> perms = permService.getByRoleId(role.getId());
if(perms!=null && !CollectionUtils.isEmpty(perms)) {
perms.forEach(perm -> {
info.addStringPermission(perm.getUrl());
});
}
}
}
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
// 获取身份信息
String principal = (String) token.getPrincipal();
// 调用业务层,获取数据库里对应的用户信息
User user = userService.getByCode(principal);
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(
principal,
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
this.getName());
}
else return null;
}
}
自定义 Realm 类里制定了 用户在注册登录时候的认证过程 doGetAuthenticationInfo,以及在登录后的授权过程 doGetAuthorizationInfo。
package com.star.ms.config;
@Configuration
public class ShiroConfig {
@Autowired
RoleService roleService;
@Autowired
PermService permService;
// 创建 Realm 对象
@Bean
public CustomRealm customerRealm(){
CustomRealm realm = new CustomRealm();
// 修改凭证校验匹配器
HashedCredentialsMatcher hcm = new HashedCredentialsMatcher();
hcm.setStoredCredentialsHexEncoded(true);
// 设置加密算法为 MD5
hcm.setHashAlgorithmName("MD5");
// 设置散列次数
hcm.setHashIterations(1024);
realm.setCredentialsMatcher(hcm);
return realm;
}
// 安全管理器
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(
@Qualifier("customerRealm") Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
// 过滤链工厂
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(
@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
Map<String, String> filterMap = new LinkedHashMap<>();
// 获取所有权限信息 配置数据库的权限信息
List<Perm> perms = permService.list();
perms.forEach(perm ->{
for (String p : perm.getUrl().split(",")) {
filterMap.put(p, String.format("perms[%s]",p));
}
});
// 配置公共权限 (游客的权限)
filterMap.put("/user/login", "anon");
filterMap.put("/user/verify", "anon");
filterMap.put("/register/*", "anon");
filterMap.put("/login.html", "anon");
filterMap.put("/home.html", "anon");
bean.setFilterChainDefinitionMap(filterMap);
// 设置登录的请求
bean.setLoginUrl("/login.html");
// 未授权页面
bean.setUnauthorizedUrl("/noauth");
return bean;
}
// 整合 ShiroDialect: 用来整合 shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
// Shiro注解开发需要的Bean
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(){
return new AuthorizationAttributeSourceAdvisor();
}
}
注入的 Bean 主要有 1)自定义的 Realm 对象;2)Shiro 安全管理器 ;3)Shiro 的 过滤链工厂 。
在配置好 Shrio 的权限以及用户的认证和鉴权方式后,接下来就是关于 Shiro 的使用,以 UserService 中的登录为例
public User login(User user) {
User loginUser = null;
if(user.getUsercode()!=null && user.getPassword()!=null) {
loginUser = userDAO.selectByCode(user.getUsercode());
// 封装一个 Token
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsercode(), user.getPassword(), false);
// 获取主体
Subject subject = SecurityUtils.getSubject();
subject.login(token);
// 验证成功后返回对象
// 查询角色
loginUser.setRole(roleService.getByUserCode(loginUser.getUsercode()));
}
return loginUser;
}
认证使用流程如下:
Shiro 鉴权的使用有许多,比如在 Controller 层:同样是通过 SecurityUtils 获取 Subject,然后调用 Subject 对象的 isPermmited 判断是否有某个权限。
@GetMapping("/admin/{type}")
public String toAdminUser(Model model, @PathVariable String type){
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
另一种就是使用经过 Thymeleaf 模板引擎渲染的 HTML,需要单独引入 thymeleaf-shiro的依赖
<dependency>
<groupId>com.github.theborakompanionigroupId>
<artifactId>thymeleaf-extras-shiroartifactId>
<version>2.1.0version>
dependency>
使用时:
<div xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
shiro:hasRole="管理员"><div>
<div>
至此, Shiro 的详细设计已完毕。
为了减轻服务器压力,对于管理系统的图片,我一致使用的是 阿里云的 OSS 云存储服务,首先得费用很低,一年10块左右,其次是更安全、更高效。
阿里云 OSS 官方地址:阿里云 OSS 服务
SpringBoot 整合 阿里云 OSS 可参考这篇文章:SpringBoot 整合 OSS
项目中用 OSS 的方法是先声明 OssService 接口,定义一些与OSS交互有关的方法。
package com.star.ms.common.service;
import com.star.ms.common.entity.api.OssUploadResult;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public interface OssService {
// 获取所有图片的路径
public List<String[]> getDefaultHeadImgLink();
// 上传文件
public OssUploadResult upload(String userCode, String img64base);
// 获取文件路径
public String getUserImgPath(String userCode, String imgType);
}
这些方法会用到 OSS 依赖提供的 OSS 对象,所以在实现这个接口前,需要声明 OSS 相关的 Bean 到 Spring 容器
package com.star.ms.common.api;
import com.aliyun.oss.*;
import com.aliyun.oss.model.ListObjectsRequest;
import com.aliyun.oss.model.OSSObjectSummary;
import com.aliyun.oss.model.ObjectListing;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.LinkedList;
import java.util.List;
@Configuration
public class OssApi {
public static final String ENDPOINT = "OSS的域";
public static final String ACCESS_KEY_ID = "OSS的验证ID";
public static final String ACCESS_KEY_SECRET = "OSS的验证密钥";
public static final String BUCKET_NAME = "OSS中Buncket块的名称";
// 根据配置将 OSS 注入到 Spring 的 IoC 容器
@Bean
public OSS OSSClient(){
return new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
}
}
以下页面是我用 Bootstrap5 凑出来的,可能不太没美观,以简约为主。由于想着用 AJAX 请求数据,尽量做到前后端分离,结果在后面用到了 Boostrap5 的模态框,写了很多重复的杂冗的 JS 代码…

































这个项目本来就没什么技术含量,所以我决定直接开源,希望能帮助到有需要的朋友吧,也希望自己将来有更好的技术水平来优化一下。
安全起见,我关闭了 OSS的可写功能,所以部署项目后的上传自定义图片功能将失效,过段时间我会考虑将 OSS 移除,采用本地存储,这样就可以一键部署成功了。
项目地址:Gitee