Java项目常用的工具/解决方法

常用工具/框架

lombok

Maven依赖:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

实习类

  • @Getter@Setter

    生成默认的get、set方法,可以加在类上,或只加在某字段上;

    注:@Getter可用于枚举而@Setter不能, 与@Accessors注解一起使用会产生影响

  • @Accessors

    用于修改getter和setter方法的内容

    功能:

    • fluent: 如果为true,让getter、setter方法的方法名没有前缀。且默认让chain为true。
    • chain:如果为true,则setter方法返回this,而不是void。
    • prefix:匹配字段前缀,如果前缀存在,则去除前缀并创建新的字段名,否则跳过该字段,并生成警告。(前缀后面不能是小写字母,不然不会不会匹配)
  • 构造方法:

    • @AllArgsConstructor:全参数构造方法
    • @RequiredArgsConstructor:带参数构造方法,参数只能是带有@NonNull和final修饰的未经初始化字段
    • @NoArgsConstructor:无参构造方法

    可以配置属性: access = AccessLevel.PRIVATE 设置构造方法为私有

  • @ToString@EqualAndHashCode: 重写toString,equals,hashCode方法

  • Data

    集成了多个注解:

    • @Getter
    • @Setter
    • @NoArgsConstructor
    • @ToString
    • @EqualsAndHashCode

    属性: staticConstructor

  • @Value: 是不可变的@Data,所有字段由private和final修饰,不会产生setter方法,类本身也由final修饰。

  • @Builder@Singular: 自动生成构造者模式代码,Singular是针对集合属性的特殊处理。

    @Builder
    public class User {
        private String name;
        private int age;
    }
    
    User user = User.builder()
                    .name("Tom")
                    .age(20)
                    .build();
    
  • @Slf4j:帮助生成日志记录代码,使用 log.xxx输出日志

  • @NonNull

    • 用在方法的参数上: 在方法/构造函数体的开头插入一个空检查,即在赋值该字段时对参数进行判空检查!

      public class Question {
      
          private String text;
      
          public Question(@NonNull final String text) {
              this.text = text;
          }
      
    • 用在字段上:任何为该字段赋值的生成方法也将生成这些空检查。

      public class Question {
          @NonNull
          private String text;
      }
      

代码块

  • @Cleanup: 自动管理资源,会自动调用close()关闭资源。

    @Cleanup InputStream in = new FileInputStream(args[0]);
    @Cleanup OutputStream out = new FileOutputStream(args[1]);
    byte[] b = new byte[10000];
    while (true) {
        int r = in.read(b);
        if (r == -1) break;
        out.write(b, 0, r);
    }
    // 如果要清理的对象没有close()方法,可以指定一个无参方法来进行自动清理
    @Cleanup("dispose") CoolBar bar = new CoolBar(parent, 0);
    

common-lang3

StringUtils

所有操作都是null安全的。

  • IsEmpty/IsBlank - 检查字符串是否包含文本

    • // 字符串为 ""和null时, 返回true
      boolean isEmpty(String str)
      
    • // 字符串为 " ","",null时, 返回true
      boolean isBlank(String str)
      
  • clean/Trim/Strip - 删除前导和尾随的控制字符(char <= 32)

    • // 如果是null则返回""
      String clean(String str)
      
    • // 允许返回null
      String trim(String str)
      // 如果只有null和空格, 则返回null
      String trimToNull(String str)
      
    • // 前后删除指定字符stripChars中的任意一个, null视为空格
      // 允许返回null
      // 如:StringUtils.strip("  abcyx", "xyz") = "  abc"
      String strip(String str, String stripChars)
      // stripChars为null, 此时效果和trim一样
      String strip(String str)
      String stripToNull(String str)
      // 可以批量操作数组中的字符串
      String[] stripAll(String[] strs, String stripChars) 
      
  • Equals - 比较两个字符串,null安全

    // 允许比较null
    boolean equals(String str1, String str2)
    // 忽略大小写
    boolean equalsIgnoreCase(String str1, String str2) 
    
  • startsWith - 检查字符串是否以前缀开头

  • endsWith - 检查字符串是否以后缀结尾

  • IndexOf/LastIndexOf/Contains - 检索下标位置

    • // 使用Stirng.indexOf方法, 字符串为null或找不到时返回-1
      int indexOf(String str, char/String searchChar, [int startPos])
      // 返回str中找到的第ordinal个searchStr的下标, 否则返回-1
      ordinalIndexOf(String str, String searchStr, int ordinal)
      // 同样有忽略大小写的版本
      int indexOfIgnoreCase(String str, String searchStr)
      
    • // str中是否包含searchChar
      boolean contains(String str, char searchChar)
      // 忽略大小写
      boolean containsIgnoreCase(String str, String searchStr) 
      
  • IndexOfAny/LastIndexOfAny/IndexOfAnyBut/LastIndexOfAnyBut - 检索字符串中是否有给定字符集中的任何一个

    • // searchChars也可以以String形式给出, 会转换为char[]
      int indexOfAny(String str, char[] searchChars)
      int indexOfAny(String str, String searchChars)
      
    • // 查找不在字符集中的任何字符
      int indexOfAnyBut(String str, char[] searchChars)
      int indexOfAnyBut(String str, String searchChars)
      
  • ContainsOnly/ContainsNone/ContainsAny - 字符串是否仅包含/无/任何这些字符

    /** 
    	StringUtils.containsOnly(null, *)       = false
        StringUtils.containsOnly(*, null)       = false
        StringUtils.containsOnly("", *)         = true
        StringUtils.containsOnly("ab", '')      = false
        StringUtils.containsOnly("abab", 'abc') = true
    */
    boolean containsOnly(String str, char[] valid)
    
  • Substring/Left/Right/Mid - 子字符串提取

  • SubstringBefore/SubstringAfter/SubstringBetween - 相对于其他字符串的子字符串提取

    • // 获取第一次出现分隔符之前的子字符串。不返回分隔符。
      String substringBefore(String str, String separator)
      
    • // 获取嵌套在两个字符串之间的字符串。仅返回第一个匹配项。
      // StringUtils.substringBetween("yabczyabcz", "y", "z")   = "abc"
      String substringBetween(String str, String open, String close)
      
  • Split/Join - 将字符串拆分为子字符串数组,反之亦然

    • // 将提供的文本拆分为具有最大长度的数组,并指定分隔符。
      // 分隔符不包含在返回的字符串数组中。相邻的分隔符被视为一个分隔符。
      // 输入 null 字符串返回 null。 null 分隔符字符在空格上拆分
      // 如果找到max个分隔的子字符串,则最后一个返回的字符串包括第一个 max - 1 返回的字符串之后的所有字符(包括分隔符)。
      String[] split(String str, String separatorChars, int max)
      
    • // 拼接数组的元素, 可以选择是否有分隔符
      String join(Object[] array, char separator) 
      
  • Remove/Delete - 删除字符串的一部分

    • // 从源字符串中删除子字符串的所有匹配项
      String remove(String str, String remove) 
      // 仅当位于字符串开头时, 删除此子字符串
      String removeStart(String str, String remove)
      
    • // 删除空格, 空格定义为{' ', '\t', '\r', '\n', '\b'}
      String deleteSpaces(String str)
      // 删除空格, 空格是' '
      String deleteWhitespace(String str) 
      
  • Replace/Overlay - 搜索字符串并用另一个字符串替换一个字符串

    • // 将text中[start, end)的字符串替换成overlay
      String overlayString(String text, String overlay, int start, int end)
      
  • Chomp/Chop - 从字符串末尾删除子字符串,否则将其保留。

    • // 末尾删除一个换行符, 换行符是 “\n”、“”\r 或 “\r\n”。
      String chomp(String str) 
      // 末尾删除separator
      String chomp(String str, String separator)
      
    • // 删除最后一个字符, 如果结尾是"\r\n",都删除
      String chop(String str)
      
  • LeftPad/RightPad/Center/Repeat - 填充字符串

  • UpperCase/LowerCase/SwapCase/Capitalize/Uncapitalize - 更改字符串的大小写

    • // 将第一个字母更改为标题大小写
      String capitalize(String str)
      // 取消大写字符串,将第一个字母更改为标题大小写
      String uncapitalize(String str)
      
    • // 大写字符转换为小写,标题大小写字符转换为小写,小写字符转换为大写
      // StringUtils.swapCase("The dog has a BONE") = "tHE DOG HAS A bone"
      String swapCase(String str) 
      
  • CountMatches - 计算一个字符串在另一个字符串中的出现次数

  • IsAlpha/IsNumeric/IsWhitespace/IsAsciiPrintable - 检查字符串中的字符

    • // 检查字符串是否仅包含 字母
      boolean isAlpha(String str) 
      // 检查字符串是否仅包含 字母和空格 (' ')
      boolean isAlphaSpace(String str)
      // 检查字符串是否仅包含 字母或数字
      boolean isAlphanumeric(String str)
      // 检查字符串是否仅包含 字母、数字或空格 (' ')
      boolean isAlphanumericSpace(String str)
      // 检查字符串是否仅包含 ASCII 可打印字符
      boolean isAsciiPrintable(String str)
      // 检查字符串是否仅包含小写字符
      boolean isAllLowerCase(String str)
      
  • DefaultString - 防止空输入字符串

    // 返回原字符串,如果字符串为 null,则返回空字符串 ("")
    String defaultString(String str)
    
  • Reverse/ReverseDelimited - 反转字符串

    • // 反转由特定字符分隔的字符串, 分隔符之间的字符串不会反转
      // 如分隔符为'.', 则java.lang.String 变为 String.lang.java
      String reverseDelimited(String str, char separatorChar)
      
  • Abbreviate - 使用省略号缩写字符串

    // 如果长度小于maxWidth字符,则str返回它。否则将其缩写为 substring(str, 0, max-3) + "..."
    // 注: maxWidth >= 4
    String abbreviate(String str, int maxWidth)
    
  • Difference - 比较字符串并报告其差异(返回第二个字符串的其余部分,从它与第一个字符串不同的位置开始)

  • getCommonPrefix - 比较数组中的所有字符串,并返回所有字符串通用的初始字符序列

  • getLevensteinDistance - 将一个字符串更改为另一个字符串所需的更改次数

ArrayUtils

这是对数组、基础类型数组(如int[])和包装类数组(Integer[])的操作。
此类优雅地处理 null 输入。数组输入不会引发 null 异常。但是,包含元素的 null Object 数组可能会引发异常。每种方法都记录了其行为。

  • add/addAll:添加元素方法

  • contains:包含方法

  • isSameLength:判断长度相等

  • nullToEmpty:null转换成空

  • subarray:接取数组

  • indexOf:获取索引

  • toPrimitive/toObject/toMap/toArray

  • reverse: 反转方法

  • toString:打印数组方法

  • remove/removeElement/removeAll:删除元素方法

  • isEmpty:判空方法

  • getLength:获取长度长度

  • clone:克隆方法

BooleanUtils

对布尔基元和布尔对象的操作。
此类尝试优雅地处理 null 输入。不会为输入引发 null 异常。

  • toString:转化为字符串
  • toBoolean/toBooleanObject:基本类和封装类转换
  • toInteger:转换为整数
  • compare:比较
  • and:与
  • or:或
  • negate:非
  • xor:异或
  • isTrue:判断真假

ClassUtils

  • hierarchy:获取父类层级
  • getAllSuperclasses:获取父类
  • getAllInterfaces:获取实现接口
  • getSimpleName/getShortCanonicalName/getShortClassName:获取类名
  • getAbbreviatedName:获取类的缩略名
  • getPackageName/getPackageCanonicalName:获取包名
  • isAssignable:判断是否可以转型
  • isInnerClass:判断是否内部类
  • isPrimitiveWrapper/isPrimitiveOrWrapper:判断是否为基础类或包装类
  • toClass:对象转Class对象
  • convertClassesToClassNames:类名和Class类互转

RandomUtils

  • // 生成 [0, Integer.MAX_VALUE) 之间的随机 int 值
    int nextInt()	
    
  • // 生成 [startInclusive,endExclusive) 之间的随机整数,起始值不能小于终止值。
    int nextInt(int startInclusive, int endExclusive)	
    
  • // 生成 [0, Long.MAX_VALUE) 之间的 long 值
    long nextLong()
    
  • // 生成 [startInclusive,endExclusive) 之间的随机 long 值
    long nextLong(long startInclusive, long endExclusive) 
    
  • // 生成 [0, Double.MAX_VALUE) 直接的随机 double 值
    double nextDouble()	
    
  • // 生成 [startInclusive,endExclusive) 之间的随机 double 值
    double nextDouble(double startInclusive, double endInclusive)	
    
  • // 随机生成一个布尔值,true 或者 false
    boolean nextBoolean()
    
  • // 生成指定个数的字节数组,如 nextBytes(10) 生成的字节数组有 10 个 byte 元素	
    byte[] nextBytes(int count)	
    

RandomStringUtils

  • // 创建长度为指定个数(count)的随机字符串,将从所有字符集中选择字符,不含字母和数字
    random(int count)
    
  • // 生成指定个数(count)的随机字符串,字符将从参数指示的字母或数字字符集中选择
    // letters和numbers的布尔取值是指随机生成是否含有字母或数字
    random(int count, boolean letters, boolean numbers)
    
  • // 创建长度为指定个数的随机字符串,从指定的字符集(chars)中选择字符.
    random(int count, char[] chars)
    

ObjectUtils

  • allNotNull(Object… values):检查给定数组中的任何元素值是否都不是 null

  • anyNotNull(Object… values):检查给定数组中的元素是否有不是 null 的值。

  • T defaultIfNull(T object, final T defaultValue): 如果传递的对象是 null,则返回默认值

  • firstNonNull(T… values):返回数组中不是 null 的第一个值

  • isEmpty(Object object):检查对象是否为空

NumberUtils

//从数组中选出最大值
NumberUtils.max(new int[] { 1, 2, 3, 4 });//---4
//判断字符串是否全是整数
NumberUtils.isDigits("153.4");//--false
//判断字符串是否是有效数字
NumberUtils.isNumber("0321.1");//---false  
// 字符串转BigDecimal, BigInteger,Double,Float,Integer,Long
NumberUtils.createXxxx("13324352")
NumberUtils.toXxxx("123", default)	// 转换,转换失败返回默认值default

DateUtils

//日期加n天
DateUtils.addDays(new Date(), n);
//判断是否同一天
DateUtils.isSameDay(date1, date2);
//字符串时间转换为Date
DateUtils.parseDate(str, parsePatterns);

DateFormatUtils

加密解密类

// MD5加密
String encodeStr=DigestUtils.md5Hex(text + key);
// 密钥进行验证
String md5Text = md5(text, key);
if(md5Text.equalsIgnoreCase(md5)){
    System.out.println("MD5验证通过");
    return true;
}

SystemUtils

swagger

swagger可以利用添加注解,来快速生成接口文档,以及进行接口测试。

  1. 引入依赖:

    <!-- Spring Boot 集成 swagger -->
    <dependency>
        <groupId>com.spring4all</groupId>
        <artifactId>swagger-spring-boot-starter</artifactId>
        <version>1.9.0.RELEASE</version>
    </dependency>
    
  2. 配置文件:

    swagger:
      title: "xxx内容管理系统"
      description: "内容管理系统对课程相关信息进行管理"
      base-package: com.zzc.content		# 要进行文档生成的包的地址
      enabled: true
      version: 1.0.0
    
  3. 在要进行生成的类上,加上@EnableSwagger2Doc

    在类上添加@Api,在方法上添加@ApiOperation,可以生成中文标题,如:

    @Api(value = "管理接口", tags = "课程信息管理接口")
    @EnableSwagger2Doc
    @RestController
    public class CourseBaseInfoController {
    
        @ApiOperation("课程查询接口")
        @PostMapping("/course/list")
        public PageResult<CourseBase> list(PageParams pageParams, @RequestBody QueryCourseParamsDto queryCourseParamsDto) {
            return null;
        }
    }
    
  4. 访问地址:http://a.b.c.d:xxxx/…/swagger-ui.html,如:

    http://localhost:63040/content/swagger-ui.html
    

一些常用的swagger注解:

@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数描述
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiResponseHTTP响应其中1个描述
@ApiResponsesHTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:一个请求参数
@ApiImplicitParams:多个请求参数

fastjson

log4j2

guava

MinIO

视频编码

FFmpeg

远程调用RestTemplate

RestTemplate这个类是 Spring 框架提供的一个工具类,用于远程调用HTTP接口(REST风格)。

创建RestTemplate:

@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
    RestTemplate restTemplate = new RestTemplate(factory);
    return restTemplate;
}

