系列文章目录
此系列文章皆为黑马头条项目的学习笔记
文章目录
一、前期准备
1.1 虚拟机登录信息
登录ip:192.168.200.130
用户名:root
密码:itcast
1.2 docker图形化工具portainer登录信息
访问地址:http://192.168.200.130:9001
用户名:admin
密码:admin123456
1.3 mysql数据库信息
用户名:root
密码:root
1.4 nacos注册配置中心地址
192.168.200.130:8848
1.5 Swagger地址
http://localhost:51801/swagger-ui.html
1.6 knife4j地址
http://localhost:51801/doc.html
1.7 minio地址
http://192.168.200.130:9000
用户名:minio
密码:minio123
二、接口代码编写
2.1 APP登录
2.1.1 加密算法
(1)可逆加密算法-对称加密
文件加密和解密使用相同的密钥,即加密秘钥也可以用作解密密钥
优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
缺点: 没有非对称加密安全。
用途: 一般用于保存用户手机号、身份证等敏感但能解密的信息。
常见的对称加密算法有:AES(号称最安全的加密算法)
、DES、3DES(已经被破解了,不建议使用)、Blowfish、IDEA、RC4、RC5、RC6、HS256
(2)可逆加密算法-非对称加密
两个密钥:公开密钥和私有密钥,公有密钥加密,私有密钥解密,
或者私钥加密,公钥解密。
优点: 非对称加密与对称加密相比,其安全性更好。
缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
用途: 一般用于签名和认证。私钥服务器保存,用来加密,公钥客户拿着用于对于令牌或者签名的解密或者校验使用。
常见的非对称加密算法有:RSA(比较常用)
、DSA(数字签名用)、ECC(移动设备用)、RS256(采用SHA-56的RSA签名)
(3)不可逆加密算法
一旦加密就不能反向解密得到密码原文。
种类: Hash加密算法,散列算法,摘要算法等
用途: 一般用于校验下载文件正确性,一般在网站下载文件都能见到;存储用户敏感信息,如密码、卡号等不可解密的信息。
常见的不可加密的算法有:MD5(已经被破解)、SHA、BCrypt
文件md5生成:http://www.metools.info/other/o21.html
(4)Base64编码
Base64是网络上最常见的用语传输8Bit字节代码的编码方式之一。Base64编码可用于在HTTP环境下传递较长的标识信息。采用Base64编码解码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。注意:Base64只是一种编码方式,不算加密方法。
在线编码工具: https://tool.oschina.net/encrypt?type=3
2.1.2 密码加密的方式选型
(1)手动加密(md5+随机字符串–>“salt”)
md5是不可逆加密,md5相同的密码每次加密都一样,不太安全。在md5的基础上手动加盐(salt)处理,这样每一次的值都不一样。
但是需要把加盐后的密码和盐一起保存到数据库,这样就有一个问题,如果数据库不安全的情况下(盐被盗走了),这样的加密方式还是不行。
不加盐测试代码
// 测试代码
@Test
public void testMd5() {
for (int i = 0; i < 10; i++) {
// 明文
String pwd = "123456";
// 密文
String pwdEncrypt = DigestUtils.md5DigestAsHex(pwd.getBytes());
System.out.println(pwdEncrypt);
}
}
测试结果
发现循环十次md5加密,密文都是一样的。
加盐测试代码
@Test
public void testMd5AndSalt() {
for (int i = 0; i < 10; i++) {
// 明文
String pwd = "123456";
// 用工具类生成随机字符串作为盐
String salt = RandomStringUtils.randomAlphabetic(10);
// 明文和盐做字符串的拼接,生成新的明文密码
String pwdNews = pwd + salt;
// 使用工具类,对新生成的明文加密
String pwdEncrypt = DigestUtils.md5DigestAsHex(pwdNews.getBytes());
System.out.println(salt + "===" + pwdEncrypt);
}
}
测试结果
md5+盐验证密码
①获取登录密码明文
②获取表中的盐
③明文+盐拼接成新的明文
④对拼接成的明文进行md5加密
⑤加密后的密文和数据库中的密文做对比
(2)自动加盐-BCrypt密码加密
BCrypt: 自动加盐,所以也就不用把盐的值存储到数据库中了。
测试代码
@Test
public void testbCrypt() {
for (int i = 0; i < 10; i++) {
// 明文
String pwd = "123456";
// 生成盐 ===> 随机字符串
String salt = BCrypt.gensalt();
// 密文
String pwdEncrypt = BCrypt.hashpw(pwd, salt);
System.out.println(salt +"====="+ pwdEncrypt);
}
}
测试结果
验证密码
①获取登录密码明文
②获取表中密码密文
③使用BCrypt.checkpw(明文,密文)方法来获取boolean类型的result
④对result进行判断
2.1.3 JWT
(1)token认证
基于token的用户认证是一种服务端无状态的认证方式(类似于存在服务端的session,就是一种有状态的认证方式),所谓服务端无状态指的是token本身包含登录用户所有的相关数据,而客户端在认证后的每次请求都会携带token,因此服务器端无需存放token数据。
当用户认证后,服务端生成一个token发给客户端,客户端可以放到cookie或localStorage等存储中,每次请求时带上token,服务端收到token通过验证即可确认用户身份。
(2)什么是JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。
JWT令牌结构:
Header.Payload.Signature
①Header:头部包括了JWT的类型,并且还定义了signature,也就是签名部分需要用到的算法
例如:
并且要将上面的json内容,使用Base64Url编码,得到的字符创就是JWT的第一部分
注意:JWT的头中的算法是可逆加密算法,要么对称,要么不对称,不可能出现不可逆算法
②Payload(载荷):载荷,也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳),sub(面相的用户)
等,也可以自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
例如:
这一部分也需要使用Base64Url编码,得到一个字符串也就是jwt的第二部分。
③Singnature
第三部分是签名,此部分用于防止JWT内容被篡改。
这个部分使用base64Url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明签名算法进行签名。
例如:
base64UrlEncode(header):jwt令牌的第一部分
base64UrlEncode(payload):jwt令牌的第二部分
secret:签名所使用的密钥
下图是一个jwt令牌:
(3)生成token
测试代码
@Test
public void testCreateJwt() {
for (int i = 0; i < 10; i++) {
// 第三部分要使用的密钥
String secretKey = "itcast";
String token = Jwts.builder().signWith(SignatureAlgorithm.HS256, secretKey)
// 内置属性 jti,表示唯一id
.setId(UUID.randomUUID().toString())
// 内置属性 iat,表示创建时间
.setIssuedAt(new Date())
// 内置属性 sub,表示面向的对象
.setSubject("all")
// 自定义属性 name age
.claim("name", "zhangsan")
.claim("age", 20)
.compact();
System.out.println(token);
}
}
执行结果
发现生成的token是变化的,这是因为,设置了变化的id和日期,将设置id和日期的代码注释掉,就会发现,生成的token就是一样的了。
(4)解析JWT令牌(token)
测试代码
@Test
public void testParseJwt() {
String secretKey = "itcast";
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4M2MxNmE5My02ODI0LTQyMzQtODNjNy1kNDY1YTk0YmQ3MmEiLCJpYXQiOjE3MDcxNTQ5MTIsInN1YiI6ImFsbCIsIm5hbWUiOiJ6aGFuZ3NhbiIsImFnZSI6MjB9.4q0JaYCIoszUqgBaQKgmcoL9id8zP2RH-fpbHnELW7c";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
System.out.println(claimsJws.getHeader()); // 获取头
System.out.println(claimsJws.getSignature()); // 获取签名
System.out.println(claimsJws.getBody()); // 获取载荷
}
测试结果
如果token被篡改,或者token过期了,在解析的时候,会报SignatureException(签名异常)和ExpiredJwtException(过期异常)
(5)JWT工具类的使用
heima-leadnews-utils服务下的AppJwtUtil类。
因为在工具类中设置了压缩方式,所以载荷在网站解码的时候解不了了,只能通过parse来解析。
// 测试生成token
// 参数是用户id
String token = AppJwtUtil.getToken(1L);
// 测试解析token
String token2 = "里面是token字符串";
Claims claimsBody = AppJwtUtil.getClaimsBody(token2);
// 0:表示无效
// 1:表示有效,且在有效期范围内
int result = AppJwtUtil.verifyToken(claimsBody);
(6)JWT和token的区别
JWT是Token无状态认证技术的具体实现规范
- JWT组成:Header、Payload、Signature
- JWT场景:登录成功生成Token、访问西戎携带Token服务端验证Token合法性
- JWT解析异常:Token被篡改、Token过期了
2.1.4 Bcrypt加密算法实现用户登录验证
(1)流程图
(2)接口设计
(3)代码实现
controller层
@RestController
@RequestMapping("api/v1/login")
public class ApUserLoginController {
@Autowired
private ApUserService apUserService;
@PostMapping("login_auth")
public ResponseResult login(@RequestBody LoginDto dto) {
return apUserService.login(dto);
}
}
service层
定义接口
public interface ApUserService extends IService<ApUser> {
/**
* 用户登录(正常用户或者游客)
* @param dto
* @return
*/
ResponseResult login(LoginDto dto);
}
接口实现类
@Slf4j
@Service
public class ApUserServiceImpl extends ServiceImpl<ApUserMapper, ApUser> implements ApUserService {
@Override
public ResponseResult login(LoginDto dto) {
// 1.如果密码和手机号都不为空的话,那么就是处理普通用户登录
if (StringUtils.isNotBlank(dto.getPassword()) && StringUtils.isNotBlank(dto.getPhone())){
/*// 设置查询条件
LambdaQueryWrapper<ApUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ApUser::getPhone, dto.getPhone());
// 根据条件查询一条数据
ApUser apUser = this.getOne(lambdaQueryWrapper);*/
// 以后就这样写
ApUser apUser = this.getOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getPhone, dto.getPhone()));
// 如果用户为空,则说明手机号填写错误
if (apUser == null) {
// 返回错误信息
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"手机号填写错误");
}
// 获取密码参数
String textPass = dto.getPassword();
// 表中的密码
String pass = apUser.getPassword();
// 使用BCrypt算法校验密码
boolean result = BCrypt.checkpw(textPass, pass);
// 如果密码错误
if (!result) {
// 返回密码错误信息
return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
// 用户名和密码都正确
// 生成token
String token = AppJwtUtil.getToken(apUser.getId().longValue());
// 封装到map集合中
Map map = new HashMap();
// 因为密码信息比较敏感,所以要把用户的密码设置为空
apUser.setPassword("");
map.put("user", apUser);
map.put("token", token);
// 返回
return ResponseResult.okResult(map);
} else {
// 如果手机号或者密码都为空的话,就是游客登录模式
// 直接为游客用户按照id=0L生成token
String token = AppJwtUtil.getToken(0L);
// 把token放入集合中
Map map = new HashMap();
map.put("token", token);
// 返回结果
return ResponseResult.okResult(map);
}
}
}
三、postman测试
四、接口工具-Swagger
Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务(https://swagger.io/)。它的主要作用是:
1.使得前后端分离开发更加方便,有利于团队协作
2.接口文档在线自动生成,降低后端开发人员编写接口文档的负担
3.功能测试
(1)Springboot集成Swagger
引入依赖,在heima-leadnews-model和heima-leadnews-common模块中引入该依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
只需要在heima-leadnews-common中进行配置即可,因为其他微服务工程都直接或间接依赖即可。
在heima-leadnews-common工程中添加一个配置类
package com.heima.common.swagger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2Configuration {
@Bean
public Docket buildDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(buildApiInfo())
.select()
// 要扫描的API(Controller)基础包
.apis(RequestHandlerSelectors.basePackage("com.heima"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo buildApiInfo() {
Contact contact = new Contact("黑马程序员","","");
return new ApiInfoBuilder()
.title("黑马头条-平台管理API文档")
.description("黑马头条后台api")
.contact(contact)
.version("1.0.0").build();
}
}
联调
在heima-leadnews-common模块中的resources目录中新增以下目录和文件
文件:resources/META-INF/Spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.common.swagger.Swagger2Configuration
(2)Swagger常用注解
在Java类中添加Swagger的注解即可生成Swagger接口文档,常用Swagger注解如下:
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数的描述信息
@ApiModel:用对象来接收参数
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:一个请求参数
@ApiImplicitParams:多个请求参数的描述信息
@ApiImplicitParam属性:
五、knife4j
基于Swagger改进优化。
原因:Swagger在线的接口文档没法去导出markdown,pdf这种离线文件。
Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是Swagger-bootstrap-ui。
核心功能: 文档说明、在线调试、个性化配置、离线文档、接口排序、
(1)快速集成
在heima-leadnews-common模块中的pom.xml文件中引入knife4j的依赖,如下:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
创建Swagger配置文件
以上有两个注解需要特别说明,如下表:
六、全局过滤器实现token校验
6.1 校验流程
6.2 代码实现
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 获取响应
ServerHttpResponse response = exchange.getResponse();
// 判断是否是登录接口,是的话,直接放行
String path = request.getURI().getPath();
// 最后的 / 是要加上的,因为我们前端的请求路径是有的
if (path.equals("/user/api/v1/login/login_auth/")){
// 路径匹配上了,放行
return chain.filter(exchange);
}
// 非登录接口,就要看是否携带token(无权访问)
String token = request.getHeaders().getFirst("token");
// token为空,不存在返回401()
if (StringUtils.isBlank(token)){
// 设置状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 表明信息处理完成
return response.setComplete();
}
// token 不为空,要对token进行核实
int result = AppJwtUtil.verifyToken(AppJwtUtil.getClaimsBody(token));
if (result == 0) {
// 响应401,非法请求
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 表明信息处理完成
return response.setComplete();
}
// 放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0; // 过滤器执行优先级,值越小,优先级越高
}
}