1、项目架构

1.1 概述

问题分析:

为了保证服务层一般使用集群的形式进行部署。借助Dubbo的负载均衡和容错机制保证高可用

image-20220718162426943

image-20220718162528669

API网关有很多实现方式,我们通过SpringCloud Gateway实现

使用Nacos作为配置中心:

image-20210626113359636

1.2 API网关

概述:

image-20220718162714071

搭建网关环境:

image-20220718162809926

统一鉴权:

1、在网关中定义过滤器:完成token检验:

2、filter方法中鉴权业务逻辑

1.2.1 搭建网关

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- nacos配置中心依赖支持
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
-->
<dependency>
<groupId>com.itheima</groupId>
<artifactId>tanhua-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

引导类

1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

跨域问题配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.itheima.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

/**
* 跨域支持
*/
@Configuration
public class CorsConfig {

@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server:
port: 8888
spring:
profiles:
active: prod
application:
name: tanhua-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.136.160:8848
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
server:
port: 8888
spring:
profiles:
active: prod
application:
name: tanhua-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.136.160:8848
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
routes:
# 探花系统
- id: tanhua-app-server
uri: lb://tanhua-app-server
predicates:
- Path=/app/**
filters:
- StripPrefix= 1
# 后台系统
- id: tanhua-admin
uri: lb://tanhua-admin
predicates:
- Path=/admin/**
filters:
- StripPrefix= 1
gateway:
excludedUrls: /user/login,/user/loginVerification,/system/users/verification,/system/users/login

1.2.2 配置鉴权管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Component
public class AuthFilter implements GlobalFilter, Ordered {

//排除的链接
@Value("#{'${gateway.excludedUrls}'.split(',')}")
private List<String> excludedUrls;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();

System.out.println( "url:"+ url);
//排除特殊接口 不校验
if(excludedUrls.contains(url)){
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if(!StringUtils.isEmpty(token)){
token = token.replace("Bearer ", "");
}
ServerHttpResponse response = exchange.getResponse();

//2、使用工具类,判断token是否有效
boolean verifyToken = JwtUtils.verifyToken(token);
//3、如果token失效,返回状态码401,拦截
if(!verifyToken) {
Map<String, Object> responseData = new HashMap<>();
responseData.put("errCode", 401);
responseData.put("errMessage", "用户未登录");
return responseError(response,responseData);
}
return chain.filter(exchange);
}

//响应错误数据
private Mono<Void> responseError(ServerHttpResponse response,Map<String, Object> responseData){
// 将信息转换为 JSON
ObjectMapper objectMapper = new ObjectMapper();
byte[] data = new byte[0];
try {
data = objectMapper.writeValueAsBytes(responseData);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 输出错误信息到页面
DataBuffer buffer = response.bufferFactory().wrap(data);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}

/**
* 设置过滤器的执行顺序
*/
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

1.3 Nacos配置中心

Nacos提供了注册中心和配置管理的能力,使用Nacos可以快速实现服务发现、服务配置管理等需求

image-20210626113928406

项目改造:

image-20220718181609860

nacos中的配置文件:

image-20220718181826773

1.3.1 添加依赖

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

1.3.2 添加bootstrap.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8888
spring:
profiles:
active: prod
application:
name: tanhua-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.136.160:8848
config:
server-addr: 192.168.136.160:8848
file-extension: yml

1.3.3 nacos添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
server:
port: 9997
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
routes:
# 用户微服务
- id: tanhua-app-server
uri: lb://tanhua-app-server
predicates:
- Path=/app/**
filters:
- StripPrefix= 1
# 文章微服务
- id: tanhua-admin
uri: lb://tanhua-admin
predicates:
- Path=/admin/**
filters:
- StripPrefix= 1
gateway:
excludedUrls: /user/login,/user/loginVerification,/system/users/verification,/system/users/login

2、后台系统

2.1 概述

探花交友APP建立的后台管理系统,目的是完成探花交友项目的业务闭环,主要功能包括:用户管理、动态管理、审核管理以及系统管理。

image-20200921180145987

课程中实现的功能有:登录、首页、用户管理、动态审核。

2.2 环境前端搭建

2.2.1 导入数据库

将资料中的tanhua-admin.sql引入到mysql数据库中

image-20210626112809240

image-20220718182339736

2.2.2 导入静态页面

后台系统也是采用前后端分离的方式,前端采用Vue.js实现,关于前端系统我们不进行实现,拿来直接使用。

image-20200921182328856

nginx安装

将资料中提供的nginx解压到合适的位置

image-20201111071005581

其中html目录中为,vue编译后的所有页面。

修改Nginx的/conf/nginx.conf配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   server {
listen 8088; #请求端口
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;


location / {
root html;
index index.html index.htm;
}

location /management {
proxy_pass http://127.0.0.1:18083/; #转发后台地址
}
#....略
}
测试

双击nginx.exe,待启动完成后访问:http://127.0.0.1:8088即可访问后台项目

2.3 搭建后端环境

2.3.1 Admin实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//后台系统的管理员对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Admin implements Serializable {
/**
* id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 头像
*/
private String avatar;
}

