1 项目概述
1.1项目介绍
基于SpringBoot+Mybatis框架和MySql数据库做了一个资源分享网站。该网站可以用于资源分享,也可以充当百度云盘重要资料整理说明的文档。其主要功能按照权限进行划分可分成游客、用户和管理员三大部分,游客:游客可进行注册,阅览资源;用户:注册用户可进行登录,密码找回,对自己的资源进行管理(分享资源或充值可获得积分)对他人的资源下载(下载需要消耗积分),对下载的资源进行评价评价、发布资源获取积分积分等;管理员:管理员可对资源类型进行管理、对资源和评价进行审核、并可以完成对登录人数、各类别资源发布数目等信息进行统计的任务。为减少用户对数据库的操作以提高系统效率,本项目采用了Redis数据库充当缓存,并用Shiro框架实现认证和权限管理。
1.2技术选型
SpringBoot作为主体框架;MyBatis作为持久层框架;数据库选用MySql;并用Redis实现了对热门数据和常用数据的缓存(是不是也可以进行阅读量的增加)以减少对MySql的操作提高效率;Javamail实现:密码找回;Shiro作为Java安全框架,执行身份验证与授权。具体功能及核心实现如下所示。
1.3 项目流程
1 建立数据库和数据表;
2 利用IDEA创建SpringBoot项目;
3 对Maven工程的Pom.xml进行配置,以导入依赖;
4 在application.yml对Datasource等信息进行配置;
5 引入前端资源文件;
6 根据业务需求编写实体类Entity代码和Mapper层代码、Mapper.xml、service层接口和实现类、controller层的接口和实现类;
7测试并部署(这一步实际没进行);
2 数据库建立
2.1数据库中的表以及表间关系
创建数据库,并建立用户表、资源表、资源类型表、消息表、评价表、资源下载表和友情链接表,各个表之间的关系如图1所示。
2.2所用mysql知识点总结
1 建立表的过程中,各个表要满足三大范式;
2 存储引擎选择上,必须是InnoDB存储引擎,而不能是MyISAM存储引擎,因为MyISAM存储引擎是不支持外键的;
3 涉及多表查询;
4 外键约束的对业务逻辑来讲,也很重要;Mysql包含四种外键约束:
CASCADE:父表delete、update的时候,子表会delete、update掉关联记录;
SET NULL:父表delete、update的时候,子表会将关联记录的外键字段所在列设为null,所以注意在设计子表时外键不能设为not null;
RESTRICT:如果想要删除父表的记录时,而在子表中有关联该父表的记录,则不允许删除父表中的记录;
NO ACTION:同 RESTRICT,也是首先先检查外键;
3 后端核心功能及代码实现
在此部分进行之前要先解决掉项目热更新热部署的问题,来减少启动,同时加入filter过滤规则,对所有页面进行过滤(注意要把filter声明为配置类)。
3.1 用户主页相关功能
3.1.1 注册功能
用户通过添加用户名、密码、昵称、邮箱和性别几个属性进行注册,同时具备通过QQ进行注册的功能。(QQ注册的功能后面完善)。在进行存储注册之前,要先写好user Entity,再写Mapper层同时注入Sql,再写service层并进行事务和权限验证方面的操作,最后再对controller进行定义。
在这个功能中用到的UserService方法主要包含如下几种:
// 根据用户名查找用户实体
public User findByUserName(String userName);
//根据邮箱查找用户实体
public User findByEmail(String email);
// 添加或修改用户信息
public void save(User user);
* 根据id获取用户信息
public User getById(Integer id);
UserController中的关于注册的方法如下,其中BindingResult提供了参数校验功能。在项目当中少不了入参校验,服务器和浏览器互不信任,不能因为前端加入参判断了后台就不处理了,这样是不对的。比如前台传过来一个对象作为入参参数,这个对象中有些属性允许为空,有些属性不允许为空,那么我们可以使用@Valid+BindingResult进行controller参数校验。同时用户注册的过程中为了信息安全,不应将明文密码存入数据库,要进行MD5加密处理。controller如下:
/**
* 用户注册
*/
@ResponseBody
@PostMapping("/register")
public Map<String,Object> register(@Valid User user, BindingResult bindingResult){
Map<String,Object> map = new HashMap<>();
if(bindingResult.hasErrors()){
map.put("success",false);
map.put("errorInfo",bindingResult.getFieldError().getDefaultMessage());
}else if(userService.findByUserName(user.getUserName())!=null){
map.put("success",false);
map.put("errorInfo","用户名已存在,请更换!");
}else if(userService.findByEmail(user.getEmail())!=null){
map.put("success",false);
map.put("errorInfo","邮箱已存在,请更换!");
}else{
//CryptographyUtil.md5()是自定义的用于MD5加密的方法
user.setPassword(CryptographyUtil.md5(user.getPassword(),CryptographyUtil.SALT));
user.setRegistrationDate(new Date());
user.setLatelyLoginTime(new Date());
user.setHeadPortrait("tou.jpg");
userService.save(user);
map.put("success",true);
}
return map;
}
3.1.2 登录功能–Shrio认证
本项目在认证和权限的实现上采用Shrio,Shiro是Apache下的一个开源项目。shiro主要有三大功能模块: Subject:主体,一般指用户,对应用提供的API的核心;SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件。(类似于SpringMVC中的DispatcherServlet);Realms:用于进行权限信息的验证,一般需要自己实现。相当于DataSource。在进行认证和权限管理之前,我们应该先定义好Shrio的配置类,并自定义Realms。
自定义Realm中要实现权限管理的方法和认证的方法,截止到目前只涉及认证方法,代码很固定,如下所示:
// 权限认证
//Token中包含了前端传来的userName+passWord
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String userName = (String) authenticationToken.getPrincipal();
User user = userService.findByUserName(userName);
if(user==null){
return null;
}else{
AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUserName(),user.getPassword(),"xx");
return authenticationInfo;
}
}
Realm自定义完成后,需要定义Shrio的配置类,配置类中包含了三个核心方法,从上到下依次为:public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) 在这个方法中定义拦截器,过滤链;public SecurityManager securityManager() 用于返回SecurityManager;public MyRealm myRealm() 在这个方法中返回上文自定义的Realm。过滤链定义,从上往下执行,一般将/**放在最下面 这是个坑,一不小心代码就不好使了,authc:所有的url必须认证通过才可以访问 anon:所有url都可以匿名访问 ,配置不会被拦截的链接。(filters:管理全部过滤器,包括默认的关于身份验证和权限验证的过滤器,这些过滤器分为两组,一组是认证过滤器,有anon,authcBasic,auchc,user,一组是授权过滤器,有perms,roles,ssl,rest,port;)所以在定义拦截链的过程中,根目录、静态页面、静态资源、登录页面、登录方法、注册方法等都不需要进行过滤;除了不需要进行过滤的页面和方法以外,其他的直接定义为 filterChainDefinitionMap.put("/星星",“authc”)即可。代码如下所示(截止到目前还没涉及太多权限问题,这里的代码是项目整体的Shrio核心代码):
Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//必须设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//如果不设置,默认会自动寻找Web工程根目录下的"login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/");
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/","anon");
filterChainDefinitionMap.put("/static/**","anon");
filterChainDefinitionMap.put("/ueditor/**","anon");
filterChainDefinitionMap.put("/upload/**","anon");
filterChainDefinitionMap.put("/user/register.html","anon");
filterChainDefinitionMap.put("/user/login.html","anon");
filterChainDefinitionMap.put("/user/findPassword.html","anon");
filterChainDefinitionMap.put("/user/register","anon");
filterChainDefinitionMap.put("/user/login","anon");
filterChainDefinitionMap.put("/user/sendEmail","anon");
filterChainDefinitionMap.put("/user/checkYzm","anon");
filterChainDefinitionMap.put("/user/bindEmail","anon");
filterChainDefinitionMap.put("/QQ/qqLogin","anon");
filterChainDefinitionMap.put("/article/**","anon");
filterChainDefinitionMap.put("/comment/**","anon");
filterChainDefinitionMap.put("/connect","anon");
filterChainDefinitionMap.put("/buyVIP","anon");
filterChainDefinitionMap.put("/fbzyzjf","anon");
filterChainDefinitionMap.put("/admin/login.html","anon");
filterChainDefinitionMap.put("/user/bindEmail.html","anon");
filterChainDefinitionMap.put("/user/logout","logout");
filterChainDefinitionMap.put("/**","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm
securityManager.setRealm(myRealm());
return securityManager;
}
/**
* 身份认证realm
* @return
*/
@Bean
public MyRealm myRealm(){
MyRealm myRealm = new MyRealm();
return myRealm;
}
在定义好Shrio配置类后,就要写登录的controller代码了,因为登录成功后一般会将用户信息添加到Session中,所以controller方法接受的参数中应该包含session。具体实现描述:当用户名和密码都不为空的时候,就要进行是否可以登录的判断了,这个判断是交给shrio来完成的,首先获取subject对象,然后将用户名和密码作为参数建立Token对象,再调用subject的login等,最后登录成功后将用户信息添加到session中,具体代码如下所示(其实整个登录过程完全可以由Realm来完成,具体实现并不唯一):
/**
* 用户登录
* StringUtil.isEmpty()是个自定义的判断字符串是否为空的工具类,其实没太大实际意义;
*/
@ResponseBody
@PostMapping("/login")
public Map<String,Object> login(User user, HttpSession session){
Map<String,Object> map = new HashMap<>();
if(StringUtil.isEmpty(user.getUserName())){
map.put("success",false);
map.put("errorInfo","请输入用户名!");
}else if(StringUtil.isEmpty(user.getPassword())){
map.put("success",false);
map.put("errorInfo","请输入密码!");
}else{
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken