D1
视频链接
Day1-05-nacos环境搭建_哔哩哔哩_bilibili
内容介绍
-
搭建微服务开发环境,登录接口包含注册中心和nacos配置中心
服务端用户…微服务。网关负载均衡转发接口请求
实现微服务间互相通信
-
接口测试
-
前后端联调
前置知识
背景介绍
类似今日头条,qq看点
功能架构图
用户前台,内容浏览,登录注册。。。类似于看小说的人
用户后台,内容发布,私信管理。。。类似于小说家
超级管理员后台,对所有作者,其发布内容管理,类似于上帝
效果预览
用户群众
用户,吃瓜的——app用户端
自媒体人,卖瓜的——自媒体系统
管理员,城管——admin管理系统
技术选型
基础层负责前端nginx转发和cdn资源存储加快访问速度,前端框架组件及其第三方插件echarts快速开发
服务层也就是后端首先cloudGateway网关转发请求到各个服务接口,例如登录接口。。
同时还有数据层,持久化数据,mysql。mongodb。hbase。
中间件kafka,redis缓存
搜索索引elasticsearch,oss对象存储
收获
技术运用
解决方案,分布式事务,任务调度,延迟队列,异步线程,热数据,评论系统,关注点赞
软硬编程思想,数据库设计,代码编写,测试用例,部署
课程大纲
环境搭建
列表查看
热点计算(大数据)
cms前后台管理系统发布和审核(队列)
部署,迁移数据
实战(app端文章行为,评论系统,自媒体端评论管理,报表)
环境搭建
nacos环境搭建
-
安装一台虚拟机centos
-
docker安装nacos
镜像拉取
docker pull nacos/nacos-server:1.2.0
创建容器
docker run --env MODE=standalone --name nacos --restart=always -d -p 8848:8848 nacos/nacos-server:1.2.0
//以下是国内无法拉取镜像的解决方案,具体请看往期文章
docker run --env MODE=standalone --name nacos --restart=always -d -p 8848:8848 registry.cn-heyuan.aliyuncs.com/zwww/nacos-server:1.2.0
单机模式,名字,开机重启,守护式进程,端口号,容器镜像
访问地址 192.168.233.136:8848/nacos
初始工程搭建
maven仓库我是用的之前自己配置的
文件编码全设置为utf-8
多模块开发——模块讲解
common——所有服务都用到的,比如全局异常处理
feign-api——对外接口类似controller
gateway——网关转发
model——实体类
service——微服务
test——测试用例
utils——全局工具包
异常类
分为两种,
一种是已知的异常,如参数缺失,返回值自行DIY,
一种是未知异常系统异常,返回值固定
是的,你的理解是正确的。
@Controller
和@ControllerAdvice
之间的工作流程可以总结如下:@ControllerAdvice会将异常丢给前端需要(自定义异常实体类,异常捕获且进行返回或是log.info打印的类)
工作流程
- 请求接收:
- 控制器(使用
@Controller
注解的类)负责接收 HTTP 请求并处理业务逻辑。
- 异常抛出:
- 在处理请求的过程中,如果发生异常(如自定义异常或系统异常),控制器会抛出该异常。
- 异常捕获:
- 被抛出的异常会被 Spring 的异常处理机制捕获。如果该异常没有在控制器内部处理,它会被匹配到对应的
@ExceptionHandler
方法。- 这里,
@ControllerAdvice
注解的类会起作用,检查是否有适用的异常处理方法。
- 返回响应:
- 在
@ExceptionHandler
方法中,你可以定义如何处理异常,并返回一个响应实体(例如ResponseEntity
)。- 这个响应会作为控制器的返回值返回给前端,包含相应的状态码和错误信息。
初始化文件
当微服务引入了common依赖,当微服务初始容器时找到该文件并进行初始化,此时微服务可以使用该全局异常处理器
APP登录
需求分析
登陆前只能看
登陆后能点赞评论关注
表结构
导入sql脚本
CREATE TABLE `ap_user` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`salt` VARCHAR(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码、通信等加密盐',
`name` VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户名',
`password` VARCHAR(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码,md5加密',
`phone` VARCHAR(11) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
`image` VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像',
`sex` TINYINT(1) UNSIGNED DEFAULT NULL COMMENT '0 男\r\n 1 女\r\n 2 未知',
`is_certification` TINYINT(1) UNSIGNED DEFAULT NULL COMMENT '0 未\r\n 1 是',
`is_identity_authentication` TINYINT(1) DEFAULT NULL COMMENT '是否身份认证',
`status` TINYINT(1) UNSIGNED DEFAULT NULL COMMENT '0正常\r\n 1锁定',
`flag` TINYINT(1) UNSIGNED DEFAULT NULL COMMENT '0 普通用户\r\n 1 自媒体人\r\n 2 大V',
`created_time` DATETIME DEFAULT NULL COMMENT '注册时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='APP用户信息表';
实体类
package com.heima.model.user.pojos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* APP用户信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("ap_user")
public class ApUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 密码、通信等加密盐
*/
@TableField("salt")
private String salt;
/**
* 用户名
*/
@TableField("name")
private String name;
/**
* 密码,md5加密
*/
@TableField("password")
private String password;
/**
* 手机号
*/
@TableField("phone")
private String phone;
/**
* 头像
*/
@TableField("image")
private String image;
/**
* 0 男
1 女
2 未知
*/
@TableField("sex")
private Boolean sex;
/**
* 0 未
1 是
*/
@TableField("is_certification")
private Boolean certification;
/**
* 是否身份认证
*/
@TableField("is_identity_authentication")
private Boolean identityAuthentication;
/**
* 0正常
1锁定
*/
@TableField("status")
private Boolean status;
/**
* 0 普通用户
1 自媒体人
2 大V
*/
@TableField("flag")
private Short flag;
/**
* 注册时间
*/
@TableField("created_time")
private Date createdTime;
}
手动加密(md5+加盐随机字符串)
md5每次加密后都是一样的 会被别人试出来正确密码,因此我们每次MD5加生成一个随机字符串一起加密,这样即使是相同密码的md5加密出来的值也是不一样的
流程为,登陆时将输入的密码和盐字符串进行加密 比对数据库一开始加密后的密码
用户端微服务搭建
service模块
<dependencies>
<!-- 引入依赖模块 -->
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-leadnews-model</artifactId>
</dependency>
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-leadnews-common</artifactId>
</dependency>
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-leadnews-feign-api</artifactId>
</dependency>
<!-- Spring boot starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
新建用户子模块
新建引导类
先搞个user包出来,在user包下粘贴以下代码
package com.heima.user;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.heima.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class,args);
}
}
文件+包初始化
bootstrap.yml
引导配置文件,相当于application.yml
ip改成你虚拟机的地址
server:
port: 51801
spring:
application:
name: leadnews-user
cloud:
nacos:
discovery:
server-addr: 192.168.233.136:8848
config:
server-addr: 192.168.233.136:8848
file-extension: yml
在nacos中创建配置文件
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.user.pojos
这里的mybatis依赖在model的pom里引入了
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--定义日志文件的存储地址,使用绝对路径-->
<property name="LOG_HOME" value="e:/logs"/>
<!-- Console 输出设置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<fileNamePattern>${LOG_HOME}/leadnews.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 异步输出 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="FILE"/>
</appender>
<logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<logger name="org.springframework.boot" level="debug"/>
<root level="info">
<!--<appender-ref ref="ASYNC"/>-->
<appender-ref ref="FILE"/>
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
日志输出位置
更加详细的日志信息
总体结构
接口定义
httpenum枚举
以后直接复制粘贴,不用设置常量了
package com.heima.model.common.enums;
public enum AppHttpCodeEnum {
// 成功段0
SUCCESS(200,"操作成功"),
// 登录段1~50
NEED_LOGIN(1,"需要登录后操作"),
LOGIN_PASSWORD_ERROR(2,"密码错误"),
// TOKEN50~100
TOKEN_INVALID(50,"无效的TOKEN"),
TOKEN_EXPIRE(51,"TOKEN已过期"),
TOKEN_REQUIRE(52,"TOKEN是必须的"),
// SIGN验签 100~120
SIGN_INVALID(100,"无效的SIGN"),
SIG_TIMEOUT(101,"SIGN已过期"),
// 参数错误 500~1000
PARAM_REQUIRE(500,"缺少参数"),
PARAM_INVALID(501,"无效参数"),
PARAM_IMAGE_FORMAT_ERROR(502,"图片格式有误"),
SERVER_ERROR(503,"服务器内部错误"),
// 数据错误 1000~2000
DATA_EXIST(1000,"数据已经存在"),
AP_USER_DATA_NOT_EXIST(1001,"ApUser数据不存在"),
DATA_NOT_EXIST(1002,"数据不存在"),
// 数据错误 3000~3500
NO_OPERATOR_AUTH(3000,"无权限操作"),
NEED_ADMIND(3001,"需要管理员权限");
int code;
String errorMessage;
AppHttpCodeEnum(int code, String errorMessage){
this.code = code;
this.errorMessage = errorMessage;
}
public int getCode() {
return code;
}
public String getErrorMessage() {
return errorMessage;
}
}
静态方法获取枚举对象,包含code和message
响应对象静态方法调用成功标识,获取枚举并填入
也可以自定义提示信息,传enum和自定义message
分页信息结果类
继承了基础的结果类,构造函数填入 pageNum,pageSize,total
该类在构造时如果没有super()构造父类则默认无参构造父类
功能实现
思路分析
思路
相关类
ApUserLoginController接口类
package com.heima.user.configcontroller.v1;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/login")
public class ApUserLoginController {
@PostMapping("/login_auth")
public ResponseResult login(@RequestBody LoginDto dto) {
return null;
}
}
logindto
dto前端传来的数据,vo,后端给前端的数据
package com.heima.model.user.dtos;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class LoginDto {
/**
* 手机号
*/
@ApiModelProperty(value = "手机号",required = true)
private String phone;
/**
* 密码
*/
@ApiModelProperty(value = "密码",required = true)
private String password;
}
mapper
package com.heima.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.user.pojos.ApUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ApUserMapper extends BaseMapper<ApUser> {
}
业务层service
package com.heima.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.user.dtos.LoginDto;
import com.heima.model.user.pojos.ApUser;
public interface ApUserService extends IService<ApUser>{
/**
* app端登录
* @param dto
* @return
*/
public ResponseResult login(LoginDto dto);
}
业务实现类ApUserServiceImpl
package com.heima.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.user.dtos.LoginDto;
import com.heima.model.user.pojos.ApUser;
import com.heima.user.mapper.ApUserMapper;
import com.heima.user.service.ApUserService;
import com.heima.utils.common.AppJwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.cli.Digest;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import java.util.HashMap;
import java.util.Objects;
@Service
@Transactional
@Slf4j
public class ApUserServiceImpl extends ServiceImpl<ApUserMapper, ApUser> implements ApUserService {
@Autowired
private ApUserMapper apUserMapper;
/**
* app端登录功能
*
* @param dto
* @return
*/
@Override
public ResponseResult login(LoginDto dto) {
// 用户登录,有传递用户名和密码就不是游客
if (StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())) {
// 拿用户名查数据库,如果是空返回用户不存在响应类
ApUser dbUser = apUserMapper.selectById(dto.getPhone());
if (Objects.isNull(dbUser)) {
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST, "用户不存在");
}
// 如果用户存在且输入了密码,获取盐值,和用户输入的密码一同进行加密,如果匹配成功则以id返回token
String salt = dbUser.getSalt();
String password = dto.getPassword();
String pswd = DigestUtils.md5DigestAsHex((password + salt).getBytes());
if (!pswd.equals(password)) {
return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);
}
String token = AppJwtUtil.getToken(dbUser.getId().longValue());
HashMap<String, Object> stringObjectHashMap = new HashMap<>();
stringObjectHashMap.put("token", token);
dbUser.setPassword("");
dbUser.setSalt("");
stringObjectHashMap.put("user", dbUser);
return ResponseResult.okResult(stringObjectHashMap);
} else {
// 如果没输入密码,以0生成token,返回前端,视为游客
HashMap<String, Object> map = new HashMap<>();
map.put("token", AppJwtUtil.getToken(0L));
return ResponseResult.okResult(map);
}
}
}
启动测试
接口测试工具
postman
http://localhost:51801/api/v1/login/login_auth
这里sql脚本的密码不与视频中admin一致,前往测试类生成一个复制到数据库里再次登录即可
swagger接口文档
-
在线的,发生变化实时更新
-
可进行功能测试
依赖引入到model和common
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
增加自动配置类SwaggerConfiguration
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 SwaggerConfiguration {
@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包下的所有类,进行接口文档配置
配置到factory清单文件里,当common加载时加载清单里的类
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.common.exception.ExceptionCatch
com.heima.common.swagger.SwaggerConfiguration
com.heima.common.swagger.Swagger2Configuration
注解
常见对应关系如下
@Api = controller整个的作用
@ApiOperation = controller下的方法或者接口
@ApiParam = 接口方法参数信息
@ApiModel = 对象
@ApiModel = 实体类
@@ApiModelProperty = 实体类属性
其他注解
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:一个请求参数
@ApiImplicitParams:多个请求参数的描述信息
启动user微服务,访问地址:http://localhost:51801/swagger-ui.html
测试成功
knife4J
提供离线文档
依赖引入
common模块下
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
配置类
package com.heima.common.swagger;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class Swagger2Configuration {
@Bean(value = "defaultApi2")
public Docket defaultApi2() {
Docket docket=new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
//分组名称
.groupName("1.0")
.select()
//这里指定Controller扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.heima"))
.paths(PathSelectors.any())
.build();
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("黑马头条API文档")
.description("黑马头条API文档")
.version("1.0")
.build();
}
}
bean扫描清单
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.common.exception.ExceptionCatch,\
com.heima.common.swagger.SwaggerConfiguration,\
com.heima.common.swagger.Swagger2Configuration
访问
http://localhost:51801/doc.html
APP网关
管理员/自媒体/app网关
依赖引入
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
包含了管理所有模块的网关依赖,nacos服务发现与注册,jwt依赖
新建app-gateway模块指定父模块
启动类
package com.heima.app.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class AppGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(AppGatewayApplication.class,args);
}
}
@EnableDiscoveryClient //开启注册中心
配置文件bootstrap.yml
server:
port: 51601
spring:
application:
name: leadnews-app-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.233.136:8848
config:
server-addr: 192.168.233.136:8848
file-extension: yml
注册到nacos
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
routes:
# 平台管理
- id: user
uri: lb://leadnews-user
predicates:
- Path=/user/**
filters:
- StripPrefix= 1
启动并请求测试
http://localhost:51601/user/api/v1/login/login_auth
认证过滤器jwt
思路分析
登录请求不鉴权,直接去微服务校验密码用户名
其他先判断token,在判断有效期
有效就放行到想去的微服务上
步骤
实现过滤器类,后续请求会被该过滤器拦截
JWT工具类
package com.heima.app.gateway.util;
import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
public class AppJwtUtil {
// TOKEN的有效期一天(S)
private static final int TOKEN_TIME_OUT = 3_600;
// 加密KEY
private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
// 最小刷新间隔(S)
private static final int REFRESH_TIME = 300;
// 生产ID
public static String getToken(Long id){
Map<String, Object> claimMaps = new HashMap<>();
claimMaps.put("id",id);
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(currentTime)) //签发时间
.setSubject("system") //说明
.setIssuer("heima") //签发者信息
.setAudience("app") //接收用户
.compressWith(CompressionCodecs.GZIP) //数据压缩方式
.signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
.addClaims(claimMaps) //cla信息
.compact();
}
/**
* 获取token中的claims信息
*
* @param token
* @return
*/
private static Jws<Claims> getJws(String token) {
return Jwts.parser()
.setSigningKey(generalKey())
.parseClaimsJws(token);
}
/**
* 获取payload body信息
*
* @param token
* @return
*/
public static Claims getClaimsBody(String token) {
try {
return getJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
}
}
/**
* 获取hearder body信息
*
* @param token
* @return
*/
public static JwsHeader getHeaderBody(String token) {
return getJws(token).getHeader();
}
/**
* 是否过期
*
* @param claims
* @return -1:有效,0:有效,1:过期,2:过期
*/
public static int verifyToken(Claims claims) {
if(claims==null){
return 1;
}
try {
claims.getExpiration()
.before(new Date());
// 需要自动刷新TOKEN
if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
return -1;
}else {
return 0;
}
} catch (ExpiredJwtException ex) {
return 1;
}catch (Exception e){
return 2;
}
}
/**
* 由字符串生成加密key
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
public static void main(String[] args) {
/* Map map = new HashMap();
map.put("id","11");*/
System.out.println(AppJwtUtil.getToken(1102L));
Jws<Claims> jws = AppJwtUtil.getJws("eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQqEMAwA_5KzhURNt_qb1KZYQSi0wi6Lf9942NsMw3zh6AVW2DYmDGl2WabkZgreCaM6VXzhFBfJMcMARTqsxIG9Z888QLui3e3Tup5Pb81013KKmVzJTGo11nf9n8v4nMUaEY73DzTabjmDAAAA.4SuqQ42IGqCgBai6qd4RaVpVxTlZIWC826QA9kLvt9d-yVUw82gU47HDaSfOzgAcloZedYNNpUcd18Ne8vvjQA");
Claims claims = jws.getBody();
System.out.println(claims.get("id"));
}
}
过滤器类
package com.heima.app.gateway.filter;
import com.heima.app.gateway.util.AppJwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 1,获取请求路径,如果是登录请求则放行
URI uri = request.getURI();
if (uri.getPath().contains("/login")) {
return chain.filter(exchange);
}
// 2,如果不是登录请求,获取token,判断是否存在
String token = exchange.getRequest().getHeaders().getFirst("token");
if (StringUtils.isEmpty(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 解析token可能会失败因此我们用try catch将其包住在catch处也返回401
// 3,如果存在token,那么再判断其有效期
try {
Claims claimsBody = AppJwtUtil.getClaimsBody(token);
//验证是否过期
int verifyToken = AppJwtUtil.verifyToken(claimsBody);
if(verifyToken==1||verifyToken==2){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
} catch (Exception e) {
e.printStackTrace();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 4,如果token在有效期内则放行
return chain.filter(exchange);
}
/**
* 优先级设置 值越小 优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
App端前端项目集成
解压nginx压缩包(路径必须全英文
),web项目,配置nginx.conf
先配置nginx.conf其端口为8222,因为80一般早被占用了
不搞到linux上了,直接在window用cmd打开根目录后输入nginx进行启动
http://localhost:8222/
配置nginx服务器地址跳转到前端项目
由于一共有三个前端服务,我们给他们分别创建一个配置文件,后续用包含的方式将他们归结在一起,类似于html=》vue的过程
在conf文件夹中新建一个leadnews.conf文件夹
新建第一个配置文件heima-leadnews-app.conf内容如下(注意,有些解压可能会给你多嵌套一层app-web。)
upstream heima-app-gateway{
server localhost:51601;
}
server {
listen 8801;
location / {
root 你前端项目的地址;
index index.html;
}
location ~/app/(.*) {
proxy_pass http://heima-app-gateway/$1;
proxy_set_header HOST $host; # 不改变源请求头的值
proxy_pass_request_body on; #开启获取请求体
proxy_pass_request_headers on; #开启获取请求头
proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息
}
}
在nginx。conf引入所有子配置文件,内容如下,直接覆盖原先所有内容
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 引入自定义配置文件
include leadnews.conf/*.conf;
}
重载nginx,生效配置文件,在刚才打开cmd启动nginx那个目录上输入
nginx.exe -s reload
登录成功
游客登录则默认token