2.3.2 导入项目

将今日资料中的tanhua-admin模块导入到探花项目中,完成开发。

image-20210626112959553

2.3.3 nacos中加入配置

DataID:tanhua-admin-prod.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://120.26.160.122:3306/tanhua?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
username: root
password: jin200727
rabbitmq:
host: 192.168.136.160
port: 5672
username: guest
password: guest
redis:
host: 192.168.136.160
port: 6379
cloud: #nacos配置
nacos:
discovery:
server-addr: 192.168.136.160:8848
dubbo: #dubbo配置
registry:
address: spring-cloud://localhost
consumer:
check: false
retries: 0
protocols:
dubbo:
port: -1

#配置短信平台信息
tanhua: #手机验证码,咱们自己注册(不要白嫖)
# 消息发送
sms:
signName: 测试专用模板
templateCode: SMS_154950909
accessKey: LTAI5tH4vxb7jSevNHgX53rv
secret: FURM3LeH6BuspIzfmFUuuotd7hBqTu
# 阿里云存储
oss:
accessKey: LTAI5tH4vxb7jSevNHgX53rv
secret: FURM3LeH6BuspIzfmFUuuotd7hBqTu
endpoint: oss-cn-hangzhou.aliyuncs.com
bucketName: kim-little
url: https://kim-little.oss-cn-hangzhou.aliyuncs.com/
#百度云识别配置
aip:
appId: 26425023
apiKey: RnLai3853A5SQopsWfU2hUP9
secretKey: vQCluUNOTPRM5wqG8llOYFqYVAGDG1v5
huanxin:
appkey: 1129220716100844#demo
clientId: YXA6mzwLcV__QVWkDmLUW2G9LA
clientSecret: YXA6oDNILlmtq50eMm_WxFHlgaB3gPo
#mybaits-plus
mybatis-plus:
global-config:
db-config:
table-prefix: tb_ #数据库表前缀
id-type: auto #数据库表主键的策略

2.4 管理员登录

后台系统的登录模块独立于APP端的登录。

2.4.1 生成验证码

验证码:页面端发送请求到服务端。服务端生成一个验证码的图片,已流的形式返回

image-20220718194238984

image-20220718194225785

image-20220718194254280

验证码:

image-20220718194339277

SystemController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 生成图片验证码
*/
@GetMapping("/verification")
public void verification(String uuid, HttpServletResponse response) throws IOException {
//设置响应参数
response.setDateHeader("Expires", 0);
// Set standard HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// Set standard HTTP/1.0 no-cache header.
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
//1、通过工具类生成验证码对象(图片数据和验证码信息)
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(299, 97);
String code = captcha.getCode(); //1234
//2、调用service,将验证码存入到redis
redisTemplate.opsForValue().set(Constants.CAP_CODE+uuid,code);
//3、通过输出流输出验证码
captcha.write(response.getOutputStream());
}

2.4.2 用户登录

