1、项目架构
1.1 概述
问题分析:
为了保证服务层一般使用集群的形式进行部署。借助Dubbo的负载均衡和容错机制保证高可用


API网关有很多实现方式,我们通过SpringCloud Gateway实现
使用Nacos作为配置中心:

1.2 API网关
概述:

搭建网关环境:

统一鉴权:
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>
<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();
boolean verifyToken = JwtUtils.verifyToken(token); 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){ 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可以快速实现服务发现、服务配置管理等需求

项目改造:

nacos中的配置文件:

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

课程中实现的功能有:登录、首页、用户管理、动态审核。
2.2 环境前端搭建
2.2.1 导入数据库
将资料中的tanhua-admin.sql
引入到mysql数据库中


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

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

其中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模块导入到探花项目中,完成开发。

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: discovery: server-addr: 192.168.136.160:8848 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 clientId: YXA6mzwLcV__QVWkDmLUW2G9LA clientSecret: YXA6oDNILlmtq50eMm_WxFHlgaB3gPo
mybatis-plus: global-config: db-config: table-prefix: tb_ id-type: auto
|
2.4 管理员登录
后台系统的登录模块独立于APP端的登录。
2.4.1 生成验证码
验证码:页面端发送请求到服务端。服务端生成一个验证码的图片,已流的形式返回



验证码:

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); response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); response.addHeader("Cache-Control", "post-check=0, pre-check=0"); response.setHeader("Pragma", "no-cache"); response.setContentType("image/jpeg"); LineCaptcha captcha = CaptchaUtil.createLineCaptcha(299, 97); String code = captcha.getCode(); redisTemplate.opsForValue().set(Constants.CAP_CODE+uuid,code); 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) { String username = (String) map.get("username"); String password = (String) map.get("password"); String verificationCode = (String) map.get("verificationCode"); String uuid = (String) map.get("uuid"); if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { Map map1 = new HashMap(); map.put("message","用户名或者密码为空"); return ResponseEntity.status(500).body(map1); } if (StringUtils.isEmpty(username)) { throw new BusinessException("验证码为空"); } 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); QueryWrapper<Admin> qw = new QueryWrapper<>(); qw.eq("username", username); Admin admin = adminMapper.selectOne(qw); password = SecureUtil.md5(password); if(admin == null || !admin.getPassword().equals(password)) { throw new BusinessException("用户名或者密码"); } Map<String, Object> claims = new HashMap<String, Object>(); claims.put("username", username); claims.put("id", admin.getId()); String token = JwtUtils.getToken(claims); Map res = new HashMap(); res.put("token", token); return ResponseEntity.ok(res); }
|
用户退出:

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
|
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 {
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) { IPage<UserInfo> iPage = userInfoApi.findAll(page,pagesize); PageResult result = new PageResult(page, pagesize, iPage.getTotal(), iPage.getRecords()); 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
|
@GetMapping("/users/{userId}") public ResponseEntity findById(@PathVariable("userId") Long userId) { return managerService.findById(userId); }
|
ManagerService
1 2 3 4 5
| public ResponseEntity findById(Long userId) { UserInfo info = userInfoApi.findById(userId); return ResponseEntity.ok(info); }
|
3.4 查看视频列表

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
| public PageResult findAllVideos(Integer page, Integer pagesize, Long userId) { List<Video> items = videoApi.findByUserId(page,pagesize,userId); UserInfo info = userInfoApi.findById(userId); List<VideoVo> list = new ArrayList<>(); for (Video item : items) { VideoVo vo = VideoVo.init(info, item); list.add(vo); } return ResponseEntity.ok(new PageResult(page,pagesize,0l,list)); }
|
3.5 查看动态列表

state是审核状态

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) { PageResult result = movementApi.findByUserId(userId,state, page, pagesize); 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); }
} 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();
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; }
|

查询动态详情:

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