常用工具/框架
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可以利用添加注解,来快速生成接口文档,以及进行接口测试。
-
引入依赖:
<!-- Spring Boot 集成 swagger --> <dependency> <groupId>com.spring4all</groupId> <artifactId>swagger-spring-boot-starter</artifactId> <version>1.9.0.RELEASE</version> </dependency>
-
配置文件:
swagger: title: "xxx内容管理系统" description: "内容管理系统对课程相关信息进行管理" base-package: com.zzc.content # 要进行文档生成的包的地址 enabled: true version: 1.0.0
-
在要进行生成的类上,加上@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; } }
-
访问地址:http://a.b.c.d:xxxx/…/swagger-ui.html,如:
http://localhost:63040/content/swagger-ui.html
一些常用的swagger注解:
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数描述
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@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请求
-
没有动态参数的请求:
// 被请求的接口 @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系列'}
-
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); }
-
接口返回值为泛型的请求:
@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); }
-
下载小文件,直接用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);
-
需要传递请求头:
@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 配合对象来接收。 |
-
普通表单请求: 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); }
-
上传本地文件: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); }
-
发送 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:
-
引入 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网页。
- 浏览器请求web服务器
- 服务器渲染页面,渲染的过程就是向 jsp页面(模板)内填充数据(模型)。
- 服务器将渲染生成的页面返回给浏览器。
常用的java模板引擎有:Jsp、Freemarker、Thymeleaf 、Velocity 等
Freemarker
使用:
-
添加依赖:
<!-- Spring Boot 对结果视图 Freemarker 集成 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
-
配置文件:(页面路径为 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文件夹下的资源)
-
编写模板文件:如test.ftl模板文件,后缀为 .ftl
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello World!</title> </head> <body> Hello ${name}! </body> </html>
-
编写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的执行流程:
- 用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
- 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
- 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
- SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
- 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List<AuthenticationProvider>列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。
使用
-
添加依赖:
<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>
-
加入配置类:
@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为用户的身份信息,这里有两个思路:
- 扩展UserDetails,使之包括更多的自定义属性,
- 扩展username的内容 ,比如存入json数据内容作为username的内容。
相比较而言,方案二比较简单还不用破坏UserDetails的结构,上述代码就是拓展了username,即username属性放入的是user信息的json字符串。
-
在对应资源上添加访问所需权限
@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解析出用户信息。这个过程就是无状态认证。
-
优点:
- jwt基于json,非常方便解析。
- 可以在令牌中自定义丰富的内容,易扩展
- 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
- 资源服务使用JWT可不依赖认证服务即可完成授权。
缺点:
- 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提供 授权码模式、密码模式、简化模式、客户端模式等四种授权模式(微信扫码登录是基于授权码模式),这四种模式中授权码模式和密码模式应用较多。
授权码模式使用例子:
-
依赖:
<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>
-
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要求配置以下几个类:
- ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),为了不让随便一个客户端都可以随便接入到它的认证服务,服务提供商会给批准接入的客户端一个身份,用于接入时的凭据,有客户端标识和客户端秘钥,在这里配置批准接入的客户端的详细信息。
- AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
- AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.
- AuthorizationServerConfigurerAdapter要求配置以下几个类:
-
测试:
### 授权码模式 ### 第一步申请授权码(浏览器请求)/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认证实现大致为:
- 支持账号和密码认证:采用OAuth2协议的密码模式即可实现。
- 支持手机号加验证码认证:用户认证提交的是手机号和验证码,并不是账号和密码。
- 微信扫码认证: 基于OAuth2协议与微信交互,网站向微信服务器申请到一个令牌,然后携带令牌去微信查询用户信息,查询成功则用户在学成在线项目认证通过。
由于不同的认证方式提交的数据不一样,比如:手机加验证码方式会提交手机号和验证码,账号密码方式会提交账号、密码、验证码。 对此我们可以在UserDetailsService实现类的loadUserByUsername()方法上作文章,将用户原来提交的账号数据改为提交json数据,json数据可以扩展不同认证方式所提交的各种参数。
实现:
-
创建一个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 }
-
定义统一的认证接口,不同的认证方式处理要实现该接口
/** * @description 统一的认证接口 */ public interface AuthService { /** * @param authParamsDto 认证参数 * @return model.po.XcUser 用户信息 * @description 认证方法 */ XcUserExt execute(AuthParamsDto authParamsDto); }
-
修改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; } }
-
原来的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); }
-
具体的校验处理逻辑,如以账号密码为例:
/** * @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:*
解决跨域的方法:
-
JSONP
通过script标签的src属性进行跨域请求,如果服务端要响应内容则首先读取请求参数callback的值,callback是一个回调函数的名称,服务端读取callback的值后将响应内容通过调用callback函数的方式告诉请求方。如下图:
-
添加响应头
服务端在响应头添加 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); } }
-
通过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=)
使用:
-
添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
-
直接使用注解定义校验规则,并在方法对应参数上添加**@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分组。
使用:
-
定义不同的接口类型(空接口)表示不同的分组
/** * 校验分组 */ public class ValidationGroups { public interface Inster{}; public interface Update{}; public interface Delete{}; }
-
在定义校验规则时,指定分组:groups = {xxx.class}
@NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空") @NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空") @ApiModelProperty(value = "课程名称", required = true) private String name;
-
在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权限模型,数据库表包含:
- 用户表:每个用户都有唯一的UID识别,并被授予不同的角色
- 角色表:不同角色具有不同的权限
- 权限表
- 用户-角色关联表
- 角色-权限关联表