@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(5000);
    factory.setConnectTimeout(15000);
    // 设置代理
    //factory.setProxy(null);
    return factory;
}

// 或者
@Bean
RestTemplate restTemplate(){
    return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}

常用方法:

Get请求

  1. 没有动态参数的请求:

    // 被请求的接口
    @GetMapping("/test/get")
    @ResponseBody
    public BookDto get() {
        return new BookDto(1, "Spring系列");
    }
    

    可以使用 getForObject(url, class) 或 getForEntity(url, class)

    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/get";
    
    public void test() {
        // getForObject方法,获取响应体,将其转换为第二个参数指定的类型
        BookDto bookDto = restTemplate.getForObject(url, BookDto.class);
        System.out.println(bookDto);
    }
    //输出: BookDto{id=1, name='Spring系列'}
    
    public void test() {
        //getForEntity方法,返回值为ResponseEntity类型
        // ResponseEntity中包含了响应结果中的所有信息,比如头、状态、body
        ResponseEntity<BookDto> responseEntity = restTemplate.getForEntity(url, BookDto.class);
        //状态码
        System.out.println(responseEntity.getStatusCode());
        //获取头
        System.out.println("头:" + responseEntity.getHeaders());
        //获取body
        BookDto bookDto = responseEntity.getBody();
        System.out.println(bookDto);
    }
    //输出: 
    // 200 OK
    // 头:[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Sat, 02 Oct 2021 07:05:15 GMT", Keep-Alive:"timeout=20", Connection:"keep-alive"]
    // BookDto{id=1, name='Spring系列'}
    
  2. url中含有动态参数:

    @GetMapping("/test/get/{id}/{name}")
    @ResponseBody
    public BookDto get(@PathVariable("id") Integer id, @PathVariable("name") String name) {
        return new BookDto(id, name);
    }
    

    还是 getForObject 或 getForEntity,参数加上map即可。

    RestTemplate restTemplate = new RestTemplate();
    //url中有动态参数
    String url = "http://localhost:8080/chat16/test/get/{id}/{name}";
    Map<String, String> uriVariables = new HashMap<>();
    uriVariables.put("id", "1");
    uriVariables.put("name", "Spring系列");
    
    public void test() {
        //getForObject方法
        BookDto bookDto = restTemplate.getForObject(url, BookDto.class, uriVariables);
        System.out.println(bookDto);
    }
    
    @Test
    public void test() {
        //getForEntity方法
        ResponseEntity<BookDto> responseEntity = restTemplate.getForEntity(url, BookDto.class, uriVariables);
        BookDto bookDto = responseEntity.getBody();
        System.out.println(bookDto);
    }
    
  3. 接口返回值为泛型的请求:

    @GetMapping("/test/getList")
    @ResponseBody
    public List<BookDto> getList() {
        return Arrays.asList(
                new BookDto(1, "Spring系列"),
                new BookDto(2, "SpringMVC系列")
        );
    }
    

    需要使用 exchange 方法,并通过其中的 ParameterizedTypeReference类型的参数来指定泛型类型。

    RestTemplate restTemplate = new RestTemplate();
    //返回值为泛型
    String url = "http://localhost:8080/chat16/test/getList";
    
    public void test() {
        //若返回结果是泛型类型的,需要使用到exchange方法,
        //这个方法中有个参数是ParameterizedTypeReference类型,通过这个参数类指定泛型类型
        ResponseEntity<List<BookDto>> responseEntity = restTemplate.exchange(
            url,
            HttpMethod.GET,
            null,
            new ParameterizedTypeReference<List<BookDto>>() {}
        );
        List<BookDto> bookDtoList = responseEntity.getBody();
        System.out.println(bookDtoList);
    }
    
  4. 下载小文件,直接用byte[]接收,如果文件过大,用字节数据接收会OOM

    ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(url, byte[].class)
    

    下载大文件,需要使用execute方法,方法中有个ResponseExtractor 类型的参数,可以在其中的回调方法拿到响应流,边读边处理,就不会导致内存溢出。

    RestTemplate restTemplate = new RestTemplate();
    String url = "http://localhost:8080/chat16/test/downFile";
    /**
     * 文件比较大的时候,比如好几个G,就不能返回字节数组了,会把内存撑爆,导致OOM
     * 需要这么做:
     * 使用execute方法,这个方法中有个ResponseExtractor类型的参数,
     * restTemplate拿到结果之后,会回调{@link ResponseExtractor#extractData}这个方法,
     * 在这个方法中可以拿到响应流,然后进行处理,这个过程就是变读边处理,不会导致内存溢出
    */
    String result = restTemplate.execute(
        url,
        HttpMethod.GET,
        null,
        new ResponseExtractor<String>() {
            @Override
            public String extractData(ClientHttpResponse response) throws IOException {
                System.out.println("状态:"+response.getStatusCode());
                System.out.println("头:"+response.getHeaders());
                //获取响应体流
                InputStream body = response.getBody();
                //处理响应体流
                String content = IOUtils.toString(body, "UTF-8");
                return content;
            }
        }, new HashMap<>());
    
    System.out.println(result);
    
  5. 需要传递请求头:

    @GetMapping("/test/getAll/{path1}/{path2}")
    @ResponseBody
    public Map<String, Object> getAll(@PathVariable("path1") String path1,
                                      @PathVariable("path2") String path2,
                                      HttpServletRequest request) {
    	....
    }
    

    用到HttpEntity类型的参数

    public void test() {
        RestTemplate restTemplate = new RestTemplate();
        String url = "http://localhost:8080/chat16/test/header";
    
        //1、请求头放在HttpHeaders对象中
        MultiValueMap<String, String> headers = new HttpHeaders();
        headers.add("header-1", "V1");
        headers.add("header-2", "Spring");
        headers.add("header-2", "SpringBoot");
        //2、RequestEntity:请求实体,请求的所有信息都可以放在RequestEntity中,比如body部分、头、请求方式、url等信息
        RequestEntity requestEntity = new RequestEntity(
            null, //body部分数据
            headers, //头
            HttpMethod.GET,//请求方法
            URI.create(url) //地址
        );
        ResponseEntity<Map<String, List<String>>> responseEntity = restTemplate.exchange(
            requestEntity,	// 请求实体
            new ParameterizedTypeReference<Map<String, List<String>>>() {}
        );
        Map<String, List<String>> result = responseEntity.getBody();
        System.out.println(result);
    }
    

