day67(SpringBoot3:Lombok框架,Slf4j日志框架,密码加密,控制层开发)
day67(SpringBoot3:Lombok框架,Slf4j日志框架,密码加密,控制层开发)
1. 使用Lombok框架
1.往常做法
- 在编写POJO类型(包括实体类、VO、DTO等)时,都有统一的编码规范,例如:
- 属性都是私有的
- 所有属性都有对应的Setter & Getter方法
- 应该重写
equals()
和hashCode()
方法,以保证:如果2个对象的字面值完全相同,则equals()
对比结果为true
,且hashCode()
返回值相同,如果2个对象的字面值不相同,则equals()
对比结果为false
,且hashCode()
返回值不同 - 实现
Serializable
接口
另外,为了便于观察对象的各属性值,通常还会重写toString()
方法。
由于以上操作方式非常固定,且涉及的代码量虽然不难,但是篇幅较长,并且,当类中的属性需要修改时(包括修改原有属性、或增加新属性、删除原有属性),对应的其它方法都需要修改(或重新生成),管理起来比较麻烦。
2.使用Lombok框架
使用Lombok框架可以极大的简化这些操作,此框架可以通过注解的方式,在编译期来生成Setters & Getters、equals()
、hashCode()
、toString()
,甚至生成构造方法等,所以,一旦使用此框架,开发人员就只需要在类中声明各属性、实现Serializable
、添加Lombok指定的注解即可。
在Spring Boot中,添加Lombok依赖,可以在创建项目时勾选,也可以后期自行添加,依赖项的代码为:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
完成后,在各POJO类型中,将不再需要在源代码添加Setters & Getters、equals()
、hashCode()
、toString()
这些方法,只需要在POJO类上添加@Data
注解即可!
当添加@Data
注解,且删除相关方法后,由于源代码中没有相关方法,则调用了相关代码的方法可能会报错,但是,并不影响程序运行!
2. Slf4j日志框架
-
在开发实践中,不允许使用
System.out.println()
或类似的输出语句来输出显示关键数据(核心数据、敏感数据等),因为,如果是这样使用,无论是在开发环境,还是测试环境,还是生产环境中,这些输出语句都将输出相关信息,而删除或添加这些输出语句的操作成本比较高,操作可行性低。 -
推荐的做法是使用日志框架来输出相关信息!
-
当添加了Lombok依赖后,可以在需要使用日志的类上添加
@Slf4j
注解,然后,在类的任意中,均可使用名为log
的变量,且调用其方法来输出日志(名为log
的变量也是Lombok框架在编译期自动补充的声明并创建对象)! -
在Slf4j日志框架中,将日志的可显示级别根据其重要程度(严重程度)由低到高分为:
- trace:跟踪信息
- debug:调试级别
- info:一般信息,通常不涉及关键流程和敏感信息
- warn:警告信息,通常代码可以运行,但不够完美,规范
- error:错误信息
- 在配置文件中,可以通过
logging.level.包名.类名
来设置当前类的日志显示级别,例如:
logging.level.cn.tedu.boot.demo.service.impl.AdminServiceImpl: info
-
当设置了显示的日志级别后,仅显示设置级别和更重要的级别的日志,例如,设置为
info
时,只显示info
、warn
、error
,不会显示debug
、trace
级别的日志! -
当输出日志时,通过
log
变量调用trace()
方法输出的日志就是trace
级别的,调用debug()
方法输出的日志就是debug()
级别的,以此类推,可调用的方法还有info()
、warn()
、error()
。 -
在开发实践中,关键数据和敏感数据都应该通过
trace()
或debug()
进行输出,在开发环境中,可以将日志的显示级别设置为trace
,则会显示所有日志,当需要交付到生产环境中时,只需要将日志的显示级别调整为info
即可! -
默认情况下,日志的显示级别是info, 显示
info
、warn
、error
,不会显示debug
、trace
级别的日志! -
当输出日志时,通过
log
变量调用trace()
方法输出的日志就是trace
级别的,调用debug()
方法输出的日志就是debug()
级别的,以此类推,可调用的方法还有info()
、warn()
、error()
。 -
在开发实践中,关键数据和敏感数据都应该通过
trace()
或debug()
进行输出,在开发环境中,可以将日志的显示级别设置为trace
,则会显示所有日志,当需要交付到生产环境中时,只需要将日志的显示级别调整为info
即可!默认情况下,日志的显示级别是
info
,所以,即使没有在配置文件中进行正确的配置,所有info、warn、error级别的日志都会输出显示。在配置时,属性名称中的
logging.level
部分是必须的,在其后,必须写至少1级包名,例如:logging.level.cn: trace
以上配置表示
cn
包及其子孙包下的所有类中的日志都按照trace
级别进行显示!在开发实践中,属性名称通常配置为
logging.level.项目根包
,例如:logging.level.cn.tedu.boot.demo: trace
在使用Slf4j时,通过
log
调用的每种级别的方法都被重载了多次(各级别对应除了方法名称不同,重载的次数和参数列表均相同),推荐使用的方法是参数列表为(String format, Object... arguments)
的,例如:public void trace(String format, Object... arguments); public void debug(String format, Object... arguments); public void info(String format, Object... arguments); public void warn(String format, Object... arguments); public void error(String format, Object... arguments);
以上方法中,第1个参数是将要输出的字符串的模式(模版),在此字符串中,如果需要包含某个变量值,则使用
{}
表示,如果有多个变量值,均是如此,然后,再通过第2个参数(是可变参数)依次表示各{}
对应的值,例如:log.debug("加密前的密码:{},加密后的密码:{}", password, encodedPassword);
使用这种做法,可以避免多变量时频繁的拼接字符串,另外,日志框架会将第1个参数进行缓存,以此提高后续每一次的执行效率。
在开发实践中,应该对程序执行关键位置添加日志的输出,通常包括:
- 每个方法的第1行有效语句,表示代码已经执行到此方法内,或此方法已经被成功调用
- 如果方法是有参数的,还应该输出参数的值
- 关键数据或核心数据在改变之前和之后
- 例如对密码加密时,应该通过日志输出加密前和加密后的密码
- 重要的操作执行之前
- 例如尝试插入数据之前、修改数据之前,应该通过日志输出相关值
- 程序走到某些重要的分支时
- 例如经过判断,走向抛出异常之前
其实,Slf4j日志框架只是日志的一种标准,并不是具体的实现(感觉上与Java中的接口有点相似),常见有具体实现了日志功能的框架有log4j、logback等,为了统一标准,所以才出现了Slf4j,同时,由于log4j、logback等框架实现功能并不统一,所以,Slf4j提供了对主流日志框架的兼容,在Spring Boot工程中,
spring-boot-starter
就已经依赖了spring-boot-starter-logging
,而在此依赖下,通常包括Slf4j、具体的日志框架、Slf4j对具体日志框架的兼容。 - 每个方法的第1行有效语句,表示代码已经执行到此方法内,或此方法已经被成功调用
-
3.密码加密
【这并不是Spring Boot框架的知识点】
1.作用
对密码进行加密,可以有效的保障密码安全,即使出现数据库泄密,密码安全也不会受到影响!为了实现此目标,需要在对密码进行加密时,使用不可逆的算法进行处理!
通常,不可以使用加密算法对密码进行加密码处理,从严格定义上来看,所有的加密算法都是可以逆向运算的,即同时存在加密和解密这2种操作,加密算法只能用于保证传输过程的安全,并不应该用于保证需要存储下来的密码的安全!
哈希算法都是不可逆的,通常,用于处理密码加密的算法中,典型的是一些消息摘要算法,例如MD5、SHA256或以上位数的算法。
2.消息摘要
消息摘要算法的主要特征有:
- 消息相同时,摘要一定相同
- 某种算法,无论消息长度多少,摘要的长度是固定的
- 消息不同时,摘要几乎不会相同
在消息摘要算法中,以MD5为例,其运算结果是一个128位长度的二进制数,通常会转换成十六进制数显示,所以是32位长度的十六进制数,MD5也被称之为128位算法。理论上,会存在2的128次方种类的摘要结果,且对应2的128次方种不同的消息,如果在未超过2的128次方种消息中,存在2个或多个不同的消息对应了相同的摘要,则称之为:发生了碰撞。一个消息摘要算法是否安全,取决其实际的碰撞概率,关于消息摘要算法的破解,也是研究其碰撞概率。
存在穷举消息和摘要的对应关系,并利用摘要在此对应关系进行查询,从而得知消息的做法,但是,由于MD5是128位算法,全部穷举是不可能实现的,所以,只要原始密码(消息)足够复杂,就不会被收录到所记录的对应关系中去!
3.盐
为了进一步提高密码的安全性,在使用消息摘要算法进行处理时,通常还会加盐!盐值可以是任意的字符串,用于与密码一起作为被消息摘要算法运算的数据即可,例如:
@Test
public void md5Test() {
String rawPassword = "123456";
String salt = "kjfcsddkjfdsajfdiusf8743urf";
String encodedPassword = DigestUtils.md5DigestAsHex(
(salt + salt + rawPassword + salt + salt).getBytes());
System.out.println("原密码:" + rawPassword);
System.out.println("加密后的密码:" + encodedPassword);
}
加盐的目的是使得被运算数据变得更加复杂,盐值本身和用法并没有明确要求!
甚至,在某些用法或算法中,还会使用随机的盐值,则可以使用完全相同的原消息对应的摘要却不同!
推荐了解:预计算的哈希链、彩虹表、雪花算法。
为了进一步保证密码安全,还可以使用多重加密,即反复调用消息摘要算法。
除此以外,还可以使用安全系数更高的算法,例如SHA-256是256位算法,SHA-384是384位算法,SHA-512是512位算法。
4.应用
一般的应用方式可以是:
public class PasswordEncoder {
public String encode(String rawPassword) {
// 加密过程
// 1. 使用MD5算法
// 2. 使用随机的盐值
// 3. 循环5次
// 4. 盐的处理方式为:盐 + 原密码 + 盐 + 原密码 + 盐
// 注意:因为使用了随机盐,盐值必须被记录下来,本次的返回结果使用$分隔盐与密文
String salt = UUID.randomUUID().toString().replace("-", "");
String encodedPassword = rawPassword;
for (int i = 0; i < 5; i++) {
encodedPassword = DigestUtils.md5DigestAsHex(
(salt + encodedPassword + salt + encodedPassword + salt).getBytes());
}
return salt + encodedPassword;
}
public boolean matches(String rawPassword, String encodedPassword) {
String salt = encodedPassword.substring(0, 32);
String newPassword = rawPassword;
for (int i = 0; i < 5; i++) {
newPassword = DigestUtils.md5DigestAsHex(
(salt + newPassword + salt + newPassword + salt).getBytes());
}
newPassword = salt + newPassword;
return newPassword.equals(encodedPassword);
}
}
4.控制器层开发
1.添加依赖
pring MVC是用于处理控制器层开发的,在使用Spring Boot时,在pom.xml
中添加spring-boot-starter-web
即可整合Spring MVC框架及相关的常用依赖项(包含jackson-databind
),可以将已存在的spring-boot-starter
直接改为spring-boot-starter-web
,因为在spring-boot-starter-web
中已经包含了spring-boot-starter
。
2.添加注解
先在项目的根包下创建controller
子包,并在此子包下创建AdminController
,此类应该添加@RestController
和@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
注解,例如:
@RestController
@RequestMapping(values = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
}
由于已经决定了服务器端响应时,将响应JSON格式的字符串,为保证能够响应JSON格式的结果,处理请求的方法返回值应该是自定义的数据类型,则从此前学习的spring-mvc
项目中找到JsonResult
类及相关类型,复制到当前项目中来。
3.增加管理员
接下来,即可在AdminController
中添加处理“增加管理员”的请求:
@Autowired
private IAdminService adminService;
// 注意:暂时使用@RequestMapping,不要使用@PostMapping,以便于直接在浏览器中测试
// http://localhost:8080/admins/add-new?username=root&password=1234
@RequestMapping("/add-new")
public JsonResult<Void> addNew(AdminAddNewDTO adminAddNewDTO) {
adminService.addNew(adminAddNewDTO);
return JsonResult.ok();
}
完成后,运行启动类,即可启动整个项目,在spring-boot-starter-web
中,包含了Tomcat的依赖项,在启动时,会自动将当前项目打包并部署到此Tomcat上,所以,执行启动类时,会执行此Tomcat,同时,因为是内置的Tomcat,只为当前项目服务,所以,在将项目部署到Tomcat时,默认已经将Context Path(例如spring_mvc_war_exploded)配置为空字符串,所以,在启动项目后,访问的URL中并没有此前遇到的Context Path值。
当项目启动成功后,即可在浏览器的地址栏中输入网址进行测试访问!
注意:如果是未添加的管理员账号,可以成功执行结束,如果管理员账号已经存在,由于尚未处理异常,会提示500错误。
4.处理异常
1.在state新增枚举
关于处理异常,应该先在State
中确保有每种异常对应的枚举值,例如本次需要补充InsertException
对应的枚举值:
public enum State {
OK(200),
ERR_USERNAME(201),
ERR_PASSWORD(202),
ERR_INSERT(500); // 新增的枚举值
// 原有其它代码
}
2.统一处理异常
然后,在cn.tedu.boot.demo.controller
下创建handler.GlobalExceptionHandler
类,用于统一处理异常,例如:
package cn.tedu.boot.demo.controller.handler;
import cn.tedu.boot.demo.ex.ServiceException;
import cn.tedu.boot.demo.ex.UsernameDuplicateException;
import cn.tedu.boot.demo.web.JsonResult;
import cn.tedu.boot.demo.web.State;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
if (e instanceof UsernameDuplicateException) {
return JsonResult.fail(State.ERR_USERNAME, "用户名错误!");
} else {
return JsonResult.fail(State.ERR_INSERT, "插入数据失败!");
}
}
}
完成后,重新启动项目,当添加管理员时的用户名没有被占用时,将正常添加,当用户名已经被占用时,会根据处理异常的结果进行响应!
3.描述异常原因
由于在统一处理异常的机制下,同一种异常,无论是在哪种业务中出现,处理异常时的描述信息都是完全相同的,也无法精准的表达错误信息,这是不合适的!另外,基于面向对象的“分工”思想,关于错误信息(异常对应的描述信息),应该是由Service来描述,即“谁抛出谁描述”,因为抛出异常的代码片段是最了解、最明确出现异常的原因的!
为了更好的描述异常的原因,应该在自定义的ServiceException
和其子孙类异常中添加基于父类的全部构造方法(5个),然后,在AdminServiceImpl
中,当抛出异常时,可以在异常的构造方法中添加String
类型的参数,对异常发生的原因进行描述,例如:
@Override
public void addNew(AdminAddNewDTO adminAddNewDTO) {
// ===== 原有其它代码 =====
// 判断查询结果是否不为null
if (queryResult != null) {
// 是:表示用户名已经被占用,则抛出UsernameDuplicateException
log.error("此账号已经被占用,将抛出异常");
throw new UsernameDuplicateException("添加管理员失败,用户名(" + username + ")已经被占用!");
}
// ===== 原有其它代码 =====
// 判断以上返回的结果是否不为1,抛出InsertException异常
if (rows != 1) {
throw new InsertException("添加管理员失败,服务器忙,请稍后再次尝试!");
}
}
最后,在处理异常时,可以调用异常对象的getMessage()
方法获取抛出时封装的描述信息,例如:
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
if (e instanceof UsernameDuplicateException) {
return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
} else {
return JsonResult.fail(State.ERR_INSERT, e.getMessage());
}
}
完成后,再次重启项目,当用户名已经存在时,可以显示在Service中描述的错误信息!
最后,当添加成功时,响应的JSON数据例如:
{
"state":200,
"message":null,
"data":null
}
当用户名冲突,添加失败时,响应的JSON数据例如:
{
"state":201,
"message":"添加管理员失败,用户名(liuguobin)已经被占用!",
"data":null
}
4.对不必要的值进行注解(@JsonInclude(JsonInclude.Include.NON_NULL))
可以看到,无论是成功还是失败,响应的JSON中都包含了不必要的数据(为null
的数据),这些数据属性是没有必要响应到客户端的,如果需要去除这些不必要的值,可以在对应的属性上使用注解进行配置,例如:
@Data
public class JsonResult<T> implements Serializable {
// 状态码,例如:200
private Integer state;
// 消息,例如:"登录失败,用户名不存在"
@JsonInclude(JsonInclude.Include.NON_NULL)
private String message;
// 数据
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
// ===== 原有其它代码 =====
}
则响应的JSON中只会包含不为null
的部分。
此注解还可以添加在类上,则作用于当前类中所有的属性,例如:
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonResult<T> implements Serializable {
// ===== 原有其它代码 =====
}
即使添加在类上,也只对当前类的3个属性有效,后续,当响应某些数据时,data
属性可能是用户、商品、订单等类型,这些类型的数据中为null
的部分依然会被响应到客户端去,所以,还需要对这些类型也添加相同的注解配置!
以上做法相对比较繁琐,可以在application.properties
/ application.yml
中添加全局配置,则作用于当前项目中所有响应时涉及的类,例如在properties
中配置为:
spring.jackson.default-property-inclusion=non_null
5.在yml
中配置为:
spring:
jackson:
default-property-inclusion: non_null
注意:当你需要在yml
中添加以上配置时,前缀属性名可能已经存在,则不允许出现重复的前缀属性名,例如以下配置就是错误的:
spring:
profiles:
active: dev
spring: # 此处就出现了相同的前缀属性名,是错误的
jackson:
default-property-inclusion: non_null
正确的配置例如:
spring:
profiles:
active: dev
jackson:
default-property-inclusion: non_null
最后,以上配置只是“默认”配置,如果在某些类型中还有不同的配置需求,仍可以在类或属性上通过@JsonInclude
进行配置。
5. Validation框架
1.validation作用
当客户端向服务器提交请求时,如果请求数据出现明显的问题(例如关键数据为null
、字符串的长度不在可接受范围内、其它格式错误),应该直接响应错误,而不是将明显错误的请求参数传递到Service!
关于判断错误,只有涉及数据库中的数据才能判断出结果的,都由Service进行判断,而基本的格式判断,都由Controller进行判断。
Validation框架是专门用于解决检查数据基本格式有效性的,最早并不是Spring系列的框架,目前,Spring Boot提供了更好的支持,所以,通常结合在一起使用。
2.添加依赖
在Spring Boot项目中,需要添加spring-boot-starter-validation
依赖项,例如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
3.添加注解
在控制器中,首先,对需要检查数据格式的请求参数添加@Valid
或@Validated
注解(这2个注解没有区别),例如:
@RequestMapping("/add-new")
public JsonResult<Void> addNew(@Validated AdminAddNewDTO adminAddNewDTO) {
adminService.addNew(adminAddNewDTO);
return JsonResult.ok();
}
真正需要检查的是AdminAddNewDTO
中各属性的值,所以,接下来需要在此类的各属性上通过注解来配置检查的规则,例如:
@Data
public class AdminAddNewDTO implements Serializable {
@NotNull // 验证规则为:不允许为null
private String username;
// ===== 原有其它代码 =====
}
4.处理报错
重启项目,通过不提交用户名的URL(例如:http://localhost:8080/admins/add-new)进行访问,在浏览器上会出现400错误页面,并且,在IntelliJ IDEA的控制台会出现以下警告:
2022-06-07 11:37:53.424 WARN 6404 --- [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [
org.springframework.validation.BindException:
org.springframework.validation.BeanPropertyBindingResult: 1 errors<EOL>Field error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]]
从警告信息中可以看到,当验证失败时(不符合所使用的注解对应的规则时),会出现org.springframework.validation.BindException
异常,则自行处理此异常即可!
1.添加枚举
首先,在State
中添加新的枚举:
public enum State {
OK(200),
ERR_USERNAME(201),
ERR_PASSWORD(202),
ERR_BAD_REQUEST(400), // 新增
ERR_INSERT(500);
// ===== 原有其它代码 =====
}
2.添加异常处理方法
然后,在GlobalExceptionHandler
中添加新的处理异常的方法:
@ExceptionHandler(BindException.class)
public JsonResult<Void> handleBindException(BindException e) {
return JsonResult.fail(State.ERR_BAD_REQUEST, e.getMessage());
}
完成后,再次重启项目,继续使用为null
的用户名提交请求时,可以看到异常已经被处理,此时,响应的JSON数据例如:
{
"state":400,
"message":"org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'adminAddNewDTO' on field 'username': rejected value [null]; codes [NotNull.adminAddNewDTO.username,NotNull.username,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.username,username]; arguments []; default message [username]]; default message [不能为null]\nField error in object 'adminAddNewDTO' on field 'password': rejected value [null]; codes [NotNull.adminAddNewDTO.password,NotNull.password,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [adminAddNewDTO.password,password]; arguments []; default message [password]]; default message [不能为null]"
}
3.@NotNull注解
关于错误提示信息,以上内容中出现了不能为null
的字样,是默认的提示文本,可以通过@NotNull
注解的message
属性进行配置,例如:
@Data
public class AdminAddNewDTO implements Serializable {
@NotNull(message = "添加管理员失败,请提交用户名!")
private String username;
@NotNull(message = "添加管理员失败,请提交密码!")
private String password;
// ===== 原有其它代码 =====
}
4.自定义提示文本
然后,在处理异常时,通过异常信息获取自定义的提示文本:
@ExceptionHandler(BindException.class)
public JsonResult<Void> handleBindException(BindException e) {
BindingResult bindingResult = e.getBindingResult();
String defaultMessage = bindingResult.getFieldError().getDefaultMessage();
return JsonResult.fail(State.ERR_BAD_REQUEST, defaultMessage);
}
再次运行,在不提交用户名和密码的情况下,会随机的提示用户名或密码验证失败的提示文本中的某1条。
5.validation注解
在Validation框架中,还有其它许多注解,用于进行不同格式的验证,例如:
@NotEmpty
:只能添加在String
类型上,不许为空字符串,例如""
即视为空字符串@NotBlank
:只能添加在String
类型上,不允许为空白,例如普通的空格可视为空白,使用TAB键输入的内容也是空白,(虽然不太可能在此处出现)换行产生的空白区域也是空白@Size
:限制大小@Min
:限制最小值@Max
:限制最大值@Range
:可以配置min
和max
属性,同时限制最小值和最大值@Pattern
:只能添加在String
类型上,自行指定正则表达式进行验证- 其它
以上注解,包括@NotNull
是允许叠加使用的,即允许在同一个参数属性上添加多个注解!
以上注解均可以配置message
属性,用于指定验证失败的提示文本。
通常:
- 对于必须提交的属性,都会添加
@NotNull
- 对于数值类型的,需要考虑是否添加
@Range
(则不需要使用@Min
和@Max
) - 对于字符串类型,都添加
@Pattern
注解进行验证
6.跨域问题
1.出现原因
因为现在开发大部分都是前后端分离开发,两个作为独立的项目,独立开发与部署。所以前端直接向后端发送异步请求,前端会出现以下错误
Access to XMLHttpRequest at 'http://localhost:8080/admins/add-new' from origin 'http://localhost:8081' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这种问题被称为跨域问题,关键字是CORS
2.解决方法
1.配置配置类
基于SpringMVC项目框架中,需要一个SpringMVC的配置类(实现了WebMvcConfigurer接口的类),并重新方法,以允许指定符合条件的进行跨域
package cn.tedu.boot.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
7.关于客户端提交请求参数的格式
1.直接通过&
拼接各参数与值
例如:
// username=root&password=123456&nickname=jackson&phone=13800138001&email=jackson@baidu.com&description=none
let data = 'username=' + this.ruleForm.username
+ '&password=' + this.ruleForm.password
+ '&nickname=' + this.ruleForm.nickname
+ '&phone=' + this.ruleForm.phone
+ '&email=' + this.ruleForm.email
+ '&description=' + this.ruleForm.description;
2.使用JSON语法来组织各参数与值
例如:
let data = {
'username': this.ruleForm.username, // 'root'
'password': this.ruleForm.password, // '123456'
'nickname': this.ruleForm.nickname, // 'jackson'
'phone': this.ruleForm.phone, // '13800138001'
'email': this.ruleForm.email, // 'jackson@baidu.com'
'description': this.ruleForm.description // 'none'
};
3.使用场景
具体使用哪种做法,取决于服务器端的设计:
- 如果服务器端处理请求的方法中,在参数前添加了
@RequestBody
,则允许使用以上第2种做法(JSON数据)提交请求参数,不允许使用以上第1种做法(使用&
拼接) - 如果没有使用
@RequestBody
,则只能使用以上第1种做法
8.处理登录
1.开发流程
1.先整理出当前项目涉及的数据的类型
- 例如:电商类包含用户、商品、购物车、订单等
2.再列举各种数据类型涉及的数据操作
- 例如:用户类型涉及注册、登陆等
3.再挑选相对简单的数据类型先处理
- 简单的易于实现,且可以积累经验
4.在各类数据涉及到的数据操作中,大致遵循增、查、删、改的开发顺序
- 只有先增,还可能查、删、改
- 只有查了以后,才能明确有哪些数据,才便于实现删、改
- 删和改相比,删一般更加简单,所以先开发删,再开发改
5.在开发具体的数据操作时,应该大致遵循持久层 >> 业务逻辑层 >> 控制器层 >> 前端页面的开发顺序
2.管理员登陆-持久层
1.创建或配置(第一次开发)
- 配置(Spring Boot项目)
- 使用@MapperScan配置接口所在的根包
- 在配置文件中通过mybatis.mapper-locations配置XML文件的位置
- 处理某种类型数据的持久层访问,需要:
- 创建接口
- 创建XML文件
2.规划需要执行的SQL语句
-
SQL大致语句:
select * from ams_admin where username=?
-
由于不允许使用*号,因此细分为:
select id,username,password,nickname,avatar,is_enable from ams_admin username=?
-
提示:理论上,还应该查出
login_count
,当登录成功后,还应该更新login_count
、gmt_last_login
等数据,此次暂不考虑。
3.在接口中添加抽象方法(含创建必要的VO类)
1.所有查询结果都应该使用VO类
-
不要使用实体类,根据阿里的开发规范,每张数据表中都应该有
id
、gmt_create
、gmt_modified
这3个字段,而gmt_create
、gmt_modified
这2个字段都是用于特殊情况下排查问题的,一般情况下均不会使用,所以,如果使用实体类,必然存在多余的属性,同时,由于不使用星号作为字段列表,则一般也不会查询这2个字段的值,会导致实体类对象中永远至少存在2个属性为null
。 -
根据以上提示,以前已经写好的
getByUsername()
是不规范的,应该调整已存在此方法,本次并不需要添加新的抽象方法。 -
创建
cn.tedu.boot.demo.pojo.vo.AdminSimpleVO
类,添加此次查询时需要的属性:package cn.tedu.boot.demo.pojo.vo; @Data public class AdminSimpleVO implements Serializable { private Long id; private String username; private String password; private String nickname; private String avatar; private Integer isEnable; }
2.修改AdminMapper接口文件
- 将Admin getByUsername(String username)改为
AdminSimpleVO getByUsername(String username);
- 因为修改了源代码,所以调用了原方法的代码都会出现错误,包括:
- 测试
- 业务逻辑层的实现类
- 因及时修改,但是由于未完成SQL配置,相关代码暂时不能运行
4.在XML中的配置SQL
1.调整AdminMapper.xml
-
删除
<sql>
中不必查询的字段,注意:此字段不要有多余的逗号 -
修改
<resultMap>
节点的type属性值 -
在
<resultMap>
节点下,删除不必要的配置<select id="getByUsername" resultMap="BaseResultMap"> select <include refid="BaseQueryFields" /> from ams_admin where username=#{username} </select> <sql id="BaseQueryFields"> <if test="true"> id, username, password, nickname, avatar, is_enable </if> </sql> <resultMap id="BaseResultMap" type="cn.tedu.boot.demo.pojo.vo.AdminSimpleVO"> <id column="id" property="id" /> <result column="username" property="username" /> <result column="password" property="password" /> <result column="nickname" property="nickname" /> <result column="avatar" property="avatar" /> <result column="is_enable" property="isEnable" /> </resultMap>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