SpringBoot聚合项目:达内知道(三)-数据库用户登录、学生注册
1.用户数据详解
下面我们要实现数据库中的用户登录,但是必须要明确数据库中保存用户信息和其资格的格式:
在数据库中,两张表的关系有:
1.一对多(多对一)
2.多对多:
上面的用户角色权限结构中,用户和角色就是多对多,角色对权限也是多对多,凡是多对多的关系,必须有一个中间表来保存他们的关系。
3.一对一(少见)
-
user表和role表:一个用户可以有多个角色,一个角色可以对应多个用户,为多对多的关系,需要使用中间表user_role进行连接,通过user_id和role_id进行查询
-
role表和permission表:一个角色可以有多个权限,一个权限可以对应多个角色,为多对多的关系,需要使用中间表role_permission进行连接,通过role_id和permission_id进行查询
-
用户表、角色表、权限表联合起来查询,至少需要5张表
-
注意:id=11代表学生,id=3代表老师,密码888888;
-
邀请码确定班级
-
由于用了Spring-Security框架, enabled、locked为针对框架在表中设置的参数
5表联查权限代码:事先可以在数据库可视化工具进行测试,测试成功后,再复制到IDEA中
SELECT p.id, p.name
FROM user u
LEFT JOIN user_role ur ON u.id=ur.user_id
LEFT JOIN role r ON r.id=ur.role_id
LEFT JOIN role_permission rp ON rp.role_id=r.id
LEFT JOIN permission p ON p.id=rp.permission_id
WHERE u.id=11
查询结果:
在UserMapper接口中定义两个方法,代码如下:
package cn.tedu.knows.portal.mapper;
import cn.tedu.knows.portal.model.Permission;
import cn.tedu.knows.portal.model.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author tedu.cn
* @since 2021-08-23
*/
这两个方法都是登录时需要的,为了保证程序正确运行,推荐大家测试一下,尤其是5表联查,测试代码如下:
输出结果:
2021-08-24 21:43:02.160 INFO 13220 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-08-24 21:43:02.525 INFO 13220 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2021-08-24 21:43:02.535 DEBUG 13220 --- [ main] c.t.k.p.m.UserMapper.findUserByUsername : ==> Preparing: select * from user where username=?
2021-08-24 21:43:02.568 DEBUG 13220 --- [ main] c.t.k.p.m.UserMapper.findUserByUsername : ==> Parameters: st2(String)
2021-08-24 21:43:02.640 DEBUG 13220 --- [ main] c.t.k.p.m.UserMapper.findUserByUsername : <== Total: 1
User(id=11, username=st2, nickname=李四同学, password={bcrypt}$2a$10$ELGiEhKyLlO9r3.WVOkHDe16JTCKCErcABhElD5CF7ZwQ.Hm6sVRW, sex=保密, birthday=null, phone=null, classroomId=null, createtime=2021-03-13T22:36:59, enabled=1, locked=0, type=0, selfIntroduction=null)
2021-08-24 21:43:02.659 DEBUG 13220 --- [ main] c.t.k.p.m.U.findUserPermissionsById : ==> Preparing: SELECT p.id,p.name FROM user u LEFT JOIN user_role ur ON u.id=ur.user_id LEFT JOIN role r ON r.id=ur.role_id LEFT JOIN role_permission rp ON rp.role_id=r.id LEFT JOIN permission p ON p.id=rp.permission_id WHERE u.id=?
2021-08-24 21:43:02.660 DEBUG 13220 --- [ main] c.t.k.p.m.U.findUserPermissionsById : ==> Parameters: 11(Integer)
2021-08-24 21:43:02.688 DEBUG 13220 --- [ main] c.t.k.p.m.U.findUserPermissionsById : <== Total: 4
Permission(id=1, name=/index.html, desc=null)
Permission(id=2, name=/question/create, desc=null)
Permission(id=3, name=/question/uploadMultipleFile, desc=null)
Permission(id=4, name=/question/detail, desc=null)
2.实现数据库登录
现在我们登录需要使用UserDetails类实现,UserDetails译作:用户详情,是Spring-Security提供的一个类型,专门用于Spring-Security框架保存用户信息。我们要做的登录方法也是Spring-Security框架规定好的,所以我们只需要编写框架需要我们编写的内容,这个内容由Spring-Security框架提供了一个接口表现,这个接口叫UserDetailsService,其中提供了一个方法loadUserByUsername,参数是用户名,返回值是UserDetails对象。
我们在service包下的impl包下创建UserDetailsServiceImpl类,编写代码如下:
package cn.tedu.knows.portal.service.impl;
import cn.tedu.knows.portal.mapper.UserMapper;
import cn.tedu.knows.portal.model.Permission;
import cn.tedu.knows.portal.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.List;
security/SecurityConfig类修改配置:
package cn.tedu.knows.portal.security;
import cn.tedu.knows.portal.service.impl.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
2.1 设置可访问内容
一般的网站,有些页面是可以不登录就能访问的,那么我们先把我们要开发的首页进行设置,不登录就能访问,也是在SecurityConfig类中进行设置,但是重写不同的方法:
//设置当前Spring-Security框架的放行规则
2.2 自定义登录页
Spring-Security提供的登录页效果不理想,我们想将我们编写的登录页替换掉原有的,也是在SecurityConfig中进行配置,编写如下配置:
//设置当前Spring-Security框架的放行规则
测试结果:
(1)登录成功,响应index_student.html首页内容
(2)用户名或密码错误
(3)登录后无权限访问的页面
(4)登出成功页面
3.实现学生注册
注册页面:
注册流程:
1.学生获得邀请码访问注册页,填写表单信息
2.学生将表单提交到控制器,控制器接收表单信息
3.控制器调用业务逻辑层方法,将用户提交的表单发送到业务逻辑层
4.业务逻辑层进行各种验证,密码加密,实例化User对象,调用Mapper进行新增操作
5.UserMapper的新增方法是Mybatis Plus提供的!不用写代码。
6.返回信息到控制器,控制器返回注册结果到页面
注意:先完成同步注册,后修改为异步注册
2.1 注册准备
首先注册页面一定是不需要登录就能访问的,将SecurityConfig类的放行列表中添加注册页:
.antMatchers( //设置匹配路径
"/index_student.html", //学生首页
"/js/*", //当前目录下的所有文件
"/css/*", //当前目录下的所有文件
"/img/**", //当前目录及子目录下的所有文件
"/bower_components/**", //当前目录及子目录下的所有文件
"/login.html", //放行自定义登录页面路径
"/register.html", //放行注册页面
"/register" //放行注册路径
).permitAll() //上述路径全部允许直接访问
.anyRequest().authenticated() //其它请求需要登录之后才能访问
.and().formLogin() //登录使用表单验证
.loginPage("/login.html") //设置自定义登录页面路径
.loginProcessingUrl("/login") //设置处理登录的路径
.failureUrl("/login.html?error") //设置登陆失败路径,页面内置登录失败信息
.defaultSuccessUrl("/index_student.html") //设置登录成功的页面
.and().logout() //设置登出
.logoutUrl("/logout") //设置登出路径
.logoutSuccessUrl("/login.html?logout") //设置登出后显示登录页面
.and().csrf().disable(); //关闭防跨域攻击,不关闭无法登录(只要设置了自定义登录页面,就必须设置这个)
我们接收表单信息需要创建一个封装表单数据的类,创建vo包,包中创建RegisterVo的类,类中匹配表单的每个属性代码如下:
package cn.tedu.knows.portal.vo;
import lombok.Data;
我们的表单信息会发送到控制器,我们编写一个控制器类方法类接收它:在controller包中新建一个SystemController类,代码如下:
package cn.tedu.knows.portal.controller;
import cn.tedu.knows.portal.vo.RegisterVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
重启服务,进行注册,访问路径:http://localhost:8888/register.html,浏览器页面显示:ok,控制台输出注册信息:
收到注册信息:RegisterVo(inviteCode=JSD1912-876840, phone=12345678901, nickname=xiaocui, password=123456, confirm=123456)
1.3 java三层结构
在企业开发中,java代码实现业务时最少有三层结构:
-
Controller:控制层
-
从前端接收用户信息
-
向前端发送信息
-
除此之外的所有工作通过调用业务逻辑层完成
-
-
Service:业务逻辑层
-
除控制层和数据访问层之外所有其它工作,由业务层完成
-
例如对信息正确性的甄别、复杂业务的工作梳理以及信息的收集
-
-
-
Mapper:数据访问层
-
只负责和数据库进行沟通,完成增删改查方法
-
1.4 使用QueryWrapper
我们之前完成过一次使用Mybatis Plus提供的方法进行全查的测试:
List<Tag> tags=tagMapper.selectList(null);
上面的代码执行了全查操作,selectList(null)表示无条件查询,我们可以向selectList的()中添加条件实现按条件查询,而无需编写sql语句。
QueryWrapper类型的对象可以设置条件,并当做查询参数。下面这个测试,就演示如何使用它:
package cn.tedu.knows.portal;
import cn.tedu.knows.portal.mapper.ClassroomMapper;
import cn.tedu.knows.portal.model.Classroom;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
//SpringBoot测试类中必须写这个注解
输出结果:
上面测试中我们使用了eq方法,这个方法的含义是设置条件:判断是否相等,除了这个方法之外,常用的其他方法有:
-
lt(less than):小于
-
le(less equals):小于等于
-
gt(great than):大于
-
ge(great equals):大于等于
-
ne(not equals):不等于
1.5 自定义异常
我们马上要编写注册代码,在注册代码编写过程中,如果邀请码错误或者手机号已经被注册,我们要怎么反馈给用户呢?解决方案是使用异常机制。
如果发生了上述问题,就抛出一个异常对象,表示出了什么问题这个异常对象的类型不能是系统本身提供的,应该是一个我们自定义的异常,表示在我们编写业务过程中发生的错误问题。于是,我们在cn.tedu.knows.portal路径下创建一个exception包,包中新建一个ServiceException类,这个类的代码从提供给大家的ServiceException.txt文件中复制即可。
package cn.tedu.knows.portal.exception;
/**
* 自定义异常:都是一些构造方法的重载,只是参数列表不同
*/
public class ServiceException extends RuntimeException{
private int code = 500;
public ServiceException() { }
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public ServiceException(int code) {
this.code = code;
}
public ServiceException(String message, int code) {
super(message);
this.code = code;
}
public ServiceException(String message, Throwable cause,
int code) {
super(message, cause);
this.code = code;
}
public ServiceException(Throwable cause, int code) {
super(cause);
this.code = code;
}
public ServiceException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace, int code) {
super(message, cause, enableSuppression, writableStackTrace);
this.code = code;
}
public int getCode() {
return code;
}
}
1.6 编写注册的业务逻辑层
注册业务是对User的新增,所以要找User对应的业务逻辑层代码。业务逻辑层由一个接口和一个实现类组成(解耦),要先写接口,再写对应的实现类。User对应的接口是IUserService,对应的实现类是UserServiceImpl。
我们新编写一个业务应该从接口开始编写,在IUserService中添加一个注册方法:
package cn.tedu.knows.portal.service;
import cn.tedu.knows.portal.model.User;
import cn.tedu.knows.portal.vo.RegisterVo;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author tedu.cn
* @since 2021-08-23
*/
public interface IUserService extends IService<User> {
//学生注册的方法,参数RegisterVo
void registerStudent(RegisterVo registerVo);
}
下面要去实现注册方法,在接口上Ctrl+Alt+B跳到对应实现类,在UserServiceImpl类中编写代码:
package cn.tedu.knows.portal.service.impl;
import cn.tedu.knows.portal.exception.ServiceException;
import cn.tedu.knows.portal.mapper.ClassroomMapper;
import cn.tedu.knows.portal.mapper.UserRoleMapper;
import cn.tedu.knows.portal.model.Classroom;
import cn.tedu.knows.portal.model.User;
import cn.tedu.knows.portal.mapper.UserMapper;
import cn.tedu.knows.portal.model.UserRole;
import cn.tedu.knows.portal.service.IUserService;
import cn.tedu.knows.portal.vo.RegisterVo;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author tedu.cn
* @since 2021-08-23
*/