目录
一、设计思路
1.项目背景
2.技术栈选择
二、系统设计
1.系统结构图
2.项目结构
3.数据建模
4.数据流图
5.主要流程图
三、问题及解决办法
1.实现安全登录、访问
2.数据库中的信息安全问题
3.Mybatis-plus如何实现多表联查问题
4.当查询到很多条数据的时候,如何高效实现分页
5.当删除了一条数据之后,怎么实现恢复
四、亮点
1.采用shiro框架实现安全访问数据库
2.封装了一个自定义的通用结果类来返回请求结果,并且使用了构造器的方法。
3.接口的设计采用了RESTful风格
五、收获与感悟
六、附录
1.接口文档
一种在线做题系统。
前端: Vue2 + VueX + VueRouter+ Element + HTML + axios
后端:Spring + Mybatis-plus + Maven
数据库:MySQL
中间件:tomcat + shiro + JWT + log4j
工具:idea + Vscode + git + navicat + Apifox
插件:fastjson,druid,lombok,
(1)数据表的信息
sh_user(ID,username,password,salt);
sh_role(ID,rolename,description,locked);
sh_user_role(ID,userId,roleId,enabled);
sh_permission(ID,permissionName,requestPath,description,label,parentId,isDeleted);
sh_role_permission(ID,roleId,permissionId,enabled);
user_info(userId,nikeName,tel,email,quesNum,collectedNum,paperNum,createdTime);
admin_info(userId,nikeName,tel,email);
user_paper(ID,userId,paperId);
paper_info(ID,paperId,paperName,paperSource);
judge_question(quesId,quesType,quesLevel,quesSource,quesContent,quesAnswer,quesAnalysis);
select_question(quesId,quesType,quesLevel,quesSource,quesContent,option1,option2,option3,option4,quesAnswer,quesAnalysis);
discuss_question(quesId,quesType,quesLevel,quesSource,quesContent,quesAnswer,quesAnalysis);
paper_question(ID,paperId,quesId);
question_comment(ID,quesId,userId,content);
question_info(ID,quesId,class);
question_type(ID,quesId,quesType);
user_question_collected(ID,userId,quesId);
user_question_state(ID,userId,quesId,state,yourAnswer,visitTime);
(2)E-R图
由于各实体的属性值过多,下面的图中不展示实体的属性值。
(1)管理员
(2)用户
(1)注册、登录、退出流程图
(2) 修改个人信息
(3)修改密码
(4)管理员对用户操作
(5)管理员对题目操作
(6)用户组卷
这里采用了shiro框架,用Shiro来帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等操作。具体介绍看我这篇博客。
为了防止数据库信息泄露而导致的用户信息泄露问题,这里采用了MD5加密的方式,在数据库中存储加密后的字段。只要方式是通过shiro的配置来完成,在shiro写入的时候使用md5+salt+散列的方式进行数据加密之后存入数据库,读出的时候进行响应的解密。
简单操作步骤:
①maven中导入shiro坐标
org.apache.shiro
shiro-spring-boot-starter
1.9.1
② 自定义一个realm,用于认证授权等操作
public class CustomerRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 认证方式
* @param authenticationToken 校验传入令牌
* @return AuthenticationInfo
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
User user = userService.getByUsername(principal);
if (!ObjectUtils.isEmpty(user)) {
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
}
return null;
}
/**
* 授权方式
* @param principalCollection SimpleAuthenticationInfo对象第一个参数
* @return AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
③编写shiro的配置文件
public class ShiroConfig {
// 1. 创建ShiroFilter,负责拦截请求
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置系统受限资源和公告资源
HashMap map = new HashMap<>();
// anon 表示公共资源, authc 表示需要认证授权
map.put("/index.html","anon");
map.put("/page/login.html","anon"); // 放行登录页面
map.put("/page/register.html","anon"); // 放行注册页面
map.put("/user/**","anon"); // 放行用户控制器
map.put("/admin/**","anon"); // 放行管理员控制器
map.put("/**","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 默认认证界面,失败后跳转
shiroFilterFactoryBean.setLoginUrl("/page/login.html");
return shiroFilterFactoryBean;
}
//2. 创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
// 3.创建自定义realm,md5 + salt + 散列
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置使用MD5加密算法
credentialsMatcher.setHashAlgorithmName("md5");
//散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
}
MP提供了大量单表查询的方法,但是没有多表的操作,所以涉及到多表的查询时,需要我们自己实现。
简单步骤如下:
①在一个的mapper中创建一个对多的对象,例如,一个用户有很多各角色,一个角色也可以对应很多各用户,那么就可以在用户mapper中定义一个角色列表,在角色mapper中定义一个用户列表。
/**
* 用户mapper
*/
@Mapper
public interface UserMapper extends BaseMapper {
/**
* 获取用户的所有角色
* @param userId 用户id
* @return List
*/
List getRoles(String userId);
}
/**
* 权限mapper
*/
@Mapper
public interface RoleMapper extends BaseMapper {
/**
* 获取一个角色有哪些用户
* @param roleId 角色id
* @return List
*/
List getUsers(String roleId);
}
②创建对应的xml文件,在xml文件中实现数据库的查询操作。
其实Mybatis-plus中已经提供了对应的分类功能,简单实现步骤如下:
①编写拦截器配置类
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //填写对应的数据库
return interceptor;
}
}
②在接口处设置分页参数
@GetMapping("/test")
public Response test(){
Page producePage = new Page<>(1,1);
Page page = produceService.page(producePage);
System.out.println(producePage == page);
List records = page.getRecords();
for (Produce record : records) {
System.out.println(record);
}
return new Result<>(records, ResultEnum.SUCCESS);
}
在表中增加一个字段名为“is_deleted"的字段,字段默认为0,表示没有删除,当执行了删除操作之后,会将该字段设置为1,表示已经删除了,以后的查询操作都会建立在”is_deleted"为0的基础上进行查询。
shiro的处理流程为:
流程如下:
1、Shiro把用户的数据封装成标识token,token一般封装着用户名,密码等信息
2、使用Subject门面获取到封装着用户的数据的标识token
3、Subject把标识token交给SecurityManager,在SecurityManager安全中心中,SecurityManager把标识token委托给认证器Authenticator进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些Realm
4、认证器Authenticator将传入的标识token,与数据源Realm对比,验证token是否合法
在这里首先定义了一个表示状态的枚举类(status.java),里面封装了所有的状态和状态描述信息。然后再定义了一个结果类,并且使用了构造器的方法。
public class Result {
private final Integer code; //状态码
private final String message; //错误的的状态信息
private final Object data; //数据
public static class Builder{
private Integer code; //状态码
private String message; //错误的的状态信息
private Object data = null; //数据
public Builder(Integer code, String message) {
this.code = code;
this.message = message;
}
public Builder code(Integer code){
this.code = code;
return this;
}
public Builder message(String message){
this.message = message;
return this;
}
public Builder data(Object data){
this.data = data;
return this;
}
public Result build(){
return new Result(this);
}
}
private Result(Builder builder){
this.code = builder.code;
this.message = builder.message;
this.data = builder.data;
}
}
这样就可以通过如下方式来构造:
// 当没有data时:
return new Result.Builder().code(Status.OK().code).message(Status.OK().message);
// 当有data时:
return new Result.Builder().code(Status.OK().code).message(Status.OK().message).data(data);
关于这一部分的详细知识点可以看我的另一篇博客(实在是太优雅啦!!)
具体介绍可以看我这一篇博客。接口文档见附录。
通过这次后端项目的实战,加深了对框架的理解和认识,熟悉了利用框架开发的流程和方法;同时还学会了不同技术和spring-boot进行整合的方式;接触到了mybatis-plus,它是mybatis的升级版,是由国人开发的,符合了中国人的开发思维,能够极大地简化开发;同时新学习了shiro安全框架,保证数据和程序运行的安全性,相比于spring-security框架,shiro更加简洁轻便,学习成本更低。
当然这个项目还有很多不完善的地方,在异常报错的处理方面还不够细致,当出现异常的时候,不能够准确地指出问题的出处,而是比较笼统地指出问题的原因,这一点需要改善,可以通过在status类中新增各种的请求状态来完成;同时,项目中没有很注重失败原子性的操作,失败的方法调用应该使对象保持在被调用之前的状态,当然实现的方法也有很多,可以看我的这篇博客;当然,由于项目的规模是比较小的,所以也没有注重高并发相关的处理。
由于篇幅限制,接口文档点击这里查看