POST请求

根据请求头中的Content-Type来指定请求的类型,一般有3种:

Content-Type说明
application/x-www-form-urlencoded页面中普通的 form 表单提交时就是这种类型,表单中的元素会按照名称和值拼接好,然后之间用&连接,格式如:p1=v1&p2=v2&p3=v3然后通过 urlencoded 编码之后丢在 body 中发送
multipart/form-data页面中表单上传文件的时候,用到的就是这种格式
application/json将发送的数据转换为 json 格式,丢在 http 请求的 body 中发送,后端接口通常用@RequestBody 配合对象来接收。
  1. 普通表单请求: application/x-www-form-urlencoded 类型的请求

    @PostMapping("/test/form1")
    @ResponseBody
    public BookDto form1(BookDto bookDto) {
        return bookDto;
    }
    
    public void test() {
        RestTemplate restTemplate = new RestTemplate();
        String url = "http://localhost:8080/chat16/test/form1";
        
        //1、表单信息,需要放在MultiValueMap中,MultiValueMap相当于Map<String,List<String>>
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        //调用add方法填充表单数据(表单名称:值)
        body.add("id","1");
        body.add("name","SpringMVC系列");
        //2、发送请求(url,请求体,返回值需要转换的类型)
        BookDto result = restTemplate.postForObject(url, body, BookDto.class);
        System.out.println(result);
    }
    
  2. 上传本地文件:multipart/form-data 类型的请求

    @PostMapping(value = "/test/form2")
    @ResponseBody
    public Map<String, String> form2(@RequestParam("file1") MultipartFile file1) {
        Map<String, String> fileMetadata = new LinkedHashMap<>();
        fileMetadata.put("文件名", file1.getOriginalFilename());
        fileMetadata.put("文件类型", file1.getContentType());
        fileMetadata.put("文件大小(byte)", String.valueOf(file1.getSize()));
        return fileMetadata;
    }
    

    上传的文件需要包装为org.springframework.core.io.Resource,常用的有 3 中[FileSystemResource、InputStreamResource、ByteArrayResource]

    • 其中InputStreamResource、ByteArrayResource即通过流和字节数组的方式传输文件,使用时需要重写getFilename()和getFilename()方法,否则会上传失败
    @Test
    public void test() {
        RestTemplate restTemplate = new RestTemplate();
        String url = "http://localhost:8080/chat16/test/form2";
        
        //1、表单信息,需要放在MultiValueMap中,MultiValueMap相当于Map<String,List<String>>
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        //调用add方法放入表单元素(表单名称:值)
        //2、文件对应的类型,需要是org.springframework.core.io.Resource类型的,常见的有[FileSystemResource、InputStreamResource、ByteArrayResource]
        body.add("file1", new FileSystemResource(".\\src\\main\\java\\com\\javacode2018\\springmvc\\chat16\\dto\\UserDto.java"));
        //3、头
        HttpHeaders headers = new HttpHeaders();
        headers.add("header1", "v1");
        headers.add("header2", "v2");
        //4、请求实体
        RequestEntity<MultiValueMap<String, Object>> requestEntity = new RequestEntity<>(body, headers, HttpMethod.POST, URI.create(url));
        //5、发送请求(请求实体,返回值需要转换的类型)
        ResponseEntity<Map<String, String>> responseEntity = restTemplate.exchange(
            requestEntity,
            new ParameterizedTypeReference<Map<String, String>>() {}
        );
        Map<String, String> result = responseEntity.getBody();
        System.out.println(result);
    }
    
  3. 发送 json 格式数据:传递 java 对象,即application/json类型的请求

    @PostMapping("/test/form4")
    @ResponseBody
    public BookDto form4(@RequestBody BookDto bookDto) {
        return bookDto;
    }
    
    public void test() {
        RestTemplate restTemplate = new RestTemplate();
        String url = "http://localhost:8080/chat16/test/form4";
        BookDto body = new BookDto(1, "SpringM系列");
        
        BookDto result = restTemplate.postForObject(url, body, BookDto.class);
        System.out.println(result);
    }
    

    如果body是java对象,RestTemplate 默认自动配上 Content-Type=application/json, 如果 body 的值是 json 格式字符串的时候,调用的时候需要在头中明确指定 Content-Type=application/json:

    public void test17() {
        RestTemplate restTemplate = new RestTemplate();
        String url = "http://localhost:8080/chat16/test/form5";
        //1、:请求体为一个json格式的字符串
        String body = "[{\"id\":1,\"name\":\"SpringMVC系列\"},{\"id\":2,\"name\":\"MySQL系列\"}]";
        /**
         * 2、若请求体为json字符串的时候,需要在头中设置Content-Type=application/json;
         * 若body是普通的java类的时候,无需指定这个,RestTemplate默认自动配上Content-Type=application/json
         */
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        //3、请求实体(body,头、请求方式,uri)
        RequestEntity requestEntity = new RequestEntity(body, headers, HttpMethod.POST, URI.create(url));
        
        //4、发送请求(请求实体,返回值需要转换的类型)
        ResponseEntity<List<BookDto>> responseEntity = restTemplate.exchange(
                requestEntity,
                new ParameterizedTypeReference<List<BookDto>>() {
                });
        //5、获取结果
        List<BookDto> result = responseEntity.getBody();
        System.out.println(result);
    }
    