SystemController
1
2
3
4
5
6
7
8
/**
* 用户登录:
*/
@PostMapping("/login")
public ResponseEntity login(@RequestBody Map map) {
Map retMap = adminService.login(map);
return ResponseEntity.ok(retMap);
}
SystemService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//用户登录
public ResponseEntity login(Map map) {
//1、获取请求的参数(username,password,verificationCode(验证码),uuid)
String username = (String) map.get("username");
String password = (String) map.get("password");
String verificationCode = (String) map.get("verificationCode");
String uuid = (String) map.get("uuid");
//2、判断用户名或者密码是否为空
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
//用户名或者密码为空
//throw new BusinessException("用户名或者密码为空");
Map map1 = new HashMap();
map.put("message","用户名或者密码为空");
return ResponseEntity.status(500).body(map1);
}
//3、判断验证码是否正确
if (StringUtils.isEmpty(username)) {
//验证码为空
throw new BusinessException("验证码为空");
}
//从redis中获取验证码
String key = Constants.CAP_CODE+uuid;
String code = redisTemplate.opsForValue().get(key);
if (StringUtils.isEmpty(code) || !code.equals(verificationCode)) {
//验证码错误
throw new BusinessException("验证码错误");
}
redisTemplate.delete(key);
//4、根据用户名查询用户
QueryWrapper<Admin> qw = new QueryWrapper<>();
qw.eq("username", username);
Admin admin = adminMapper.selectOne(qw);
//5、判断用户是否存在,密码是否一致
password = SecureUtil.md5(password); //md5加密
if(admin == null || !admin.getPassword().equals(password)) {
//用户名错误或者密码不一致
throw new BusinessException("用户名或者密码");
}
//6、通过JWT生成token
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("username", username);
claims.put("id", admin.getId());
String token = JwtUtils.getToken(claims);
//8、构造返回值
Map res = new HashMap();
res.put("token", token);
return ResponseEntity.ok(res);
}

用户退出:

image-20200922215207553

AdminController

1
2
3
4
5
6
7
8
9
/**
* 退出
*/
@RequestMapping(value = "/logout",method = RequestMethod.POST)
public ResponseEntity logout(@RequestHeader("Authorization") String token){
String headToken = token.replace("Bearer ", "");
adminService.logout(headToken);
return ResponseEntity.ok(null);
}

3.4.3. AdminService

1
2
3
4
5
6
7
8
/**
* 登出
* @param headToken
*/
public void logout(String headToken) {
String tokenKey = CACHE_KEY_TOKEN_PREFIX + headToken;
redisTemplate.delete(tokenKey);
}

2.4.3 获取用户资料

AdminVO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdminVo {
/**
* id
*/
private String id;
/**
* 用户名
*/
private String username;

/**
* 头像
*/
private String avatar;

public static AdminVo init(Admin admin) {
AdminVo vo = new AdminVo();
vo.setAvatar(admin.getAvatar());
vo.setUsername(admin.getUsername());
vo.setId(admin.getId().toString());
return vo;
}

}
SystemController
1
2
3
4
5
6
7
8
/**
* 获取用户的信息
*/
@PostMapping("/profile")
public ResponseEntity profile() {
AdminVo vo = adminService.profile();
return ResponseEntity.ok(vo);
}
SystemService
1
2
3
4
5
6
//获取当前用户的用户资料
public AdminVo profile() {
Long id = AdminHolder.getId();
Admin admin = adminMapper.selectById(id);
return AdminVo.init(admin);
}

3、用户管理

3.1 需求分析

用户管理:对探花交友客户端注册用户的管理(查询业务数据),使用Dubbo的形式调用tanhua-dubbo-service获取响应的数据结果

3.2 查询用户列表

ManageController

1
2
3
4
5
6
@GetMapping("/users")
public ResponseEntity users(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pagesize) {
PageResult result = managerService.findAllUsers(page,pagesize);
return ResponseEntity.ok(result);
}

ManagerService

1
2
3
4
5
6
7
8
public ResponseEntity findAllUsers(Integer page, Integer pagesize) {
//1、调用API分页查询数据列表 Ipage<UserInfo>
IPage<UserInfo> iPage = userInfoApi.findAll(page,pagesize);
//2、需要将Ipage转化为PageResult
PageResult result = new PageResult(page, pagesize, iPage.getTotal(), iPage.getRecords());
//3、构造返回值
return ResponseEntity.ok(result);
}

