如果想在用户登录时用我们自己的登录页面代替Spring-Security提供的登录页面
需要进行如下配置
步骤1:
登录页面是视图模板引擎生成的,所以需要引入Thymeleaf的依赖
子项目的pom.xml文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
步骤2:
将static文件夹中的login.html
复制到templates文件夹下
需要注意
现在login.html提交的路径是/login
用户名和密码输入框的name是username和password
这两个名字也是Spring-Security约定的不要改!!
我们需要写一个控制器来访问显示这个页面
这个控制器不输于任何实体类,新建一个SystemController
@RestController
public class SystemController {
// 显示登录页面的方法
@GetMapping("/login.html")
public ModelAndView loginForm(){
//ModelAndView("login");对应的是resources/templates/login.html
return new ModelAndView("login");
}
}
步骤3:
要对login.html进行放行
要配置登录时的各种信息
要配置登出时的各种信息
SecurityConfig类中编写
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()//对当前全部请求进行授权
.antMatchers(
"/index.html",
"/img/*",
"/js/*",
"/css/*",
"/bower_components/**",
"/login.html"
)//设置路径
.permitAll()//允许全部请求访问上面定义的路径
//其它路径需要全部进行表单登录验证
.anyRequest().authenticated().and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureUrl("/login.html?error")
.defaultSuccessUrl("/index.html")
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html?logout");
}
方法说明:
1.csrf().disable():关闭防跨域攻击功能,不关闭容易发生错误
2.loginPage:指定登录页面路径
3.loginProcessingUrl:指定表单提交的路径
4.failureUrl:指定登录失败时的路径
5.defaultSuccessUrl:指定登录成功时的路径
6.logout():表示开始配置登出时的内容
7.logoutUrl:指定出的路径(当页面有这个请求时,Spring-Security去执行用户登出操作)
8.logoutSuccessUrl:指定登出成功之后显示的页面
每个网站都需要用户注册的功能
页面如下图
1.获得邀请码(开发过程是从数据库获得,运营时向老师索取)
2.通过登录页上的注册连接显示注册页面
3.向服务器请求注册页并显示到浏览器
4.注册页面填写信息并提交表单
5.服务器接收到表单信息,控制层调用业务逻辑层执行注册操作
6.业务层执行连库操作新增之前验证邀请码
7.邀请码验证通过在执行数据库新增操作
8.返回新增操作的运行结果
9.根据结果反馈到控制层,有异常就报异常
10.控制器将注册结果信息使用JSON返回给浏览器
11.浏览器中局部刷新页面,将注册结果显示给用户
步骤1:
复制static文件夹中的register.html页面到templates文件夹
步骤2:
编写控制器SystemController类中添加方法
// 显示注册页面的方法
@GetMapping("/register.html")
public ModelAndView register(){
return new ModelAndView("register");
}
步骤3:
SecurityConfig类中放行register.html
http.csrf().disable()
.authorizeRequests()//对当前全部请求进行授权
.antMatchers(
"/index.html",
"/img/*",
"/js/*",
"/css/*",
"/bower_components/**",
"/login.html",
"/register.html" //放行在这个!!!!!!
)//设置路径
.permitAll()//允许全部请求访问上面定义的路径
//其它路径需要全部进行表单登录验证
.anyRequest().authenticated().and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureUrl("/login.html?error")
.defaultSuccessUrl("/index.html")
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html?logout");
我们先编写一个简单的控制器代码接收表单的信息,并返回内容
在页面上显示
步骤1:
表单提交的5个属性创建一个Vo类接收代码如下
@Data
public class RegisterVo implements Serializable {
private String inviteCode;
private String phone;
private String nickname;
private String password;
private String confirm;
}
步骤2:
上面步骤1中的实体类能接收表单发送过来的信息
但是我们控制器处理完成后,想返回Json格式的对象给JS,也需要一个实体类
这个实体类最好能够通用于所有业务
现在行业中流行使用一个"R"类来返回JSON格式信息
这个R类中主要包含3个属性
1.状态码
2.状态消息
3.实体(控制器查询出的任何内容)
创建R类(代码无需掌握,会使用即可)
@Data
@Accessors(chain = true)
public class R<T> implements Serializable {
/** 200 OK - [GET]:服务器成功返回用户请求的数据 */
public static final int OK = 200;
/** 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。 */
public static final int CREATED = 201;
/** 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务) */
public static final int ACCEPTED = 202;
/** 204 NO CONTENT - [DELETE]:用户删除数据成功。 */
public static final int NO_CONTENT = 204;
/** 400 INVALID REQUEST - [POST/PUT/PATCH]:
用户发出的请求有错误,服务器没有进行新建或修改数据的操作。*/
public static final int INVALID_REQUEST = 400;
/** 401 Unauthorized - [*]:
表示用户没有权限(令牌、用户名、密码错误)。 */
public static final int UNAUTHORIZED = 401;
/** 403 Forbidden - [*]
表示用户得到授权(与401错误相对),但是访问是被禁止的。*/
public static final int FORBIDDEN = 403;
/** 404 NOT FOUND - [*]:
用户发出的请求针对的是不存在的记录,服务器没有进行操作。 */
public static final int NOT_FOUND = 404;
/** 410 Gone -[GET]:
用户请求的资源被永久删除,且不会再得到的。*/
public static final int GONE = 410;
/** 422 Unprocesable entity - [POST/PUT/PATCH]
当创建一个对象时,发生一个验证错误。 */
public static final int UNPROCESABLE_ENTITY = 422;
/** 500 INTERNAL SERVER ERROR - [*]:
服务器发生错误,用户将无法判断发出的请求是否成功。 */
public static final int INTERNAL_SERVER_ERROR = 500;
private int code;
private String message;
private T data;
/**
* 服务器成功返回用户请求的数据
* @param message 消息
*/
public static R ok(String message){
return new R().setCode(OK).setMessage(message);
}
/**
* 服务器成功返回用户请求的数据
* @param data 数据
*/
public static R ok(Object data){
return new R().setMessage("OK").setCode(OK).setData(data);
}
/**
* 用户新建或修改数据成功。
*/
public static R created(String message){
return new R().setCode(CREATED).setMessage(message);
}
/**
* 表示一个请求已经进入后台排队(异步任务)
*/
public static R accepted(String message){
return new R().setCode(ACCEPTED).setMessage(message);
}
/**
* 用户删除数据成功
*/
public static R noContent(String message){
return new R().setCode(NO_CONTENT).setMessage(message);
}
/**
* 用户发出的请求有错误,服务器没有进行新建或修改数据的操作。
*/
public static R invalidRequest(String message){
return new R().setCode(INVALID_REQUEST).setMessage(message);
}
/**
* 表示用户没有权限(令牌、用户名、密码错误)
*/
public static R unauthorized(String message){
return new R().setCode(UNAUTHORIZED).setMessage(message);
}
/**
* 登录以后,但是没有足够权限
*/
public static R forbidden(){
return new R().setCode(FORBIDDEN).setMessage("权限不足!");
}
/**
* 用户发出的请求针对的是不存在的记录,服务器没有进行操作。
*/
public static R notFound(String message){
return new R().setCode(NOT_FOUND).setMessage(message);
}
/**
* 用户请求的资源被永久删除,且不会再得到的。
*/
public static R gone(String message){
return new R().setCode(GONE).setMessage(message);
}
/**
* 当创建一个对象时,发生一个验证错误。
*/
public static R unproecsableEntity(String message){
return new R().setCode(UNPROCESABLE_ENTITY)
.setMessage(message);
}
/**
* 将异常消息复制到返回结果中
*/
public static R failed(ServiceException e){
return new R().setCode(e.getCode())
.setMessage(e.getMessage());
}
/**
* 服务器发生错误,用户将无法判断发出的请求是否成功。
*/
public static R failed(Throwable e){
return new R().setCode(INTERNAL_SERVER_ERROR)
.setMessage(e.getMessage());
}
}
步骤3:
上面两个步骤分别解决了控制器的参数和返回值的问题,
下面我们就测试一个控制器代码,观察是否能够获得参数信息,并返回
SystemController中编写代码
@RestController
@Slf4j//启动日志功能!
public class SystemController {
//省略其它方法....
@PostMapping("/register")
public R registerStudent(RegisterVo registerVo){
System.out.println(registerVo);
log.debug("得到信息为:{}",registerVo);
return R.created("注册成功!");
}
}
步骤4:
配置/register请求的放行
SecurityConfig代码中
http.csrf().disable()
.authorizeRequests()//对当前全部请求进行授权
.antMatchers(
"/index.html",
"/img/*",
"/js/*",
"/css/*",
"/bower_components/**",
"/login.html",
"/register.html",
"/register" //放行注册业务!!!!!!
)//设置路径
.permitAll()//允许全部请求访问上面定义的路径
//其它路径需要全部进行表单登录验证
.anyRequest().authenticated().and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.failureUrl("/login.html?error")
.defaultSuccessUrl("/index.html")
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html?logout");
步骤5:
配置日志等级
在控制器的代码中我们启用了日志
需要配置debug等级才能显示在控制台
application.properties文件
logging.level.cn.tedu.straw.portal.mapper=trace
logging.level.cn.tedu.straw.portal=debug
步骤1:
注册业务逻辑属于User表
所以在IUserService接口中新建注册方法
// 用户注册的方法(现在是针对学生注册)
void registerStudent(RegisterVo registerVo);
步骤2:
在IUserService的实现类UserServiceImpl类中重写接口的方法
在方法中排定业务逻辑顺序
@Override
public void registerStudent(RegisterVo registerVo) {
//判断registerVo非空
//根据输入的邀请码查询班级,验证邀请码有效性
//验证数据库中是否已经注册过输入的用户名(手机号)
//User对象的赋值(将表单中的值和一些默认值确定后)
//执行User新增
//验证新增结果
//将新增的用户赋予学生的角色(新增user_role的关系表)
//验证关系表新增结果
}
步骤3:
编写判定以及验证时需要的自定义业务异常类
这个类和R类相同也不需要掌握代码,只需要掌握用法
package cn.tedu.straw.portal.service;
import cn.tedu.straw.portal.vo.R;
public class ServiceException extends RuntimeException{
private int code = R.INTERNAL_SERVER_ERROR;
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;
}
/** 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作。*/
public static ServiceException invalidRequest(String message){
return new ServiceException(message, R.INVALID_REQUEST);
}
/** 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作。 */
public static ServiceException notFound(String message){
return new ServiceException(message, R.NOT_FOUND);
}
/** 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。*/
public static ServiceException gone(String message){
return new ServiceException(message, R.GONE);
}
/** 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。 */
public static ServiceException unprocesabelEntity(String message){
return new ServiceException(message, R.UNPROCESABLE_ENTITY);
}
}
步骤4:
完成UserServiceImpl类的编写
@Service
@Slf4j//添加了log4j的支持
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails getUserDetails(String username) {
// 此方法代码略
}
@Autowired
ClassroomMapper classroomMapper;
@Autowired
UserRoleMapper userRoleMapper;
BCryptPasswordEncoder passwordEncoder=
new BCryptPasswordEncoder();
@Override
public void registerStudent(RegisterVo registerVo) {
//判断registerVo非空
if(registerVo==null){
//如果信息是空则发生异常
//这里的异常逻辑是我们编写的项目发生的,不是系统异常
//所以这里以及以后的方法中都需要抛出自定义的异常
throw ServiceException.unprocesabelEntity("表单数据为空");
}
//根据输入的邀请码查询班级,验证邀请码有效性
QueryWrapper<Classroom> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("invite_code",registerVo.getInviteCode());
Classroom classroom=classroomMapper.selectOne(queryWrapper);
log.debug("邀请码对应的班级为:{}",classroom);
if(classroom==null){
throw ServiceException.unprocesabelEntity("邀请码错误!");
}
//验证数据库中是否已经注册过输入的用户名(手机号)
//用户名查询用户对象
User u=userMapper.findUserByUsername(registerVo.getPhone());
if(u!=null){
//用户已存在
throw ServiceException.unprocesabelEntity("手机号已经注册!");
}
//User对象的赋值(将表单中的值和一些默认值确定后)
User user=new User();
user.setUsername(registerVo.getPhone());
user.setPhone(registerVo.getPhone());
user.setNickname(registerVo.getNickname());
//用户输入的是明文密码,数据库保存的是带算法ID的加密结果!
user.setPassword("{bcrypt}"+
passwordEncoder.encode(registerVo.getPassword()));
user.setClassroomId(classroom.getId());
user.setCreatetime(LocalDateTime.now());
user.setEnabled(1);
user.setLocked(0);
//执行User新增
int num=userMapper.insert(user);
//验证新增结果
if(num!=1) {
throw new ServiceException("服务器忙,稍后再试");
}
//将新增的用户赋予学生的角色(新增user_role的关系表)
UserRole userRole=new UserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(2);
num=userRoleMapper.insert(userRole);
//验证关系表新增结果
if(num!=1) {
throw new ServiceException("服务器忙,稍后再试");
}
}
}
步骤5:
编写测试类
@Autowired
IUserService userService;
@Test
public void testUserService(){
RegisterVo registerVo=new RegisterVo();
registerVo.setPhone("13333113131");
registerVo.setNickname("大树");
registerVo.setInviteCode("JSD1912-876840");
registerVo.setPassword("123456");
registerVo.setConfirm("123456");
userService.registerStudent(registerVo);
System.out.println("complate!");
}
@Autowired
IUserService userService;
@PostMapping("/register")
public R registerStudent(RegisterVo registerVo){
System.out.println(registerVo);
log.debug("得到信息为:{}",registerVo);
try{
userService.registerStudent(registerVo);
return R.created("注册成功!");
}catch (ServiceException e){
log.error("注册失败",e);
return R.failed(e);
}
}
也是一个js为基础的前端框架
提供了一套前端信息和服务器信息交互的一种方式
这种方式要比以前的信息交互方式简单
一般情况下,程序要结合JQuery的ajax操作和Vue的功能完成前后端信息交互
使用准备
static文件夹下创建一个测试Vue的页面vue.html
这个页面项目中不使用,就是测使用
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script src="bower_components/vue/dist/vue.js">script>
head>
<body>
<div id="app">
<p v-text="message">VUE演示p>
<input type="text" v-model="content">
<button type="button" v-on:click="hello">按钮button>
div>
body>
<script>
//使用Vue
let app=new Vue({
el:"#app",
data:{
message:"Vue发送的文字!",
content:"输入框的内容"
},
methods:{
hello:function(){
console.log(this.content);
//this.content=this.content+"!";
}
}
});
script>
html>
Vue功能的强大之处在于信息实时同步的双向绑定
修改register.html代码
DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>稻草FAQ注册title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="bower_components/plugins/css/vueAlert.css">
<link rel="stylesheet" href="css/login.css">
<script src="bower_components/jquery/dist/jquery.min.js" >script>
<script src="bower_components/bootstrap/dist/js/bootstrap.min.js" >script>
<script src="bower_components/vue/dist/vue.js">script>
head>
<body class="bg-light">
<div class="container-fluid">
<div class="row">
<div class="mx-auto mt-5" style="width: 400px">
<div class="alsrtInfo" style="display: none">
<div class="profPrompt_test" >div>
div>
<h2 class="text-center">达内稻草问答系统h2>
<div class="bg-white p-4" id="app">
<p class="text-center"><b>注册新用户b>p>
<div id="error" class="alert alert-danger" style="display: none">
<i class="fa fa-exclamation-triangle">i> <span >邀请码错误!span>
div>
<form action="/register" method="post"
v-on:submit.prevent="register">
<div class="form-group has-icon">
<input type="text" name="inviteCode" class="form-control" placeholder="请输入邀请码"
required="required" v-model="inviteCode">
<span class="fa fa-barcode form-control-icon">span>
div>
<div class="form-group has-icon">
<input type="tel" name="phone" class="form-control" placeholder="请输入手机号"
pattern="^\d{11}$" required="required" v-model="phone" >
<span class=" fa fa-phone form-control-icon">span>
div>
<div class="form-group has-icon">
<input type="text" name="nickname" class="form-control" placeholder="请设置昵称,字数为2-20之间"
pattern="^.{2,20}$" required="required" v-model="nickname" >
<span class="fa fa-user form-control-icon">span>
div>
<div class="form-group has-icon">
<input type="password" name="password" class="form-control" placeholder="设置密码6-20个字母、数字、下划线"
required="required" pattern="^\w{6,20}$" v-model="password" >
<span class="fa fa-lock form-control-icon">span>
div>
<div class="form-group has-icon">
<input type="password" name="confirm" class="form-control" placeholder="请再次输入密码"
required="required" v-model="confirm">
<span class="fa fa-lock form-control-icon">span>
div>
<button type="submit" class="btn btn-primary btn-block btn-flat" >注册button>
form>
<a href="login.html" class="text-center d-inline-block mt-3">已有账号,立即登录a>
div>
div>
div>
div>
body>
<script src="js/utils.js">script>
<script src="js/register.js">script>
html>
修改static/js/register.js代码
let app = new Vue({
el:'#app',
data:{
inviteCode:'',
phone:'',
nickname:'',
password:'',
confirm:''
},
methods:{
register:function () {
console.log('Submit');
let data = {
inviteCode: this.inviteCode,
phone: this.phone,
nickname: this.nickname,
password: this.password,
confirm: this.confirm
}
console.log(data);
if(data.password !== data.confirm){
return;
}
$.ajax({
url:"/register",
method: "POST",
data: data,
success: function (r) {
console.log(r);
if(r.code == CREATED){
console.log("注册成功");
console.log(r.message);
}else{
console.log(r.message);
}
}
});
}
}
});