DELETE、PUT、OPTION请求

public void delete(String url, Object... uriVariables);
public void delete(String url, Map<String, ?> uriVariables);
public void delete(URI url);

put和post类似,把类型改为PUT即可

OPTIONS 请求用来探测接口支持哪些 http 方法:

public Set<HttpMethod> optionsForAllow(String url, Object... uriVariables);
public Set<HttpMethod> optionsForAllow(String url, Map<String, ?> uriVariables);
public Set<HttpMethod> optionsForAllow(URI url);

集成HttpClient

RestTemplate 内部默认用的是 jdk 自带的 HttpURLConnection 发送请求的,性能上面并不是太突出。 可以将其替换为 httpclient 或者 okhttp。

以下为集成HttpClient:

  1. 引入 maven 配置

    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.7</version>
    </dependency>
    

    创建 RestTemplate 时指定 HttpClient 配置,代码如下

    public HttpClient httpClient() {
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        try {
            //设置信任ssl访问
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build();
            httpClientBuilder.setSSLContext(sslContext);
            HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;
            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                    // 注册http和https请求
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", sslConnectionSocketFactory).build();
    
            //使用Httpclient连接池的方式配置(推荐),同时支持netty,okHttp以及其他http框架
            PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
            // 最大连接数
            poolingHttpClientConnectionManager.setMaxTotal(1000);
            // 同路由并发数
            poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100);
            //配置连接池
            httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
            // 重试次数
            httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(0, true));
            //设置默认请求头
            List<Header> headers = new ArrayList<>();
            httpClientBuilder.setDefaultHeaders(headers);
            return httpClientBuilder.build();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    public ClientHttpRequestFactory clientHttpRequestFactory() {
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient());
        // 连接超时(毫秒),这里设置10秒
        clientHttpRequestFactory.setConnectTimeout(10 * 1000);
        // 数据读取超时时间(毫秒),这里设置60秒
        clientHttpRequestFactory.setReadTimeout(60 * 1000);
        // 从连接池获取请求连接的超时时间(毫秒),不宜过长,必须设置,比如连接不够用时,时间过长将是灾难性的
        clientHttpRequestFactory.setConnectionRequestTimeout(10 * 1000);
        return clientHttpRequestFactory;
    }
    
    public RestTemplate restTemplate(){
        //创建RestTemplate的时候,指定ClientHttpRequestFactory
        return new RestTemplate(this.clientHttpRequestFactory());
    }
    
    @Test
    public void test18() {
        RestTemplate restTemplate = this.restTemplate();
        String url = "http://localhost:8080/chat16/test/get";
        //getForObject方法,获取响应体,将其转换为第二个参数指定的类型
        BookDto bookDto = restTemplate.getForObject(url, BookDto.class);
        System.out.println(bookDto);
    }
    