UserInfoAPI

1
2
3
4
@Override
public IPage<UserInfo> findAll(Integer page, Integer pagesize) {
return userInfoMapper.selectPage(new Page<UserInfo>(page,pagesize),null);
}

3.3 查看用户详情

ManageController

1
2
3
4
5
6
7
/**
* 根据id查询用户详情
*/
@GetMapping("/users/{userId}")
public ResponseEntity findById(@PathVariable("userId") Long userId) {
return managerService.findById(userId);
}

ManagerService

1
2
3
4
5
//根据id查询用户详情
public ResponseEntity findById(Long userId) {
UserInfo info = userInfoApi.findById(userId);
return ResponseEntity.ok(info);
}

3.4 查看视频列表

image-20220718213749910

ManageController

1
2
3
4
5
6
7
8
9
10
/**
* 查询指定用户发布的所有视频列表
*/
@GetMapping("/videos")
public ResponseEntity videos(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pagesize,
Long uid ) {
PageResult result = managerService.findAllVideos(page,pagesize,uid);
return ResponseEntity.ok(result)
}

ManagerService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//根据用户id分页查询此用户发布的所有视频列表
public PageResult findAllVideos(Integer page, Integer pagesize, Long userId) {
//1、调用API查询视频列表(PageResult<video>)
List<Video> items = videoApi.findByUserId(page,pagesize,userId);
//2、获取到分页对象中的List List<Video>
UserInfo info = userInfoApi.findById(userId);
//3、一个Video转化成一个VideoVo
List<VideoVo> list = new ArrayList<>();
for (Video item : items) {
VideoVo vo = VideoVo.init(info, item);
list.add(vo);
}
//4、构造返回
return ResponseEntity.ok(new PageResult(page,pagesize,0l,list));
}

3.5 查看动态列表

image-20220718213820302

state是审核状态

image-20220718213731627

ManageController

1
2
3
4
5
6
7
@GetMapping("/messages")
public ResponseEntity messages(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pagesize,
Long uid,Integer state ) {
PageResult result = managerService.findAllMovements(page,pagesize,uid,state);
return ResponseEntity.ok(result)
}

ManagerService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

//查询指定用户发布的所有动态
public ResponseEntity findAllMovements(Integer page, Integer pagesize, Long userId, Integer state) {
//1、调用API查询 :(PageResult<Publish>)
PageResult result = movementApi.findByUserId(userId,state, page, pagesize);
//2、一个Publsh构造一个Movements
List<Movement> items = (List<Movement>) result.getItems();
if (CollUtil.isEmpty(items)) {
return ResponseEntity.ok(result);
}
List<Long> userIds = CollUtil.getFieldValues(items,"userId",Long.class);
Map<Long,UserInfo> map =userInfoApi.findByIds(userIds,null);
List<MovementsVo> vos = new ArrayList<>();
for (Movement movement : items) {

UserInfo userInfo = map.get(movement.getUserId());
if (userInfo!=null){
MovementsVo vo = MovementsVo.init(userInfo,movement);
vos.add(vo);
}

}
//3、构造返回值
result.setItems(vos);
return ResponseEntity.ok(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    @Override
public Map<Long, UserInfo> findByIds(List<Long> userIds, UserInfo info) {
QueryWrapper qw = new QueryWrapper();
// 用户id列表
qw.in("id", userIds);
// 构造查询条件
if (info != null) {
if (info.getAge() != null) {
qw.lt("age", info.getAge());
}
if (!StringUtils.isEmpty(info.getGender())) {
qw.eq("gender", info.getGender());
}
}
List<UserInfo> list = userInfoMapper.selectList(qw);
Map<Long, UserInfo> map = CollUtil.fieldValueMap(list, "id");
return map;
}

image-20220718220944001

查询动态详情:

image-20220718221101053

68-探花10-后台管理系统(第10章)-cnblog - ofanimon - 博客园 (cnblogs.com)