queryString格式就像是我们给网站地址“附加”一些额外的信息。比如,我们想在搜索引擎里搜索“苹果”,我们可能会输入http://search.com/?q=苹果。这里的?q=苹果就是queryString,它告诉我们搜索引擎要搜索的关键字是“苹果”。这种方式很适合传递少量、简单的信息,并且通常用于GET请求。
x-www-form-urlencoded格式则更像是我们把一个完整的表单数据“打包”起来发送给服务器。
两者区别在于前者存放在url路径后面,后者存放在请求体里面。
登录认证除了登录接口,其他接口在用户访问前都必须对用户的登录状态进行检查(作用:保护数据,防止数据泄露),这个检查过程就叫作登录认证。
令牌技术令牌技术就是用来实现登录认证的,浏览器访问登录接口,用户登录成功后,就会生成一个令牌响应给浏览器,浏览器拿到令牌后,访问其他接口时都必须拿上这个令牌,其他接口在用户访问前都会检查该令牌,令牌有效,则允许访问。
令牌的要求令牌技术需要的令牌是要满足一定要求的。
令牌其实就是一个字符串。 令牌必须能承担一定的业务数据,如令牌上可以存放具体用户的用户信息(用户要访问的目标接口如果需要,能直接使用该用户的个人信息,减少查询数据库的次数,提高效率)等。 令牌必须要防止篡改,保证其中的信息合法和安全。
具体要求如下图:
JWT令牌规范JWT 规范是Web开发中最常用的一种令牌规范,具体介绍如下。
如上图,JWT 令牌分为三部分组成,每个部分都是由对应的JSON格式数据通过Base64编码方式(国际通用编码方式)编码过的。
第一个部分(头) "alg"是该令牌使用的数据加密算法的类型,"type"是该令牌的规范的类型。第二部分(有效载荷) 用于携带一些用户自定义信息(为了防止泄露,此处不建议携带私密数据)。第三部分(签名)根据该令牌前两个部分的内容,利用加密算法(就是头部记录的那个算法)进行数据加密。浏览器接口就是通过签名来判断前面两个部分的数据是否被篡改的。令牌示例使用该类前提是引入依赖:
<!--java JWT令牌规范 起步依赖--><dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version></dependency>import com.auth0.jwt.JWT;import com.auth0.jwt.JWTVerifier;import com.auth0.jwt.algorithms.Algorithm;import com.auth0.jwt.interfaces.Claim;import com.auth0.jwt.interfaces.DecodedJWT;import org.junit.jupiter.api.Test;import java.util.Date;import java.util.HashMap;import java.util.Map;public JwtTest { //获取token public void testGen(){ //创建了一个集合,Object 是所有类的根基,这意味着该集合中的值可以 //是任意类型 Map<String,Object>claim =new HashMap<>(); //为claim集合插入数据 claim.put("id",1); claim.put("username","张三"); //生成JWT 的代码 String token= JWT.create() .withClaim("user",claim) //添加负荷 withClaim(键,值) .withExpiresAt(new Date(System.currentTimeMillis()+1000*60*60*12)) //添加过期时间,即JWT失效时间(这里是12小时) .sign(Algorithm.HMAC256("it")); . //指定加密算法,配置密钥(密钥是自定义的) //密钥(Key)主要用于加密和解密数据//new Date()是用来创建一个新的 Date 对象,该对象表示当前日期和时间// (自1970年1月1日00:00:00 GMT以来的毫秒数,然后可能跟随一个特定的时区表示)//System.currentTimeMillis() 获得当前毫秒值(与new Date()效果相同)//上面代码外面套了个new Date()是因为withExpiresAt()函数要求参数必须//是一个Date对象 } //验证token public void testP(){ //用户传过来的token String token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + ".eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MTIxMDQxMjR9" + ".i7XygJGq-DwNQOv0L89ZL1NrUSccRARsBaMqzb-yRWY"; JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256("it")) .build(); //获得token的验证器,加密算法要与token的一致 DecodedJWT decodedJWT = jwtVerifier.verify(token); //验证token,生成一个解析后的JWT对象 Map<String, Claim> claims = decodedJWT.getClaims(); //获得解析后的JWT对象的所有载荷 System.out.println(claims.get("user")); }}token令牌生成和校验工具类(JWT令牌规范)使用该类前提是引入依赖:
<!--java JWT令牌规范 起步依赖--><dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version></dependency>使用此类:
import com.auth0.jwt.JWT;import com.auth0.jwt.algorithms.Algorithm;import java.util.Date;import java.util.Map;public JwtUtil { private static final String KEY = "itheima"; //接收业务数据,生成token并返回 public static String genToken(Map<String, Object> claims) { return JWT.create() .withClaim("claims", claims) .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) .sign(Algorithm.HMAC256(KEY)); } //接收token,验证token,并返回业务数据 public static Map<String, Object> parseToken(String token) { return JWT.require(Algorithm.HMAC256(KEY)) .build() .verify(token) .getClaim("claims") .asMap(); }}拦截器定义拦截器(此处用于登录认证)该类是一个拦截器,能拦截前端发出的http请求,进行预处理,再决定放不放行。其中,有两个需要重写的方法,如下:
preHandle() 方法位于 Controller层对应接口的方法调用之前执行,可以用来拦截请求,对请求进行预处理,如验证token令牌等。afterCompletion() 方法用于在整个请求结束之后进行一些清理工作,或者记录一些请求处理完成后的信息。在Controller层对应接口的方法处理完请求后调用。定义拦截器:(该拦截器用来进行登录认证)
import com.example.two_project.utils.JwtUtil;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import java.util.Map;//该类是一个拦截器,能拦截前端发出的http请求,进行预处理,//再决定放不放行//@Component是一个通用注解,可以将标记的类实例作为Bean//注入IOC容器中@Componentpublic LoginInterceptors implements HandlerInterceptor { //HttpServletRequest类的对象表示一个 HTTP 请求 //当你与前端页面交互时,收到了一个Http请求,你 //可以用一个 HttpServletRequest 对象来当作这个http请求 //HttpServletResponse类的对象表示一个 HTTP 响应 //当你与前端页面交互时,收到了一个Http请求,你 //可以用一个 HttpServletResponse 对象来当作这 //个http请求响应 //preHandle() 方法位于 Controller层对应接口的方法调用 //之前执行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //从该http请求中获取token String token = request.getHeader("Authorization"); //验证token try { Map<String, Object> claims = JwtUtil.parseToken(token); //将该请求放行 return true; } catch (Exception e) { //根据接口文档,要求http状态响应码为401 response.setStatus(401); //将该请求不放行 return false; } } //afterCompletion() 方法用于在整个请求结束之后进行一些//清理工作,或者记录一些请求处理完成后的信息。//在Controller层对应接口的方法处理完请求后调用@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //使用完业务数据后,清除数据,防止内存泄露 ThreadLocalUtil.remove(); }}注册拦截器该类用来注册拦截器,即将拦截器添加到系统中,以便系统能识别和使用它。
拦截器注册:
import com.example.two_project.interceptors.LoginInterceptors;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;//该类用来注册拦截器,即将拦截器添加到系统中,以便系统//能识别和使用它@Configurationpublic WebConfig implements WebMvcConfigurer { @Autowired LoginInterceptors loginInterceptors; //获取已定义好的拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptors) .excludePathPatterns("/user/login","/user/register"); //registry是一个注册器, addInterceptor()方法用来 //注册拦截器,注册过后,默认拦截所有请求 //excludePathPatterns()方法设置保护路径,即该路径 //的请求,系统不拦截 }}ThreadLocal(线程局部变量)ThreadLocal 提供线程局部变量,用set()/get()来存取数据,它存储的数据都具有线程隔离,即同一变量,在不同线程中取出来的值是不同的,如下图。
上图,蓝色线程里get()到的就是"萧炎",绿色线程里get()到的就是"药尘"。
应用场景:
当同时有多个用户访问系统时,Tomcat就会为每个用户自动创建一个对应的线程,来执行各自的用户操作。
当系统中多个接口或多个层都要使用同一个变量时,可以直接将该变量设置为一个全局变量,设置一个全局threadlocal变量,既保证所有线程均可调用(普通的全局变量所有的线程均可调用和修改),又保证每个线程拿到的值不同,如下图。
ThreadLocal工具类(线程局部变量)该类直接拿去使用即可。
import java.util.HashMap;import java.util.Map;/** * ThreadLocal 工具类 */@SuppressWarnings("all")public ThreadLocalUtil { //提供ThreadLocal对象, private static final ThreadLocal THREAD_LOCAL = new ThreadLocal(); //根据键获取值 public static <T> T get(){ return (T) THREAD_LOCAL.get(); } //存储键值对 public static void set(Object value){ THREAD_LOCAL.set(value); } //清除ThreadLocal 防止内存泄漏 public static void remove(){ THREAD_LOCAL.remove(); }}密码加密工具类(md5加密)该类直接拿去使用即可。
通过该类的静态方法 getMD5String(String password) :传入普通密码(String类型)来获得加密后的新密码(String类型),无需解密。通过该类的静态方法checkPassword(String password, String md5PwdStr)来判断用户输入的普通密码与存放在数据库中的加密后的新密码是否相同,返回值为Boolean类型。import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public Md5Util { /** * 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合 */ protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; protected static MessageDigest messagedigest = null; static { try { messagedigest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException nsaex) { System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。"); nsaex.printStackTrace(); } } /** * 生成字符串的md5校验值 * * @param s * @return */ public static String getMD5String(String s) { return getMD5String(s.getBytes()); } /** * 判断字符串的md5校验码是否与一个已知的md5码相匹配 * * @param password 要校验的字符串 * @param md5PwdStr 已知的md5校验码 * @return */ public static boolean checkPassword(String password, String md5PwdStr) { String s = getMD5String(password); return s.equals(md5PwdStr); } public static String getMD5String(byte[] bytes) { messagedigest.update(bytes); return bufferToHex(messagedigest.digest()); } private static String bufferToHex(byte bytes[]) { return bufferToHex(bytes, 0, bytes.length); } private static String bufferToHex(byte bytes[], int m, int n) { StringBuffer stringbuffer = new StringBuffer(2 * n); int k = m + n; for (int l = m; l < k; l++) { appendHexPair(bytes[l], stringbuffer); } return stringbuffer.toString(); } private static void appendHexPair(byte bt, StringBuffer stringbuffer) { char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>> // 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同 char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换 stringbuffer.append(c0); stringbuffer.append(c1); }}前端统一响应数据类不同项目,要求不同
要求:
统一的响应数据类:
//该类用于统一接收要返回给前端页面的数据,系统会自动将该类型转换成JSON格式//统一响应结果//下面两个注解是lombok中的,能在在编译阶段,自动为实体类生成对应的无参构造方法//和有参构造方法@NoArgsConstructor@AllArgsConstructorpublic Result<T> { //java泛型类,T 可以是任何类型 private Integer code;//业务状态码 0-成功 1-失败 private String message;//提示信息 private T data;//响应数据 //快速返回操作成功响应结果(带响应数据) public static <E> Result<E> success(E data) { return new Result<>(0, "操作成功", data); } //第一个<E> 是类型参数声明,它告诉编译器这个方法里面有一个类型参数 E, //Result<E>表示该方法的返回值是一个叫Result的泛型类 //快速返回操作成功响应结果(无响应数据) public static Result success() { return new Result(0, "操作成功", null); } public static Result error(String message) { return new Result(1, message, null); }}上面类叫泛型类,类似C++中的模板类。
lombok 依赖lombok 作用:在编译阶段,自动为实体类生成对应的基本方法,如setXXX(),getXXX()等。
使用条件:
在pom.xml文件引入依赖在实体类上添加注解@Data在pom.xml引入依赖:
<!-- lombok依赖 --><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId></dependency>在类上添加注解@Data
@Datapublic User { private Integer id;//主键ID private String username;//用户名 private String password;//密码 private String nickname;//昵称 private String email;//邮箱 private String userPic;//用户头像地址 private LocalDateTime createTime;//创建时间 private LocalDateTime updateTime;//更新时间}Spring Validation依赖(参数校验)常规校验如图,该依赖是由Spring提供,用于对指定参数进行合法性校验(合法的条件是自定义的)。
安装依赖<!-- Spring Validation起步依赖 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId></dependency>在要进行校验的参数前加上注解@Pattern(regexp="");参数regexp接收的是一个正则表达式,即对参数合法性的判断3. 在Controller类上添加注解@Validated,如下图
若参数合法,程序照常进行,若参数不合法,则程序会向前端抛出异常。这时就需要结合注解@RestControllerAdvice和注解@ExceptionHandler(异常类类名.class)来共同处理该异常,保证该异常返回给前端时,能符合前端要求的数据格式。
4.参数校验相关的其他注解:
注解@NotNull该注解标记的变量的值不能为null。注解@NotEmpty该注解标记的变量的值不能为null,且如果是字符串的话,不能为空字符串。注解@Email该注解标记的变量必须满足邮箱格式上面这三个注解是放在实体类中的,当实体类作为方法参数时,外部有注解@Validated标记且参数本身又是请求体的映射(@RequestBody)时,其内部的上面这三种注解才会生效,这只对实体类内部Validation依赖相关的注解有效。
分组校验在满足常规校验的前提下,当我们开发多个功能接口时,可能需要对实体类内部的不同字段进行校验,但是实体类只有一个,因此校验之间可能会产生冲突(功能1需要校验此字段,功能2不需要),为了解决这种情况,我们就要用到分组校验,如下图所示。
在实体类中添加接口,想分几组就添加几个接口(接口名自定义);在该实体类的注解标记后面添加上该注解的组名,格式:@注解(groups={接口名.class,接口名.class});有冲突的字段就将它们分到不同组中。外面的方法在进行参数校验时,同样需要在@Validated后面添加对应的组名,以便采用不同的校验规则,格式:@Validated(接口名.class)注意:
//若某个校验项未指定分组,则默认属于Default//分组间是可以继承的,A extends B,那么A中拥有B中所有校验项//public interface AA extends BB{} //分组AA继承自分组BB自定义校验当我们遇见已有的注解不能满足所有的校验需求,此时特殊的情况就需要自定义校验(自定义校验注解)
首先自定义一个注解(上面自定义了一个注解State)。2.进入该自定义的注解的文件中,该文件中的注解类上的四个注解和类当中的三个类方法缺一不可,具体代码如下。
import com.example.two_project.Validation.StateValidation;import jakarta.validation.Constraint;import jakarta.validation.Payload;import jakarta.validation.constraints.NotEmpty;import java.lang.annotation.*;//这是一个名叫State的自定义校验注解@Documented //元注解,让该自定义注解的信息能够包含在第三方软件生成的//java文档中@Constraint( validatedBy = {StateValidation.class} //指定提供校验规则的类)@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})//元注解,标记本自定义注解可以用在什么地方//(如类上,方法的参数上等)(FIELD 表示可以用在属性字段上)@Retention(RetentionPolicy.RUNTIME) //表明该注解会保留到运行阶段public @interface State { //用来提供校验失败的提示信息 String message() default "{状态只能是草稿或者已发布}"; //指定分组 Class<?>[] groups() default {}; //负载,可以获取到本注解的附加信息(不常用) Class<? extends Payload>[] payload() default {};}上面是自定义校验注解State的代码,该代码当中的注解@Constraint需要一个提供校验规则的类,而这个类也需要我们自定义,即校验规则需要自定义
//这是一个自定义的校验规则(已经与上面的@State注解绑定)import com.example.two_project.anno.State;import jakarta.validation.ConstraintValidator;import jakarta.validation.ConstraintValidatorContext;public StateValidation implements ConstraintValidator<State,String> { //ConstraintValidator 接口有两个泛型,第一个泛型表示给 //哪个注解提供校验,第二个泛型表示给什么类型的数据提供校验 //该方法就是来提供校验规则的,如果该方法返回false, //就是校验不通过,返回true,就是校验通过 @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { //参数 s 就是系统自动传入的要进行校验的数据 if(s==null){ return false; } if (s.equals("草稿")||s.equals("已发布")){ //注解标记的变量的值为"草稿"或者"已发布",则校验通过 return true; } return false; }}如上面所示,校验规则需要继承一个接口,并重写其中的方法,该规则必须要通过注解@Constraint来与对应的自定义校验注解绑定才能使用。
使用自定义校验注解:
分页查询分页查询,简单来说,就是将数据库中的大量数据分成多个页面进行展示,以便用户能够更方便地浏览和处理这些数据。其中,一般是围绕这几个参数:数据库表中的总的记录数,当前页数,每页显示的记录数,总页数。
分页查询插件PageHelper<!--PageHelper 依赖 --><dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.6</version></dependency>使用.startPage() 方法,传入当前页数和每页展示的数据条数开启分页查询,如下代码。(分页查询必须在调用Mapper层之前开启!!!)
//开启分页查询(借助Mybatis插件 PageHelper)PageHelper.startPage(pageNum,pageSize);//作用将pageNum(当前页数),pageSize(每页展示的数据条数)自动的//添加到Mapper层中对应的SQL全表查询语句后面(limit …………),//完成分页查询分页查询结束:
//获取到查询结果List<Article> as=articleMapper.list(id,categoryId,state);//Article是该表对应的实体类,list为Mapper层中SQL全表查询语句//映射绑定的方法//Page类是List类的子类,其中拥有子类特有的方法,通过这些方法//可以获取到PageHelper分页查询后,得到的总的记录数和当前页的数据//这就是为什么要将List强转为PagePage<Article> p= (Page<Article>) as;//把数据依次填充到PageBean对象当中pageBean.setTotal(p.getTotal());pageBean.setItems(p.getResult());//pageBean是根据接口文件要求,用来存放查询结果的自定义的实体类分页实体类(统一返回分页查询结果)import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import java.util.List;//分页类,用来分页返回结果对象@Data@NoArgsConstructor@AllArgsConstructorpublic PageBean <T>{ private Long total;//总记录数 private List<T> items;//当前页的数据集合}传统xml文件映射(配置动态的SQL语句)在Mapper接口层,可以直接使用注解来将SQL语句映射绑定到对应方法上,如下:
@Mapperpublic interface CategoryMapper { //新增文章分类 @Insert("insert into big_event.category(category_name, category_alias, create_user, create_time, update_time) " + "value(#{category.categoryName},#{category.categoryAlias},#{category.createUser},now(),now())") public void addCategory(Category category); //通过文章的分类名查找分类 @Select("select * from big_event.category where category_name=#{categoryName}") public Category findByCategoryName(String categoryName); //通过文章的分类ID查找分类 @Select("select * from big_event.category where id=#{id}") public Category findById(Integer id); //通过用户ID获得其创建的所有分类 @Select("select * from big_event.category where create_user=#{userId}") public List<Category> findByCreateUser(Integer userId); }但是,上面却无法映射绑定动态的SQL语句到对应方法上,要想映射动态的sql语句,就只要采用传统的数据库映射方式----xml文件映射。
xml映射文件要求xml配置文件必须放在resources 目录下,在该目录下,它的路径必须与Mapper层接口所在路径一致。
例如Mapper层接口路径:com.example.two_project.mapper,那么对应的xml文件路径就必须是resources/com/example/two_project/mapper
xml文件配置动态SQL语句<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.two_project.mapper.ArticleMapper"><!-- namespace 必须是对应的Mapper层接口的全类名 --> <select id="list" resultType="com.example.two_project.entity.Article"> select * from big_event.article <where> <if test="state!=null"> state=#{state} </if> <if test="categoryId"> and category_id=#{categoryId} </if> and create_user=#{id} </where> </select></mapper><select> 标签就表示Select语句,属性id为该条SQL语句对应的函数的函数名, 属性resultType为数据库查询结果的每一条数据的返回类型(即该表对应的实体类的全类名)
<where> 标签就是SQL语句中的where,与一般的where相比,它的区别是它可以通过if的判断来动态地改变查询条件
<if> 标签中的test参数为判断条件,如果条件满足,则将标签内的语句加入where当中且若这是where中的第一个条件, 那么该条件前面的and将会被省略,若不满足,则不将标签内的语句加入.
文件对应的映射方法中的参数可以直接在标签中使用
xml映射文件的配置<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="">常用函数NOW() :在数据库系统中,NOW() 是一个常用的函数,它用于返回当前的日期和时间,它的格式: 'YYYY-MM-DD HH:MM:SS'(年-月-日 时:分:秒)LocalDateTime.now() 获得当前系统时间,它的格式YYYY-MM-DDTHH:MM:SS.SSSSSSSSS(年-月-日T时:分:秒)(秒保留小数点后九位)作者:freejackman链接:https://juejin.cn/post/7353543714151940135