集成 okhttp

引入 maven 配置

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.3.1</version>
</dependency>

复制

创建 RestTemplate

new RestTemplate(new OkHttp3ClientHttpRequestFactory());

模板引擎

模板引擎就是:模板+数据=输出,以Jsp为例,Jsp页面就是模板,页面中嵌入的jsp标签就是数据,两者相结合输出html网页。

  1. 浏览器请求web服务器
  2. 服务器渲染页面,渲染的过程就是向 jsp页面(模板)内填充数据(模型)。
  3. 服务器将渲染生成的页面返回给浏览器。

常用的java模板引擎有:Jsp、Freemarker、Thymeleaf 、Velocity 等

Freemarker

使用:

  1. 添加依赖:

    <!-- Spring Boot 对结果视图 Freemarker 集成 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    
  2. 配置文件:(页面路径为 resources下的templates)

    spring:
      freemarker:
        enabled: true
        cache: false   #关闭模板缓存,方便测试
        settings:
          template_update_delay: 0
        suffix: .ftl   #页面模板后缀名
        charset: UTF-8
        template-loader-path: classpath:/templates/   #页面模板位置(默认为 classpath:/templates/)
        resources:
          add-mappings: false   #关闭项目中的静态资源映射(static、resources文件夹下的资源)
    
  3. 编写模板文件:如test.ftl模板文件,后缀为 .ftl

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8">
            <title>Hello World!</title>
        </head>
        <body>
            Hello ${name}!
        </body>
    </html>
    
  4. 编写controller方法,准备模型数据

    @Controller
    public class FreemarkerController {
    
        @GetMapping("/testfreemarker")
        public ModelAndView test(){
            ModelAndView modelAndView = new ModelAndView();
            //设置模型数据
            modelAndView.addObject("name","小明");
            //设置模板名称-会根据视图名称加.ftl找到模板
            modelAndView.setViewName("test");
            return modelAndView;
        }
    }
    

认证

用户授权: 用户认证通过后可以访问系统的资源,但只允许访问自己拥有权限的资源,系统会判断用户是否拥有对应的访问权限。

  • 统一认证:平台的不同身份的用户使用统一的认证入口。
  • 单点登录(SSO,Single Sign On):只需认证一次便可以在多个拥有访问权限的系统中访问; 即在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
  • 第三方认证:

Spring Security

介绍

Spring Security是一个用于身份验证和访问控制的框架。

原理: Spring Security对Web资源的保护是靠Filter实现的,当初始化时,Spring Security会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类。

  • FilterChainProxy是一个代理,真正起作用的是SecurityFilterChain所包含的各个Filter,不过这些过滤器并不直接处理认证和授权,而是由 认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)调用这些过滤器进行处理。

  • Spring Security功能的实现主要是由一系列过滤器链相互配合完成。以下为几个主要的过滤器:

    • SecurityContextPersistenceFilter :这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
    • UsernamePasswordAuthenticationFilter :用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
    • FilterSecurityInterceptor :是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
    • ExceptionTranslationFilter :能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
  • Spring Security的执行流程:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    1. 用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
    2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
    3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
    4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
    5. 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List<AuthenticationProvider>列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。
使用
  1. 添加依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    
  2. 加入配置类:

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            // 密码为明文方式
            return NoOpPasswordEncoder.getInstance();
    		// return new BCryptPasswordEncoder();
        }
    
        // 配置安全拦截机制
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
                    .anyRequest().permitAll()// 其它请求全部放行
                    .and()
                    .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
        }
    
    }
    

    认证服务service,主要用于返回用户信息UserDetails

    // 实现UserDetailsService,在loadUserByUsername方法中返回用户信息UserDetails
    @Service
    public class UserServiceImpl implements UserDetailsService {
    
        @Autowired
        XcUserMapper xcUserMapper;
    
        /**
         * @description 根据账号查询用户信息
         * @param s  账号
         * @return org.springframework.security.core.userdetails.UserDetails
        */
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    
            XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s));
            if(user == null){
                //返回空表示用户不存在
                return null;
            }
    
            //取出数据库存储的正确密码
            String password = user.getPassword();
            //TODO 校验密码
            ....
                
            //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
            String[] authorities = {"p1"};
           //为了安全在令牌中不放密码
            user.setPassword(null);
            
            //将user对象转json
            String userString = JSON.toJSONString(user);
            //创建UserDetails对象
            UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();
    
            return userDetails;
        }
    
    }
    

    UserDetails接口只返回了username、密码等信息,如果需要拓展用户信息

    由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息,这里有两个思路:

    1. 扩展UserDetails,使之包括更多的自定义属性,
    2. 扩展username的内容 ,比如存入json数据内容作为username的内容。

    相比较而言,方案二比较简单还不用破坏UserDetails的结构,上述代码就是拓展了username,即username属性放入的是user信息的json字符串。

  3. 在对应资源上添加访问所需权限

    @RestController
    public class LoginController {
        ....
        @RequestMapping("/r/r1")
        @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
        public String r1(){
          return "访问r1资源";
        }
        
        @RequestMapping("/r/r2")
        @PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
        public String r2(){
          return "访问r2资源";
        }
        ...
    }
    

JWT

客户端访问网站前会先申请到令牌,接下来客户端携带令牌去访问资源,资源服务器会请求认证服务 校验令牌的合法性。 如果每次访问都需要一次远程校验,那么执行效率会降低。这就可以用到JWT格式的令牌了。

JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。

  • JSON Web Token(JWT)是一种使用JSON格式传递数据的网络令牌技术,它是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任,它可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止内容篡改。

  • JWT是无状态认证:

    • 传统的基于session的方式是有状态认证,用户登录成功将用户的身份信息存储在服务端,这样加大了服务端的存储压力,并且这种方式不适合在分布式系统中应用。

      当用户访问应用服务,每个应用服务都会去服务器查看session信息,如果session中没有该用户则说明用户没有登录,此时就会重新认证,而解决这个问题的方法是Session复制。

    JWT令牌可以将用户身份信息存储在令牌中,无需存储session,用户认证通过后认证服务颁发令牌给用户,用户将令牌存储在客户端,去访问应用服务时携带令牌去访问,服务端从jwt解析出用户信息。这个过程就是无状态认证。

优点:

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  4. 资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

  1. JWT令牌较长,占存储空间比较大。

组成: JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

  • Header

    头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA),内容是一个json对象。(使用Base64Url编码,得到的字符串即第一部分)

  • Payload

    负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的信息字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。(使用Base64Url编码,得到的字符串即第二部分)

    此部分不能存放敏感信息,因为这部分可以直接解码还原得到原始内容。

  • Signature

    签名,用于防止jwt内容被篡改。这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明的签名算法进行签名。如:

    // 其中 secret: 签名所使用的密钥。
    HMACSHA256(
        base64UrlEncode(header) + "." + base64UrlEncode(payload), 
        secret
    )
    
    • 为什么 JWT可以防止篡改?

      第三部分使用签名算法对第一部分和第二部分的内容进行签名,常用的签名算法是 HS256,常见的还有md5,sha 等,签名算法需要使用密钥进行签名,密钥不对外公开,并且签名是不可逆的,如果第三方更改了内容那么服务器验证签名就会失败,要想保证验证签名正确必须保证内容、密钥与签名前一致。

OAuth2

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务。现在业界有OAUTH的多种语言实现,很多大公司也提供了OAUTH认证服务。(如微信扫码认证,这是一种第三方认证的方式,这种认证方式是基于OAuth2协议实现)

  • Spring Security支持OAuth2认证,OAuth2提供 授权码模式、密码模式、简化模式、客户端模式等四种授权模式(微信扫码登录是基于授权码模式),这四种模式中授权码模式和密码模式应用较多。

授权码模式使用例子:

  1. 依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    
  2. security配置略

    令牌配置:

    @Configuration
    public class TokenConfig {
    
        @Autowired
        TokenStore tokenStore;
    
        // 密钥
        private String SIGNING_KEY = "mq123";
    
        @Autowired
        private JwtAccessTokenConverter accessTokenConverter;
    
        // TokenStore是auth2的持久性接口, JwtTokenStore并不实际存储令牌, 而是可以将访问令牌和身份验证相互转换
        @Bean
        public TokenStore tokenStore() {
            return new JwtTokenStore(accessTokenConverter());
        }
    
        @Bean
        public JwtAccessTokenConverter accessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setSigningKey(SIGNING_KEY);
            return converter;
        }
    
        //令牌管理服务
        @Bean(name = "authorizationServerTokenServicesCustom")
        public AuthorizationServerTokenServices tokenService() {
            DefaultTokenServices service = new DefaultTokenServices();
            service.setSupportRefreshToken(true);   //支持刷新令牌
            service.setTokenStore(tokenStore);  //令牌存储策略
            service.setAccessTokenValiditySeconds(7200);    //令牌默认有效期2小时
            service.setRefreshTokenValiditySeconds(259200); //刷新令牌默认有效期3天
    
            TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
            service.setTokenEnhancer(tokenEnhancerChain);
    
            return service;
        }
    
    }
    

    auth2授权配置:

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    
        @Resource(name = "authorizationServerTokenServicesCustom")
        private AuthorizationServerTokenServices authorizationServerTokenServices;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        //客户端详情服务
        @Override
        public void configure(ClientDetailsServiceConfigurer clients)
                throws Exception {
            clients
                    .inMemory()  // 使用in-memory存储
                    .withClient("XcWebApp") // client_id
                    .secret(new BCryptPasswordEncoder().encode("XcWebApp"))  //客户端密钥
                    .resourceIds("xuecheng-plus")   //资源列表
                    // 该client允许的授权类型有authorization_code,password,refresh_token,implicit,client_credentials
                    .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                    .scopes("all")      //允许的授权范围
                    .autoApprove(false)     //false表示跳转到授权页面
                    .redirectUris("http://www.51xuecheng.cn/")  //客户端接收授权码的重定向地址
            ;
        }
        
        //令牌端点的访问配置
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
            endpoints
                    .authenticationManager(authenticationManager)  //认证管理器(在security配置类中)
                    .tokenServices(authorizationServerTokenServices)  //令牌管理服务(在令牌配置类中)
                    .allowedTokenEndpointRequestMethods(HttpMethod.POST);
        }
    
        //令牌端点的安全配置
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) {
            security
                    .tokenKeyAccess("permitAll()")          //oauth/token_key是公开
                    .checkTokenAccess("permitAll()")        //oauth/check_token公开
                    .allowFormAuthenticationForClients()    //表单认证(申请令牌)
            ;
        }
        
    }
    
    • AuthorizationServerConfigurerAdapter要求配置以下几个类:
      1. ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),为了不让随便一个客户端都可以随便接入到它的认证服务,服务提供商会给批准接入的客户端一个身份,用于接入时的凭据,有客户端标识和客户端秘钥,在这里配置批准接入的客户端的详细信息。
      2. AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
      3. AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.
  3. 测试:

    ### 授权码模式
    ### 第一步申请授权码(浏览器请求)/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn
    ### 第二步申请令牌
    POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=CTvCrB&redirect_uri=http://www.51xuecheng.cn
    

    得到的令牌大致为:

    {
      "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
      "token_type": "bearer",
      "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
      "expires_in": 7199,
      "scope": "all"
    }
    

密码模式:

密码模式相对授权码模式简单,授权码模式需要借助浏览器供用户亲自授权,密码模式不用借助浏览器。 但是也直接将用户敏感信息泄漏给了client,因此这种模式只能用于client是我们自己开发的情况下。

### 密码模式
POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123

同一认证入口

一般网站都会支持多种认证方式:账号密码认证、手机验证码认证、扫码登录等,基于当前研究的Spring Security认证实现大致为:

  1. 支持账号和密码认证:采用OAuth2协议的密码模式即可实现。
  2. 支持手机号加验证码认证:用户认证提交的是手机号和验证码,并不是账号和密码。
  3. 微信扫码认证: 基于OAuth2协议与微信交互,网站向微信服务器申请到一个令牌,然后携带令牌去微信查询用户信息,查询成功则用户在学成在线项目认证通过。

由于不同的认证方式提交的数据不一样,比如:手机加验证码方式会提交手机号和验证码,账号密码方式会提交账号、密码、验证码。 对此我们可以在UserDetailsService实现类的loadUserByUsername()方法上作文章,将用户原来提交的账号数据改为提交json数据,json数据可以扩展不同认证方式所提交的各种参数。

实现:

  1. 创建一个DTO类表示认证的参数

    /**
     * @description 认证用户请求参数
     */
    @Data
    public class AuthParamsDto {
    
        private String username; //用户名
        private String password; //域  用于扩展
        private String cellphone;//手机号
        private String checkcode;//验证码
        private String checkcodekey;//验证码key
        private String authType; // 认证的类型   password:用户名密码模式类型    sms:短信模式类型
        private Map<String, Object> payload = new HashMap<>();//附加数据,作为扩展,不同认证类型可拥有不同的附加数据。如认证类型为短信时包含smsKey : sms:3d21042d054548b08477142bbca95cfa; 所有情况下都包含clientId
    
    }
    
  2. 定义统一的认证接口,不同的认证方式处理要实现该接口

    /**
     * @description 统一的认证接口
     */
    public interface AuthService {
    
        /**
         * @param authParamsDto 认证参数
         * @return model.po.XcUser 用户信息
         * @description 认证方法
         */
        XcUserExt execute(AuthParamsDto authParamsDto);
    
    }
    
  3. 修改loadUserByUsername,返回用户信息前进行统一认证处理

    @Service
    public class UserServiceImpl implements UserDetailsService {
    
        @Autowired
        XcUserMapper xcUserMapper;
    
        @Autowired
        XcMenuMapper xcMenuMapper;
    
        @Autowired
        ApplicationContext applicationContext;
    
        //传入的请求认证的参数就是AuthParamsDto
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            //将传入的json转成AuthParamsDto对象
            AuthParamsDto authParamsDto = null;
            try {
                authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
            } catch (Exception e) {
                throw new RuntimeException("请求认证参数不符合要求");
            }
    
            //认证类型,目前有password,wx, 根据不同的认证类型得到不同的认证接口
            String authType = authParamsDto.getAuthType();
            //根据认证类型从spring容器取出指定的bean
            String beanName = authType + "_authservice";
            AuthService authService = applicationContext.getBean(beanName, AuthService.class);
    
            //调用统一execute方法完成认证
            XcUserExt xcUserExt = authService.execute(authParamsDto);
            
            //封装xcUserExt用户信息为UserDetails并返回
            UserDetails userPrincipal = getUserPrincipal(xcUserExt);
            return userPrincipal;
        }
    
        /**
         * @param xcUser 用户id,主键
         * @return model.po.XcUser 用户信息
         * @description 查询用户信息
         */
        public UserDetails getUserPrincipal(XcUserExt xcUser) {
            String password = xcUser.getPassword();
            //权限
            String[] authorities = {"test"};
            //根据用户id查询用户的权限
            List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(xcUser.getId());
            if (xcMenus.size() > 0) {
                List<String> permissions = new ArrayList<>();
                xcMenus.forEach(m -> {
                    //拿到了用户拥有的权限标识符
                    permissions.add(m.getCode());
                });
                //将permissions转成数组
                authorities = permissions.toArray(new String[0]);
            }
    
            xcUser.setPassword(null);
            //将用户信息转json
            String userJson = JSON.toJSONString(xcUser);
            UserDetails userDetails = User.withUsername(userJson).password(password).authorities(authorities).build();
            return userDetails;
        }
    
    }
    
  4. 原来的DaoAuthenticationProvider会进行密码校验,所以需要重新定义DaoAuthenticationProviderCustom类,重写类的additionalAuthenticationChecks方法。

    // 重写了DaoAuthenticationProvider的校验的密码的方法,因为要统一认证入口,有一些认证方式不需要校验密码
    @Component
    public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
    
        // 被实现了的userDetailsService,进行统一认证处理
        @Autowired
        public void setUserDetailsService(UserDetailsService userDetailsService) {
            super.setUserDetailsService(userDetailsService);
        }
    
        // 取消默认的密码对比
        @Override
        protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
        }
    }
    

    在配置类WebSecurityConfig类指定daoAuthenticationProviderCustom

    @Autowired
    DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProviderCustom);
    }
    
  5. 具体的校验处理逻辑,如以账号密码为例:

    /**
     * @description 账号名密码方式
     */
    @Service("password_authservice")
    public class PasswordAuthServiceImpl implements AuthService {
    
        @Autowired
        XcUserMapper xcUserMapper;
    
        @Autowired
        PasswordEncoder passwordEncoder;
    
        @Autowired
        CheckCodeClient checkCodeClient;
    
        @Override
        public XcUserExt execute(AuthParamsDto authParamsDto) {
            //账号
            String username = authParamsDto.getUsername();
    
            //输入的验证码
            String checkcode = authParamsDto.getCheckcode();
            //验证码对应的key
            String checkcodekey = authParamsDto.getCheckcodekey();
    
            if (StringUtils.isEmpty(checkcode) || StringUtils.isEmpty(checkcodekey)) {
                throw new RuntimeException("请输入的验证码");
            }
    
            // 远程调用验证码服务接口去校验验证码
            Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
            if (verify == null || !verify) {
                throw new RuntimeException("验证码输入错误");
            }
    
            //账号是否存在
            //根据username账号查询数据库
            XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));
    
            //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
            if (xcUser == null) {
                throw new RuntimeException("账号不存在");
            }
    
            //验证密码是否正确
            //如果查到了用户拿到正确的密码
            String passwordDb = xcUser.getPassword();
            //拿到用户输入的密码
            String passwordForm = authParamsDto.getPassword();
            //校验密码
            boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
            if (!matches) {
                throw new RuntimeException("账号或密码错误");
            }
            XcUserExt xcUserExt = new XcUserExt();
            BeanUtils.copyProperties(xcUser, xcUserExt);
    
            return xcUserExt;
        }
    }
    

常见问题

跨域

如果请求的协议、主机或端口不同,会被CORS policy阻止,因为没有Access-Control-Allow-Origin 头信息。CORS全称是 cross origin resource share 表示跨域资源共享。

出现这个问题的原因是基于浏览器的同源策略,去判断是否跨域请求,同源策略是浏览器的一种安全机制,从一个地址请求另一个地址,如果协议、主机、端口三者全部一致则不属于跨域,否则有一个不一致就是跨域请求。

比如:

从http://localhost:8601 到 http://localhost:8602 由于端口不同,是跨域。

从http://192.168.101.10:8601 到 http://192.168.101.11:8601 由于主机不同,是跨域。

从http://192.168.101.10:8601 到 https://192.168.101.10:8601 由于协议不同,是跨域。

  • 注:服务器之间不存在跨域请求。

浏览器判断是跨域请求会在请求头上添加origin,表示这个请求来源哪里。比如:

GET / HTTP/1.1   
Origin: http://localhost: 8601 

服务器收到请求判断这个Origin是否允许跨域,如果允许则在响应头中说明允许该来源的跨域请求,如下:

Access-Control-Allow-Origin: http://localhost:8601  

如果允许任何域名来源的跨域请求,则响应如下:

Access-Control-Allow-Origin:*  

解决跨域的方法:

  1. JSONP

    通过script标签的src属性进行跨域请求,如果服务端要响应内容则首先读取请求参数callback的值,callback是一个回调函数的名称,服务端读取callback的值后将响应内容通过调用callback函数的方式告诉请求方。如下图:

  2. 添加响应头

    服务端在响应头添加 Access-Control-Allow-Origin:*, 设置一个过滤器即可:

    @Configuration
    public class GlobalCorsConfig {
    
        @Bean
        public CorsFilter corsFilter(){
    
            CorsConfiguration config = new CorsConfiguration();
            config.addAllowedOrigin("*");  // 允许跨域调用的白名单
            config.addAllowedHeader("*");  // 放行全部头信息
            config.addAllowedMethod("*");  // 允许所有请求方法跨域调用
            config.setAllowCredentials(true);   // 允许跨域发送cookie
    
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", config); // 允许所有请求都走过滤器
    
            return new CorsFilter(source);
        }
    }
    
  3. 通过nginx代理跨域

    由于服务端之间没有跨域,浏览器通过nginx去访问跨域地址。

JSR303校验

前端请求的参数的校验,在Controller和Service中都需要校验:

  • Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。
  • Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。

其中Contoller中的校验可以写成通用代码,而 JSR-303 是在JavaEE6规范中就定义了的参数校验的规范,它定义了Bean Validation,即对bean属性进行校验。

SpringBoot提供了JSR-303的支持,即spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。

基本校验规则:

  • 空检查
    @Null 验证对象是否为null
    @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
    @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
    @NotEmpty 检查约束元素是否为NULL或者是EMPTY.

  • Booelan检查
    @AssertTrue 验证 Boolean 对象是否为 true
    @AssertFalse 验证 Boolean 对象是否为 false

  • 长度检查
    @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
    @Length(min=, max=) Validates that the annotated string is between min and max included.

  • 日期检查
    @Past 验证 Date 和 Calendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期
    @Future 验证 Date 和 Calendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期
    @Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式,regexp:正则表达式 flags: 指定 Pattern.Flag 的数组,表示正则表达式的相关选项。

  • 数值检查
    建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为”“,Integer为null
    @Min 验证 Number 和 String 对象是否大等于指定的值
    @Max 验证 Number 和 String 对象是否小等于指定的值
    @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
    @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
    @Digits 验证 Number 和 String 的构成是否合法
    @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
    @Range(min=, max=) 被指定的元素必须在合适的范围内
    @Range(min=10000,max=50000,message=”range.bean.wage”)
    @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
    @CreditCardNumber信用卡验证
    @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
    @ScriptAssert(lang= ,script=, alias=)
    @URL(protocol=,host=, port=,regexp=, flags=)

使用:

  1. 添加依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 直接使用注解定义校验规则,并在方法对应参数上添加**@Validated**以开启校验,如:

    @Data
    @ApiModel(value="AddCourseDto", description="新增课程基本信息")
    public class AddCourseDto {
    
        @NotEmpty(message = "课程名称不能为空")
        @ApiModelProperty(value = "课程名称", required = true)
        private String name;
    
        @NotEmpty(message = "适用人群不能为空")
        @Size(message = "适用人群内容过少",min = 10)
        @ApiModelProperty(value = "适用人群", required = true)
        private String users;
    
        @NotEmpty(message = "课程分类不能为空")
        @ApiModelProperty(value = "大分类", required = true)
        private String mt;
    
        @NotEmpty(message = "课程分类不能为空")
        @ApiModelProperty(value = "小分类", required = true)
        private String st;
    	
        ...
    }
    
    @ApiOperation("新增课程基础信息")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody @Validated AddCourseDto addCourseDto){
        //机构id,由于认证系统没有上线暂时硬编码
        Long companyId = 1L;
      return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
    }
    

分组校验:

有时候在同一个属性上设置一个校验规则不能满足要求,比如:订单编号由系统生成,在添加订单时要求订单编号为空,在更新订单时要求订单编写不能为空。

于是可以用分组校验,如:添加订单定义@NULL规则属于insert分组,更新订单定义@NotEmpty规则属于update分组。

使用:

  1. 定义不同的接口类型(空接口)表示不同的分组

    /**
    * 校验分组
    */
    public class ValidationGroups {
        
        public interface Inster{};
        public interface Update{};
        public interface Delete{};
    }
    
  2. 在定义校验规则时,指定分组:groups = {xxx.class}

    @NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空")
    @NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空")
    @ApiModelProperty(value = "课程名称", required = true)
    private String name;
    
  3. 在Controller方法中启动校验规则指定要使用的分组名:

    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto){
        //机构id,由于认证系统没有上线暂时硬编码
        Long companyId = 1L;
      return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
    }
    

自定义校验规则:

RBAC授权

业界通常基于RBAC实现授权。

RBAC分为两种方式:

  • 基于角色的访问控制(Role-Based Access Control)

    • 这种是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,如:

      if(主体.hasRole("总经理角色id")){
      	查询工资
      }
      

      如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是总经理或部门经理”,修改代码如下:

      if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
          查询工资
      }
      
  • 基于资源的访问控制(Resource-Based Access Control)

    • 这种是按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等,如:

      if(主体.hasPermission("查询工资权限标识")){
          查询工资
      }
      

上述可以看出,基于资源的访问控制的可扩展性会好些

RBAC权限模型,数据库表包含:

  1. 用户表:每个用户都有唯一的UID识别,并被授予不同的角色
  2. 角色表:不同角色具有不同的权限
  3. 权限表
  4. 用户-角色关联表
  5. 角色-权限关联表
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值