当前位置:   article > 正文

《学成在线》微服务实战项目实操笔记系列(P92~P120)【下】

《学成在线》微服务实战项目实操笔记系列(P92~P120)【下】

史上最详细《学成在线》项目实操笔记系列【下】,跟视频的每一P对应,全系列18万字,涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳,参考这篇,相信会带给你极大启发。

 四、课程发布模块

4.1 (课程发布)模块需求 P92

课程预览:在发布课程之前需要预览一下,看最终的效果有没有问题,课程信息是否完整。

课程审核:预览之后就是运营人员进行审核,审核分为程序自动审核和人工审核。

课程发布:发布之后课程可以被搜索到。

4.2 (课程发布)freemarker P93

freemarker是模板引擎

在xuecheng-plus-content的xuecheng-plus-content-api的pom.xml下新增依赖:

  1. <!-- Spring Boot 对结果视图 Freemarker 集成 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-freemarker</artifactId>
  5. </dependency>

nacos的freemarker-config-dev.yaml中写入如下代码:

在xuecheng-plus-content-api下进行如下配置:

 

在xuecheng-plus-content的xuecheng-plus-content-api的api下创建FreemarkerController中写入如下代码:

  1. @Slf4j
  2. @RestController
  3. public class FreemarkerController {
  4. @GetMapping("/testfreemarker")
  5. public ModelAndView test(){
  6. ModelAndView modelAndView = new ModelAndView();
  7. //指定模型
  8. modelAndView.addObject("name","小明");
  9. //指定模板
  10. modelAndView.setViewName("test");//根据视图名称加.ftl找到模板
  11. return modelAndView;
  12. }
  13. }

测试没啥问题:

4.3 (课程发布)部署门户 P94

静态资源一般部署到nginx上。

首先解压nginx文件,配置文件位置如下:

 

啥也不配置直接启动,效果如下:

windows本地机hosts文件地址如下:C:\Windows\System32\drivers\etc,写入如下配置

127.0.0.1 www.51xuecheng.cn 51xuecheng.cn ucenter.51xuecheng.cn teacher.51xuecheng.cn file.51xuecheng.cn

在server_name中写入如下门户地址:

完整的配置如下:

  1. server {
  2. listen 80;
  3. server_name www.51xuecheng.cn localhost;
  4. #rewrite ^(.*) https://$server_name$1 permanent;
  5. #charset koi8-r;
  6. ssi on;
  7. ssi_silent_errors on;
  8. #access_log logs/host.access.log main;
  9. location / {
  10. alias C:/xuechengzaixian/xc-ui-pc-static-portal/;
  11. index index.html index.htm;
  12. }
  13. #静态资源
  14. location /static/img/ {
  15. alias C:/xuechengzaixian/xc-ui-pc-static-portal/img/;
  16. }
  17. location /static/css/ {
  18. alias C:/xuechengzaixian/xc-ui-pc-static-portal/css/;
  19. }
  20. location /static/js/ {
  21. alias C:/xuechengzaixian/xc-ui-pc-static-portal/js/;
  22. }
  23. location /static/plugins/ {
  24. alias C:/xuechengzaixian/xc-ui-pc-static-portal/plugins/;
  25. add_header Access-Control-Allow-Origin http://ucenter.51xuecheng.cn;
  26. add_header Access-Control-Allow-Credentials true;
  27. add_header Access-Control-Allow-Methods GET;
  28. }
  29. location /plugins/ {
  30. alias C:/xuechengzaixian/xc-ui-pc-static-portal/plugins/;
  31. }
  32. }

保存之后, 2种方法让配置生效,方法1:ctrl+shift+esc直接把任务停掉。方法2:nginx.exe -s reload。可以看到显示没有任何问题:

课程详情页面能正常浏览:

记得下面weight=10后面有一个分号;

  1. upstream fileserver{
  2. server 192.168.101.65:9000 weight=10;
  3. }
  1. server {
  2. listen 80;
  3. server_name file.51xuecheng.cn;
  4. #charset koi8-r;
  5. ssi on;
  6. ssi_silent_errors on;
  7. #access_log logs/host.access.log main;
  8. location /video {
  9. proxy_pass http://fileserver;
  10. }
  11. location /mediafiles {
  12. proxy_pass http://fileserver;
  13. }
  14. }

效果如下: 

下面测试一下:

http://file.51xuecheng.cn/mediafiles/2022/09/13/a16da7a132559daf9e1193166b3e7f52.jpg

最后想要视频能够播放,添加如下配置:

  1. location /course/preview/learning.html {
  2. alias D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/course/learning.html;
  3. }
  4. location /course/search.html {
  5. root D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal;
  6. }
  7. location /course/learning.html {
  8. root D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal;
  9. }

进入到播放详情页面:

http://www.51xuecheng.cn/course/course_template.html

搜索videoObject,然后找到video对应的url链接,填写minio上视频的链接。

经测试视频播放没有问题:

完整配置如下:

  1. worker_processes 1;
  2. events {
  3. worker_connections 1024;
  4. }
  5. http {
  6. include mime.types;
  7. default_type application/octet-stream;
  8. sendfile on;
  9. keepalive_timeout 65;
  10. upstream fileserver{
  11. server 192.168.101.65:9000 weight=10;
  12. }
  13. server {
  14. listen 80;
  15. server_name file.51xuecheng.cn;
  16. ssi on;
  17. ssi_silent_errors on;
  18. location /video {
  19. proxy_pass http://fileserver;
  20. }
  21. location /mediafiles {
  22. proxy_pass http://fileserver;
  23. }
  24. }
  25. server {
  26. listen 80;
  27. server_name www.51xuecheng.cn localhost;
  28. ssi on;
  29. ssi_silent_errors on;
  30. location / {
  31. alias C:/xuechengzaixian/xc-ui-pc-static-portal/;
  32. index index.html index.htm;
  33. }
  34. location /static/img/ {
  35. alias C:/xuechengzaixian/xc-ui-pc-static-portal/img/;
  36. }
  37. location /static/css/ {
  38. alias C:/xuechengzaixian/xc-ui-pc-static-portal/css/;
  39. }
  40. location /static/js/ {
  41. alias C:/xuechengzaixian/xc-ui-pc-static-portal/js/;
  42. }
  43. location /static/plugins/ {
  44. alias C:/xuechengzaixian/xc-ui-pc-static-portal/plugins/;
  45. add_header Access-Control-Allow-Origin http://ucenter.51xuecheng.cn;
  46. add_header Access-Control-Allow-Credentials true;
  47. add_header Access-Control-Allow-Methods GET;
  48. }
  49. location /plugins/ {
  50. alias C:/xuechengzaixian/xc-ui-pc-static-portal/plugins/;
  51. }
  52. location /course/preview/learning.html {
  53. alias C:/xuechengzaixian/xc-ui-pc-static-portal/course/learning.html;
  54. }
  55. location /course/search.html {
  56. root C:/xuechengzaixian/xc-ui-pc-static-portal;
  57. }
  58. location /course/learning.html {
  59. root C:/xuechengzaixian/xc-ui-pc-static-portal;
  60. }
  61. }
  62. }

4.4 (课程预览)接口开发 P95

把course_template.html(这里面都是写死的数据)拷贝到xuecheng-plus-content-api的resources的templates下,改后缀名为course_template.ftl:

在xuecheng-plus-content-api的api下创建CoursePublishController,写入如下代码:

  1. @Controller
  2. public class CoursePublishController {
  3. @Autowired
  4. CoursePublishService coursePublishService;
  5. @GetMapping("/coursepreview/{courseId}")
  6. public ModelAndView preview(@PathVariable("courseId")Long courseId){
  7. ModelAndView modelAndView = new ModelAndView();
  8. //查询课程的信息作为模板数据
  9. CoursePreviewDto coursePreviewDto = coursePublishService.getCoursePreviewInfo(courseId);
  10. modelAndView.addObject("model",coursePreviewDto);
  11. modelAndView.setViewName("course_template");
  12. return modelAndView;
  13. }
  14. }

注解@Controller响应页面,@RestController响应json

启动content后,访问下面连接:

http://localhost:63040/content/coursepreview/12

会出现下面界面: 

css页面在nginx里面,现在可以直接在nginx中配置经过网关。

-

代码如下(千万记得在gatewayserver后面还要加上一个/):

  1. upstream gatewayserver{
  2. server 127.0.0.1:63010 weight=10;
  3. }
  4. #api
  5. location /api/ {
  6. proxy_pass http://gatewayserver/;
  7. }

解析:比如现在我输入:www.51xuecheng.cn/api/content/coursepreview/12,nginx会将www.51xuecheng.cn/api/,解析成http://127.0.0.1:63010/,然后把content/coursepreview/12拼接到解析后的访问前缀中,http://127.0.0.1:63010/content/coursepreview/12。

输入url,先到nginx,然后到网关,最后到微服务。

在xuecheng-plus-content-model下面创建CoursePreviewDto,然后写入如下代码:

  1. @Data
  2. public class CoursePreviewDto {
  3. //课程基本信息,营销信息
  4. private CourseBaseInfoDto courseBase;
  5. //课程计划信息
  6. private List<TeachplanDto> teachplans;
  7. //课程师资信息..
  8. }

在xuecheng-plus-content-service的service下面创建CoursePublishService:

  1. //课程发布相关接口
  2. public interface CoursePublishService {
  3. /**
  4. * @param courseId 课程id
  5. * @return
  6. */
  7. public CoursePreviewDto getCoursePreviewInfo(Long courseId);
  8. }

在xuecheng-plus-content-service的service的impl下面创建CoursePublishServiceImpl,写入如下代码:

  1. //课程发布相关接口实现
  2. @Slf4j
  3. @Service
  4. public class CoursePublishServiceImpl implements CoursePublishService {
  5. @Autowired
  6. CourseBaseInfoService courseBaseInfoService;
  7. @Autowired
  8. TeachplanService teachplanService;
  9. @Override
  10. public CoursePreviewDto getCoursePreviewInfo(Long courseId) {
  11. CoursePreviewDto coursePreviewDto = new CoursePreviewDto();
  12. //课程基本信息,营销信息
  13. CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
  14. //课程计划信息
  15. List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
  16. coursePreviewDto.setTeachplans(teachplanTree);
  17. return coursePreviewDto;
  18. }
  19. }

前端放开下面的服务网关端口,启动下面3个服务: 

 

访问下面的地址:

http://localhost:8601/

在IDEA中打上断点,点击预览按钮, 然后看看取得的数据是不是完整正确的

 

上一步只是获取到数据,下一步开始在页面中动态展示。

更改完模板之后,可以选择build下面的recompile进行重新编译。

举例修改下面2个地方:

 有一个现成的course_template.ftl文件

这里要记得修改27行的下面这个地方:

在nginx中配置如下,主要是为了看视频和目录:

/content/open/主要为了显示目录,/media/open/主要为了显示视频资源。

 在xuecheng-plus-content-api的api下面创建CourseOpenController类,写入如下代码:

  1. @Api(value = "课程公开查询接口",tags = "课程公开查询接口")
  2. @RestController
  3. @RequestMapping("/open")
  4. public class CourseOpenController {
  5. @Autowired
  6. private CourseBaseInfoService courseBaseInfoService;
  7. @Autowired
  8. private CoursePublishService coursePublishService;
  9. @GetMapping("/course/whole/{courseId}")
  10. public CoursePreviewDto getPreviewInfo(@PathVariable("courseId") Long courseId) {
  11. //获取课程预览信息
  12. CoursePreviewDto coursePreviewInfo = coursePublishService.getCoursePreviewInfo(courseId);
  13. return coursePreviewInfo;
  14. }
  15. }

在xuecheng-plus-media-api的api下面创建MediaOpenController类,写入如下代码:

  1. @Api(value = "媒资文件管理接口",tags = "媒资文件管理接口")
  2. @RestController
  3. @RequestMapping("/open")
  4. public class MediaOpenController {
  5. @Autowired
  6. MediaFileService mediaFileService;
  7. @ApiOperation("预览文件")
  8. @GetMapping("/preview/{mediaId}")
  9. public RestResponse<String> getPlayUrlByMediaId(@PathVariable String mediaId){
  10. MediaFiles mediaFiles = mediaFileService.getFileById(mediaId);
  11. if (mediaFiles==null) {
  12. return RestResponse.validfail("找不到视频");
  13. }
  14. String url = mediaFiles.getUrl();
  15. if (StringUtils.isEmpty(url)) {
  16. return RestResponse.validfail("该视频正在处理中");
  17. }
  18. return RestResponse.success(mediaFiles.getUrl());
  19. }
  20. }

启动下面4个服务: 

测试如下,视频可以正常播放:

 

4.5 提交课程审核 P96

在course_base表中设置课程审核状态字段,包括:未提交、已提交、审核通过、审核不通过。

只有审核通过才能够发布。只有未提交和审核不通过才能到已提交状态。

但是要注意教学机构可以在审核状态中修改部分信息,但是在运营人员正在审核时教学机构不能修改信息(可以新建一个副本)。

如上图建一个预发布表,集成了课程营销信息、课程师资、课程基本信息、课程计划这几张表。

审核审的是预发布表,修改修改的是左边的4张表。

如果可以发布,就是把预发布表的拷贝到发布表。

如果审核人员正在审核预发布表,则教育机构不能提交审核。

信息组合可以直接以json串的格式传入:

对提交的约束如下:

在xuecheng-plus-content-service的service下创建CoursePublishService中写入如下代码:

public void commitAudit(Long companyId,Long courseId);

在xuecheng-plus-content-service的service下的CoursePublishServiceImpl下写入如下代码:

  1. @Override
  2. public void commitAudit(Long companyId, Long courseId) {
  3. CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
  4. if(courseBaseInfo == null){
  5. XueChengPlusException.cast("课程找不到");
  6. }
  7. //审核状态
  8. String auditStatus = courseBaseInfo.getAuditStatus();
  9. //如果课程的审核状态为已提交则不允许提交
  10. if(auditStatus.equals("202003")){
  11. XueChengPlusException.cast("课程已提交请等待审核");
  12. }
  13. //课程的图片、计划信息没有填写也不允许提交
  14. String pic = courseBaseInfo.getPic();
  15. if(StringUtils.isEmpty(pic)){
  16. XueChengPlusException.cast("请求上传课程图片");
  17. }
  18. //查询课程计划
  19. //课程计划信息
  20. List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
  21. if(teachplanTree==null || teachplanTree.size()==0){
  22. XueChengPlusException.cast("请编写课程计划");
  23. }
  24. //查询到课程基本信息、营销信息。计划等信息插入到课程预发布表
  25. CoursePublishPre coursePublishPre = new CoursePublishPre();
  26. BeanUtils.copyProperties(courseBaseInfo,coursePublishPre);
  27. //营销信息
  28. CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
  29. //转JSON
  30. String courseMarketJson = JSON.toJSONString(courseMarket);
  31. coursePublishPre.setMarket(courseMarketJson);
  32. //计划信息
  33. //转json
  34. String teachplanTreeJson = JSON.toJSONString(teachplanTree);
  35. coursePublishPre.setTeachplan(teachplanTreeJson);
  36. //状态为已提交
  37. coursePublishPre.setStatus("202003");
  38. //提交时间
  39. coursePublishPre.setCreateDate(LocalDateTime.now());
  40. //查询预发布表,如果有记录则更新,没有则插入
  41. CoursePublishPre coursePublishPreObj = coursePublishPreMapper.selectById(courseId);
  42. if(coursePublishPreObj==null){
  43. //插入
  44. coursePublishPreMapper.insert(coursePublishPre);
  45. }else{
  46. //更新
  47. coursePublishPreMapper.updateById(coursePublishPre);
  48. }
  49. //更新课程基本信息表的审核状态为已提交
  50. CourseBase courseBase = courseBaseMapper.selectById(courseId);
  51. courseBase.setAuditStatus("202003");//审核状态为已提交
  52. courseBaseMapper.updateById(courseBase);
  53. }

记得在xuecheng-plus-content-service下的CourseBaseInfoServiceImpl的getCourseBaseInfo方法下写入如下代码:

  1. CourseCategory mtObj = courseCategoryMapper.selectById(courseBase.getMt());
  2. String mtName = mtObj.getName();//大分类名称
  3. courseBaseInfoDto.setMtName(mtName);
  4. CourseCategory stObj = courseCategoryMapper.selectById(courseBase.getSt());
  5. String stName = stObj.getName();//小分类名称
  6. courseBaseInfoDto.setStName(stName);

重启content模块,测试:

如果提交审核失败,页面上端会显示失败的原因。

如果提交审核成功,会在content数据库的course_publish_pre表里面看到这条记录。

首先更改预发布表的状态为审核通过

然后更改课程基本信息表

 4.6 (课程发布)需求分析 P97

发布之后课程信息的网页是能够众多网民看的,如果存储在数据库中,可能导致性能低下。

课程的信息要插入到Elasticsearch中,把课程信息缓存到Redis中。生成的课程静态页面(html文件)上传到minio中。

4.7 什么是分布式事务 P98

本地事务(使用服务自己的数据库来控制事务)是spring利用数据库本身的事务特性去控制事务。本地事务具有CAID四大特性,会将事务纳入一个不可分割的执行单元。

分布式事务(特点是涉及到多个服务来执行同一件事。分布式系统之间要完成一件事,服务之间还要通过远程调用交互)

分布式事务例子:

微服务架构,比如下订单后调用库存服务减库存:

单服务多数据库:

多服务单数据库:

4.8 什么是CAP理论 P99

CAP是Consistency、Availability、Partition tolerance,即一致性、可用性、分区容忍性的缩写。

一致性:用户不管访问哪个结点拿到的数据都是最新的,比如查询小明的信息,不能出现在数据库没有改变情况下两次查询结果不一样。

可用性:指任何时候查询用户信息都可以查询到结果,但不保证查询到最新的数据。

分区容忍性:也叫分区容错性,当系统采用分布式架构时由于网络通信异常导致请求中断、消息丢失,但系统依然对外提供服务。

A和C不能同时满足,要么满足AP(强调可用性)要么满足CP(一致性)。

比如用户把自己的名字“小明”上传到服务节点1。如果要保证一致性,只有当服务节点1中的数据同步到服务节点2中系统才可用。如果要保证可用性,就不能等待信息同步完成,在同步过程中也能使用。

银行转账一定保证CP。

但现实生活中一般AP的场景比较多。所以提出了BASE理论。

BASE是Basically Available(基本可用),Soft State(软状态)和Eventuallyconsistent(最终一致性)。

基本可用:比如在订单高峰的时候,只要支付能用即可。

软状态:有一个中间状态,比如运输中...支付中...

最终一致性:最终的数据要一致。

实现AP保证数据最终一致性:

使用消息队列:如失败自动充实,达到最大失败次数人工处理。

使用任务调度的方案:启动任务调度将课程信息由数据库同步到Elasticsearch、Minio、redis中。

4.9 分布式事务控制方案 P100

现在可以新建一张消息表,现在可以在该表中标记一个字段值表示为要发布的课程,然后任务调度中心去调度服务,把信息同步到redis、Elasticsearch和minio中。写完之后把数据删掉

本地消息表+任务调度的机制来完成分布式事务的最终事务一致性的控制。

course_publish和mq_message表是在同一个数据库,可以使用数据库事务来控制

任务调度程序可以读取mq_message的数据,然后同步到redis,Elasticsearch和minio中。

现在假如redis挂掉了怎么办呢?无数轮也没用了。程序自动运维,监管系统网管系统告警系统,运维人员收到告警远程处理或者去现场处理。

4.10 (课程发布)发布接口P101

在xuecheng-plus-content-api的api的CoursePublishController下写入代码:

在xuecheng-plus-content-service的service的CoursePublishService下写入代码:

在xuecheng-plus-content-service的service的impl的CoursePublishServiceImpl下写入代码:

4.11 (课程发布)消息sdk P102

在第8天资料中,解压出下面文件xuecheng-plus-message-sdk:

把工具包拷贝到工程目录中,设置为maven工程:

在xuecheng-plus-content-service下的pom.xml文件中写入依赖:

  1. <dependency>
  2. <groupId>com.xuecheng</groupId>
  3. <artifactId>xuecheng-plus-message-sdk</artifactId>
  4. <version>0.0.1-SNAPSHOT</version>
  5. </dependency>

在xuecheng-plus-content-api的api的CoursePublishController下写入代码:

  1. @ApiOperation("课程发布")
  2. @ResponseBody
  3. @PostMapping("/coursepublish/{courseId}")
  4. public void coursepublish(@PathVariable("courseId") Long courseId){
  5. Long companyId = 1232141425L;
  6. coursePublishService.publish(companyId,courseId);
  7. }

在xuecheng-plus-content-service的service的CoursePublishService下写入代码:

public void publish(Long companyId,Long courseId);

在xuecheng-plus-content-service的service的impl的CoursePublishServiceImpl下写入代码:

  1. @Autowired
  2. CoursePublishMapper coursePublishMapper;
  3. @Autowired
  4. MqMessageService mqMessageService;
  5. @Transactional
  6. @Override
  7. public void publish(Long companyId, Long courseId) {
  8. //查询预发布表
  9. CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
  10. if(coursePublishPre==null){
  11. XueChengPlusException.cast("课程没有审核记录,无法发布");
  12. }
  13. //状态
  14. String status = coursePublishPre.getStatus();
  15. //课程如果没有审核通过不允许发布
  16. if(!status.equals("202004")){
  17. XueChengPlusException.cast("课程没有审核通过不允许发布");
  18. }
  19. //向课程发布表写入数据
  20. CoursePublish coursePublish = new CoursePublish();
  21. BeanUtils.copyProperties(coursePublishPre,coursePublish);
  22. //先查询课程发布表,有则更新,没有再添加
  23. CoursePublish coursePublishObj = coursePublishMapper.selectById(courseId);
  24. if(coursePublishObj==null){
  25. coursePublishMapper.insert(coursePublish);
  26. }else{
  27. coursePublishMapper.updateById(coursePublish);
  28. }
  29. //向消息表写入数据
  30. MqMessage mqMessage = mqMessageService.addMessage("course_publish", String.valueOf(courseId), null, null);
  31. if(mqMessage==null){
  32. XueChengPlusException.cast(CommonError.UNKOWN_ERROR);
  33. }
  34. //将预发布表数据删除
  35. coursePublishPreMapper.deleteById(courseId);
  36. }

启动contentApplication和gatewayApplication和systemApplication,记得启动nginx和前端,进行前后端联调。

在预发布表course_publish_pre已有一条数据,改为202004

course_base那条数据也改为202004,记得是在audit_status这个字段进行修改:

进入前端,找到之前那条已经审核通过的,然后点击发布,在课程发布表可以看到course_publish,在mq_message也可以看到记录,

4.12 (课程发布)课程发布任务调度 P103

在xuecheng-plus-content-service的service下面创建jobhandler包,然后创建一个CoursePublishTask类,写入如下代码:

在xuecheng-plus-content-service的pom.xml下面写入依赖:

  1. <dependency>
  2. <groupId>com.xuxueli</groupId>
  3. <artifactId>xxl-job-core</artifactId>
  4. </dependency>

在content-service-dev.yaml配置文件中进行配置:

  1. xxl:
  2. job:
  3. admin:
  4. addresses: http://192.168.101.65:8088/xxl-job-admin
  5. executor:
  6. appname: coursepublish-job
  7. address:
  8. ip:
  9. port: 8999
  10. logpath: /data/applogs/xxl-job/jobhandler
  11. logretentiondays: 30
  12. accessToken: default_token

把XxlJobConfig拷贝到xuecheng-plus-content-service的config下面: 

执行器coursepublish-job

在任务管理-课程发布任务执行器新建下面的任务,记得启动:

要在下面这个地方打上断点:

4.13 (课程发布)页面静态化P104

原理:因为静态页面可以使用nginx(每秒大约5万并发),apache等高性能的web服务器,并发性能高。

页面静态化:将生产html页面过程提前,提前使用模板引擎技术生成html页面,客户端可以直接请求到html页面。

用页面静态化技术的时机:当数据不频繁变化。因为课程发布后仍能修改,但需要经过课程审核。

在xuecheng-plus-content-service中添加如下依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-freemarker</artifactId>
  4. </dependency>

在xuecheng-plus-content-service的test下创建FreemarkerTest,写入如下测试代码(这里我是把classpath写死了,老师的写法会报错,可能是因为路径上有中文的缘故吧):

  1. @SpringBootTest
  2. public class FreemarkerTests {
  3. @Autowired
  4. CoursePublishService coursePublishService;
  5. //测试页面静态化
  6. @Test
  7. public void testGenerateHtmlByTemplate() throws IOException, TemplateException {
  8. //配置freemarker
  9. Configuration configuration = new Configuration(Configuration.getVersion());
  10. //加载模板
  11. //选指定模板路径,classpath下templates下
  12. //得到classpath路径
  13. //String classpath = this.getClass().getResource("/").getPath();
  14. String classpath = "C:\\xuechengzaixian\\xuecheng-plus-project\\xuecheng-plus-content\\xuecheng-plus-content-service\\target\\test-classes";
  15. configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
  16. //设置字符编码
  17. configuration.setDefaultEncoding("utf-8");
  18. //指定模板文件名称
  19. Template template = configuration.getTemplate("course_template.ftl");
  20. //准备数据
  21. CoursePreviewDto coursePreviewInfo = coursePublishService.getCoursePreviewInfo(2L);
  22. Map<String, Object> map = new HashMap<>();
  23. map.put("model", coursePreviewInfo);
  24. //静态化
  25. //参数1:模板,参数2:数据模型
  26. String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
  27. System.out.println(content);
  28. //将静态化内容输出到文件中
  29. InputStream inputStream = IOUtils.toInputStream(content);
  30. //输出流
  31. FileOutputStream outputStream = new FileOutputStream("C:\\software\\test.html");
  32. IOUtils.copy(inputStream, outputStream);
  33. }
  34. }

因为微服务各个服务之间是各司其职的,现在媒资服务是专门负责上传的,现在如果想把生成的静态文件上传到minio,需要Feign。

在xuecheng-plus-content-service的pom.xml文件中写入如下代码:

  1. <dependency>
  2. <groupId>com.alibaba.cloud</groupId>
  3. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  4. </dependency>
  5. <!-- Spring Cloud 微服务远程调用 -->
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-openfeign</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>io.github.openfeign</groupId>
  12. <artifactId>feign-httpclient</artifactId>
  13. </dependency>
  14. <!--feign支持Multipart格式传参-->
  15. <dependency>
  16. <groupId>io.github.openfeign.form</groupId>
  17. <artifactId>feign-form</artifactId>
  18. <version>3.8.0</version>
  19. </dependency>
  20. <dependency>
  21. <groupId>io.github.openfeign.form</groupId>
  22. <artifactId>feign-form-spring</artifactId>
  23. <version>3.8.0</version>
  24. </dependency>

把如下的代码写入到nacos的feign-dev.yaml中:

  1. feign:
  2. hystrix:
  3. enabled: true
  4. circuitbreaker:
  5. enabled: true
  6. hystrix:
  7. command:
  8. default:
  9. execution:
  10. isolation:
  11. thread:
  12. timeoutInMilliseconds: 30000 #熔断超时时间
  13. ribbon:
  14. ConnectTimeout: 60000 #连接超时时间
  15. ReadTimeout: 60000 #读超时时间
  16. MaxAutoRetries: 0 #重试次数
  17. MaxAutoRetriesNextServer: 1 #切换实例的重试次数

拷贝MultipartSupportConfig到xuecheng-plus-content-service的config下

在xuecheng-plus-content-api和service的test的配置文件中,都引入下面的配置文件:

  1. shared-configs:
  2. - data-id: feign-${spring.profiles.active}.yaml
  3. group: xuecheng-plus-common
  4. refresh: true

在MediaFileService和MediaFileServiceImpl的uploadFile方法中添加一个参数:String objectName。

4.14 (课程发布)熔断降级 P105

现在是内容管理服务调用媒资管理服务。

spring会生成一个代理对象,在代理对象中去实现远程调用。

@FeignClient(value="media-api")用FeignClient注解来指定属于哪个服务,比如媒资服务。

在xuecheng-plus-content-service的content的feignclient下创建MediaServiceClient,写入如下代码:

  1. //远程调用媒资服务的接口
  2. @FeignClient(value="media-api",configuration = {MultipartSupportConfig.class})
  3. public interface MediaServiceClient {
  4. @RequestMapping(value="/media/upload/coursefile",consumes= MediaType.MULTIPART_FORM_DATA)
  5. public String upload(@RequestPart("filedata")MultipartFile filedata,
  6. @RequestParam(value="objectName",required = false)String objectName);
  7. }

 在xuecheng-plus-content-service的test下的content下创建FeignUploadTest,写入如下代码:

  1. @SpringBootTest
  2. public class FeignUploadTest {
  3. @Autowired
  4. MediaServiceClient mediaServiceClient;
  5. @Test
  6. public void test() {
  7. //将file转MultipartFile
  8. File file = new File("C:\\software\\test.html");
  9. MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);
  10. mediaServiceClient.upload(multipartFile,"course/test.html");
  11. }
  12. }

  在xuecheng-plus-content-service的test下的ContentApplication的类上加入如下注解:

@EnableFeignClients(basePackages={"com.xuecheng.content.feignclient"})

现在出现的是下面这个问题: 

需要加入下面蓝色区域的代码: 

可以看到minio中的mediafiles下的course目录下有了test.html文件:

可以通过下面的链接查看上传的文件,其中test.html要替换为你自己的文件名

http://192.168.101.65:9000/mediafiles/course/test.html

但没有基本的样式: 

在nginx的配置文件中,找到server_name为www.51xuecheng.cn localhost的server配置,然后在其下添加如下配置:

访问下面这个页面:

http://www.51xuecheng.cn/course/test.html

效果如下: 

 

feign远程调用涉及熔断。

微服务雪崩:如果A调B,B调C,假如此时C服务出现问题,此时A和B都会出现问题。

内容管理服务(上游服务)要调用媒资管理服务(下游服务)。

熔断是下游服务异常时一种保护系统的手段。降级是熔断后上游服务处理熔断的方法。

方法1:使用fallback来定义降级的类,无法拿到熔断降级的具体诱因。

方法2:使用fallbackFactory,定义一个MediaServiceClientFallbackFactory继承FallbackFactory<T>,这个T泛型写的是MediaServiceClient这个类。可以拿到熔断的异常信息。

在xuecheng-plus-content-service的feignclient包的MediaServiceClient接口下面,主要完善@FeignClient注解:

@FeignClient(value="media-api",configuration = MultipartSupportConfig.class,fallbackFactory = MediaServiceClientFallbackFactory.class)

 在xuecheng-plus-content-service的feignclient包的MediaServiceClientFallbackFactory接口下面写入如下代码:

  1. @Component
  2. @Slf4j
  3. public class MediaServiceClientFallbackFactory implements FallbackFactory<MediaServiceClient> {
  4. //拿到了熔断的异常信息throwable
  5. @Override
  6. public MediaServiceClient create(Throwable throwable) {
  7. return new MediaServiceClient() {
  8. //发生熔断上传服务调用此方法执行降级逻辑
  9. @Override
  10. public String upload(MultipartFile filedata, String objectName) {
  11. log.debug("远程调用上传文件的接口发生熔断:{}",throwable.toString(),throwable);
  12. return null;
  13. }
  14. };
  15. }
  16. }

结构如下: 

我的xuecheng-plus-content-service的test很奇怪没办法识别到nacos的配置,所以我单独在test的resources的bootstrap.yml中写入如下配置代码:

  1. feign:
  2. hystrix:
  3. enabled: true
  4. circuitbreaker:
  5. enabled: true
  6. hystrix:
  7. command:
  8. default:
  9. execution:
  10. isolation:
  11. thread:
  12. timeoutInMilliseconds: 30000 #熔断超时时间
  13. ribbon:
  14. ConnectTimeout: 60000 #连接超时时间
  15. ReadTimeout: 60000 #读超时时间
  16. MaxAutoRetries: 0 #重试次数
  17. MaxAutoRetriesNextServer: 1 #切换实例的重试次数

测试:首先停掉媒资服务,可以把所有服务都停掉,然后在return null上打断点,如果进入到return null即表示降级成功。 

熔断:下游服务出现问题触发熔断。

4.15 (课程发布)页面静态化任务 P106

在xuecheng-plus-content-service的service的CoursePublishService下新增2个方法的声明:

  1. //课程静态化
  2. public File generateCourseHtml(Long courseId);
  3. //上传课程静态化页面
  4. public void uploadCourseHtml(Long courseId,File file);

在xuecheng-plus-content-service的service的impl的CoursePublishServiceImpl下写入2个方法的实现代码:

  1. @Autowired
  2. CoursePublishService coursePublishService;
  3. @Override
  4. public File generateCourseHtml(Long courseId) {
  5. //配置freemarker,加载模板
  6. Configuration configuration = new Configuration(Configuration.getVersion());
  7. //最终的静态文件
  8. File htmlFile = null;
  9. try{
  10. //得到classpath路径
  11. String classpath = this.getClass().getResource("/").getPath();
  12. //选指定模板路径,classpath下templates下
  13. configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
  14. //设置字符编码
  15. configuration.setDefaultEncoding("utf-8");
  16. //指定模板文件名称
  17. Template template = configuration.getTemplate("course_template.ftl");
  18. //准备数据
  19. CoursePreviewDto coursePreviewInfo = this.coursePublishService.getCoursePreviewInfo(courseId);
  20. Map<String, Object> map = new HashMap<>();
  21. map.put("model", coursePreviewInfo);
  22. //静态化
  23. //参数1:模板,参数2:数据模型
  24. String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
  25. //输入流
  26. InputStream inputStream = IOUtils.toInputStream(html,"utf-8");
  27. htmlFile = File.createTempFile("coursepublish",".html");
  28. //输出流
  29. FileOutputStream outputStream = new FileOutputStream(htmlFile);
  30. IOUtils.copy(inputStream, outputStream);
  31. }catch (Exception ex){
  32. log.error("页面静态化出现问题,课程id:{}",courseId,ex);
  33. ex.printStackTrace();
  34. }
  35. return htmlFile;
  36. }
  37. @Autowired
  38. MediaServiceClient mediaServiceClient;
  39. @Override
  40. public void uploadCourseHtml(Long courseId, File file) {
  41. try {
  42. //file转成MultipartFile
  43. MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);
  44. //远程调用得到返回值
  45. String upload = mediaServiceClient.upload(multipartFile, "course/" + courseId + ".html");
  46. if(upload==null){
  47. log.debug("远程调用走降级逻辑得到上传的结果为null,课程id:{}",courseId);
  48. XueChengPlusException.cast("上传静态文件过程中存在异常");
  49. }
  50. }catch(Exception ex){
  51. ex.printStackTrace();
  52. XueChengPlusException.cast("上传静态文件过程中存在异常");
  53. }
  54. }

把下面这行代码复制粘贴到xuecheng-plus-content-api的ContentApplication的启动类上:

@EnableFeignClients(basePackages={"com.xuecheng.content.feignclient"})

 xuecheng-plus-content-api的pom.xml文件中进行如下配置:

第1步:启动媒资,系统管理服务,网关启动。前端启动。内容服务以断点方式启动。

第2步:在前端提交审核,然后course_publish_pre会有一条记录,我们手动更改为审核通过(status设为202004),记得course_base表中相应记录的audit_status同样也要修改为审核通过。

第3步:任务调度中心的课程发布任务执行器启动。在(xuecheng-plus-content-service的service的jobhandler的CoursePublishTask下)execute下面打上断点。

第4步:点发布,记录会被写入course_publish表和mq_message表(是在content数据库下)。逐步跟进。

第5步:注意执行结束后,mq_message的消息要被删除,最终需要写入mq_message_history表。

4.16 课程搜索 P107

传统搜索方法:先找文章再找词。

全文检索方法:先找词再找文章。首先把词提取出来,创建索引,索引里面都是词,然后拿着词去搜索。

首先要创建索引,然后再搜索。

在虚拟机上已经有elasticsearch和kibana。尚未启动,输入下面4行命令启动:

  1. docker stop elasticsearch
  2. docker stop kibana
  3. docker start elasticsearch
  4. docker start kibana

Elasticsearch中的Index索引相当于MySQL的表

Elasticsearch中的Document文档相当于MySQL的行

Elasticsearch中的Field字段相当于MySQL的列

Elasticsearch中的Mapping字段相当于MySQL的列

解压下面的文件,然后拷贝到项目下面。

这里需要注意配置文件中的内容:

namespace和group要进行更改:

启动搜索工程。

在api-test下面创建xc-media-api,然后写入测试的代码:

课程信息索引同步:

实时性高:1.可以手动编写代码,在service里面同步。2.可以通过Canal实现。

实时性不强:1.MQ,向mysql写数据的时候向mq写入消息,搜索服务监听mq,收到消息后写入索引。存在问题:要保证消息可靠性,在向mq发消息要可靠,mq本身要可靠,服务监听mq也要可靠。代码实现比较复杂。

2.Logstash,开源实时日志分析平台ELK包括Elasticsearch、Kibana、Logstash,其中Logstash负责收集、解析和转换日志信息。可以实现MySQL与Elasticsearch之间数据同步。

3.任务调度,向mysql写数据的时候记录修改记录,开启一个定时任务根据修改记录将数据同步到Elasticsearch。

内容管理调搜索服务。

在xuecheng-plus-content的feignclient这个包下,定义一个SearchServiceClient接口,写如下代码:

  1. @FeignClient(value="search",fallbackFactory = SearchServiceClientFallbackFactory.class)
  2. public interface SearchServiceClient {
  3. @PostMapping("/search/index/course")
  4. public Boolean add(@RequestBody CourseIndex courseIndex);
  5. }

把xuecheng-plus-search的po下的CourseIndex拷贝到xuecheng-plus-content-service的feignclient下。 

 

在xuecheng-plus-content的feignclient这个包下,定义一个SearchServiceClientFallbackFactory接口,写如下代码:

  1. @Slf4j
  2. @Component
  3. public class SearchServiceClientFallbackFactory implements FallbackFactory<SearchServiceClient> {
  4. @Override
  5. public SearchServiceClient create(Throwable throwable) {
  6. return new SearchServiceClient() {
  7. @Override
  8. public Boolean add(CourseIndex courseIndex) {
  9. log.error("添加课程索引发生熔断,索引信息:{},熔断异常:{}",courseIndex,throwable.toString(),throwable);
  10. //走降级了返回false
  11. return false;
  12. }
  13. };
  14. }
  15. }

在xuecheng-plus-content-service的service的jobhandler下的CoursePublishTask的代码如下:

  1. @Slf4j
  2. @Component
  3. public class CoursePublishTask extends MessageProcessAbstract {
  4. @Autowired
  5. CoursePublishService coursePublishService;
  6. @Autowired
  7. SearchServiceClient searchServiceClient;
  8. @Autowired
  9. CoursePublishMapper coursePublishMapper;
  10. //任务调度入口
  11. @XxlJob("CoursePublishJobHandler")
  12. public void coursePublishJobHandler() throws Exception{
  13. //分片参数
  14. int shardIndex = XxlJobHelper.getShardIndex();
  15. int shardTotal = XxlJobHelper.getShardTotal();
  16. //调用抽象类的方法执行任务
  17. process(shardIndex,shardTotal,"course_publish",30,60);
  18. }
  19. //执行课程发布任务的逻辑,如果此方法抛出异常说明任务执行失败
  20. @Override
  21. public boolean execute(MqMessage mqMessage) {
  22. //从mqMessage拿到课程id
  23. Long courseId = Long.parseLong(mqMessage.getBusinessKey1());
  24. //课程静态化上传到minio
  25. generateCourseHtml(mqMessage,courseId);
  26. //向elasticsearch写索引数据
  27. saveCourseIndex(mqMessage,courseId);
  28. //向redis写缓存
  29. //课程静态化上传到minio
  30. //返回true任务完成
  31. return true;
  32. }
  33. //生成课程静态化页面并上传至文件系统
  34. private void generateCourseHtml(MqMessage mqMessage,long courseId){
  35. //消息id
  36. Long taskId = mqMessage.getId();
  37. MqMessageService mqMessageService = this.getMqMessageService();
  38. //做任务幂等性处理
  39. //取出该阶段执行状态
  40. int stageOne = mqMessageService.getStageOne(taskId);
  41. if(stageOne>0){
  42. log.debug("课程静态化任务完成,无须处理...");
  43. return;
  44. }
  45. //开始进行课程静态化,生成html页面
  46. File file = coursePublishService.generateCourseHtml(courseId);
  47. if(file==null){
  48. XueChengPlusException.cast("生成的静态页面为空");
  49. }
  50. //将html上传到minio
  51. coursePublishService.uploadCourseHtml(courseId,file);
  52. //任务处理完成写任务状态为完成
  53. mqMessageService.completedStageOne(taskId);
  54. }
  55. //保存课程索引信息 第二个阶段任务
  56. private void saveCourseIndex(MqMessage mqMessage,long courseId){
  57. //任务id
  58. Long taskId = mqMessage.getId();
  59. MqMessageService mqMessageService = this.getMqMessageService();
  60. //取出第二个阶段状态
  61. int stageTwo = mqMessageService.getStageTwo(taskId);
  62. //任务幂等性处理
  63. if(stageTwo>0){
  64. log.debug("课程索引信息已写入,无需执行...");
  65. return;
  66. }
  67. //查询课程信息,调用搜索服务添加索引接口
  68. //从课程发布表查询课程信息
  69. CoursePublish coursePublish = coursePublishMapper.selectById(courseId);
  70. CourseIndex courseIndex = new CourseIndex();
  71. BeanUtils.copyProperties(coursePublish,courseIndex);
  72. //远程调用
  73. Boolean add = searchServiceClient.add(courseIndex);
  74. if(!add){
  75. XueChengPlusException.cast("远程调用搜索服务添加课程索引失败");
  76. }
  77. //完成本阶段的任务
  78. mqMessageService.completedStageTwo(courseId);
  79. }
  80. }

把GatewayApplication和ContentApplication和SearchApplication和SystemApplication和MediaApplication启动。

可以看到一些初始数据:

测试:

第1步:我拿最后一条数据做试验,先把图片上传上去。

第2步:然后点击提交审核。然后course_publish_pre会有一条记录,我们手动更改为审核通过(status设为202004),记得course_base表中相应记录的audit_status同样也要修改为审核通过。

第4步:点发布。可以看到新增了这门课。

 五、认证授权模块

5.1 SpringSecurity认证授权测试 P108

什么是用户身份认证?用户去访问系统资源时要求验证用户的身份信息,身份合法即可继续访问。

常见的用户身份认证方式:用户名密码、微信扫码等。

项目包含:学生、学习机构老师、平台运营人员三类用户,每一类用户在访问项目受保护资源时都需要进行身份认证。

什么是用户授权?用户认证通过后去访问系统资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源无法访问,这个过程叫用户授权。

微信扫码和QQ扫码能方便用户登录(省去了用户注册的成本),能够对资源进行共享。

认证功能几乎是每个项目都要具备的功能,并且它与业务无关,市面上有很多的认证框架。Spring Security。

把资料中的xuecheng-plus-search解压后拷贝到项目当中,更改bootstrap.yml配置文件中的如下内容:

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.cloud</groupId>
  7. <artifactId>spring-cloud-starter-oauth2</artifactId>
  8. </dependency>

出现下面问题: 

class lombok.javac.apt.LombokProcessor (in unnamed module @0x6002e944) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module @0x6002e944 

解决方法:把xuecheng-plus-parent的lombok版本调高。

访问localhost:63070/auth/user/52效果如下:

访问localhost:63070/auth/login

在登录界面输入用户名:zhangsan,密码:123,登录成功。反之提示登录失败。

加入如下注解:

  1. @PreAuthorize("hasAuthority('p1')")
  2. @PreAuthorize("hasAuthority('p2')")

重新启动项目,效果如下:

下面logout:

点击Log Out,然后输入用户名:lisi,密码:456。

授权:把权限赋予不同类别的用户,判断谁有权限,有权限就能访问资源。

过滤器是在请求到达之前的预处理或者后处理,拦截器是对方法调用的前后或者抛出异常时的处理,监听器是监听特定事件执行相应操作

5.2 OAuth2协议测试 P109

现在的问题是:一个新用户在网站没有信息,如果要注册需要填写一大堆的个人信息,如果可以通过扫码获取信息,会比较方便。

流程:客户端向用户申请到授权码,用户同意后客户端携带授权码去获取令牌,获取到令牌客户端携带令牌去获取用户信息。

 

把下面这个bean复制到WebSecurityConfig中

  1. @Bean
  2. public AuthenticationManager authenticationManagerBean() throws Exception {
  3. return super.authenticationManagerBean();
  4. }

重新启动项目。 如果启动失败注意一定要用老师配套的jdk1.8,在第1天的资料中。

在api-test包下面创建一个xc-auth-api.http文件,

获取授权码,输入下面的链接:http://localhost:63070/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn。用户名:zhangsan,密码:123。选择Approve。

可以获取到授权码:

把授权码填在请求的url中

POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=w8QpqV&redirect_uri=http://www.51xuecheng.cn

密码模式:

POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123

5.3 jwt令牌 P110

现在的问题是:认证服务发的令牌,资源服务要每次请求认证服务才能拿到令牌。

现在想解决上面的问题,可以使用JWT格式的令牌解决上面的问题。

jwt令牌包含3个方面:Header、Payload、Signature

Header里面包含算法类型。Payload里面是内容,不建议存放敏感信息,因为可以被解码还原为原始内容。前2部分用的编码方式都是base64url。

Signature是签名,用于防止jwt内容被篡改。只要秘钥不泄露,就无法篡改后不被知觉。

现在资源服务只需要获得秘钥,就能验证JWT。

如果认证服务和资源服务使用相同秘钥,叫作对称加密,效率高,秘钥泄露可以伪造jwt令牌。

测试的时候只需要把TokenConfig的配置全部换成下面的配置:

重新启动项目,点击密码模式登录,头、载体、签名之间用.号分割。

5.4 资源服务继承JWT P111

逻辑:客户端访问门户,通过统一认证入口,请求统一认证服务,如果认证成功,将jwt令牌颁发给客户端,客户端携带jwt令牌才能去访问教学管理、选课学习、运营管理这些模块。

首先让微服务整合spring security管控所有的资源。

把spring security的依赖加入到xuecheng-plus-content-api。

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.cloud</groupId>
  7. <artifactId>spring-cloud-starter-oauth2</artifactId>
  8. </dependency>

将TokenConfig和ResourceServiceConfig拷贝到content下的config下,config要新建。

ResourceServiceConfig的RESOURCE_ID不要乱变要和前面的一致。

要管控的文件全部在这里配(注意老师给的初始文件,配置这里是注释掉的):

启动content服务。

访问下面的会提示说请求不到:

正常时能访问到。加入jwt后访问不到:

 

先用Auth的方法获取到令牌,加上令牌后访问就能获取到资源。

 

5.5 网关认证 P112

网关的职责:一个是路由转发,一个是进行校验。

针对认证:1.网站白名单维护,针对不用认证的URL全部放行。2.校验jwt的合法性。校验jwt的合法性,jwt合法则放行,不合法则阻塞。

在网关工程添加如下依赖:

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.cloud</groupId>
  7. <artifactId>spring-cloud-starter-oauth2</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.projectlombok</groupId>
  11. <artifactId>lombok</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>com.alibaba</groupId>
  15. <artifactId>fastjson</artifactId>
  16. </dependency>

把资料中的文件全部拷贝到网关服务中,格式如下: 

 

在security-whitelist.properties中配置的是白名单:

如下是白名单的位置:

记得先把临时全部放行那条注释掉:

启动gateway、auth、content服务。

在xuecheng-plus-content-api中把config下的ResourceServerConfig给屏蔽掉:

注意事项:1.要在网关添加依赖。2.读懂网关的过滤器。3.要配置白名单。4.在微服务要放行所有。

可以在网关的如下位置打断点进行跟踪调试:

访问如下的请求:

可以看到请求请求到的token

5.6 连接用户数据库 P113

首先要把xuecheng-plus-auth的config下的WebSecurityConfig这个类的配置用户信息服务部分给注释起来。

在xuecheng-plus-auth的ucenter下面创建service/impl包,然后创建一个UserServiceImpl类,写入如下代码:

  1. @Slf4j
  2. @Component
  3. public class UserServiceImpl implements UserDetailsService {
  4. @Autowired
  5. XcUserMapper xcUserMapper;
  6. @Override
  7. public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
  8. //账号
  9. String username = s;
  10. //根据username账号查询数据库
  11. XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s));
  12. //查询到用户不存在,返回null即可
  13. if(xcUser==null){
  14. return null;
  15. }
  16. //如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由
  17. String password = xcUser.getPassword();
  18. //权限
  19. String[] authorities = {"test"};
  20. UserDetails userDetails = User.withUsername(username).password(password).authorities(authorities).build();
  21. return userDetails;
  22. }
  23. }

在WebSecurityConfig中写入如下代码:

  1. public static void main(String[] args) {
  2. String password="111111";
  3. PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
  4. //生成密码
  5. for(int i=0;i<5;i++){
  6. String encode = passwordEncoder.encode(password);
  7. System.out.println(encode);
  8. }
  9. }

执行后,可以看到生成的每个串都不一样:

可以看到代码的每次比对都是一致的:

测试代码如下:

  1. public static void main(String[] args) {
  2. String password="111111";
  3. PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
  4. //生成密码
  5. for(int i=0;i<5;i++){
  6. String encode = passwordEncoder.encode(password);
  7. System.out.println(encode);
  8. boolean matches = passwordEncoder.matches(password, encode);
  9. System.out.println(matches);
  10. }
  11. }

修改下面这个地方: 

数据库如下: 

测试:在如下位置打断点

当用户名为zhangsan时因为数据库里没有匹配的所以失败

接下来把username改为t1,然后密码改为111111,可以正常获取到token。

5.7 扩展用户信息 P114

用户的相关信息需要记录到令牌,像昵称,头像,用户id都要记录进去,所以需要扩展用户的信息。

现在采用的方案是不仅存放username,同时还存放一大堆的东西进去。

在api-test包下的xc-auth-api.http下写入如下代码,token后面填写密码模式获取到的token

  1. ###校验jwt令牌
  2. POST {{auth_host}}/auth/oauth/check_token?token=

可以看到json解析出来的内容就很多了! 

5.8 工具类获取用户身份 P115

在xuecheng-plus-content-api的content下面建一个util,在util下面创建一个SecurityUtil类,把代码全部拷贝进去:

在CourseBaseInfoController中,现在可以直接通过SecurityUtil.getUser()来获取用户的信息。

重新启动content服务,以断点调试的方式启动。然后在xc-content-api.http中执行下面的方法:

可以看到获取到了user的信息。

5.9 统一认证入口 P116

统一认账入口,包括:账号密码认证,微信扫码认证,手机验证码认证。

首先要统一请求的参数,建了一个统一的认证类统一认证请求的参数统一为AuthParamsDto。

第1步:是对xuecheng-plus-auth的ucenter的service的impl下的UserServiceImpl类的代码进行修改,主要是将传入的json转成AuthParamsDto对象,在loadUserByUsername的开头写入如下代码:

  1. //将传入的json转成AuthParamsDto对象
  2. AuthParamsDto authParamsDto = null;
  3. try {
  4. authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
  5. } catch (Exception e) {
  6. throw new RuntimeException("请求认证的参数不符合要求");
  7. }
  8. //账号
  9. String username = authParamsDto.getUsername();

第2步:在api-test下的xc-auth-api.http中,添加如下的代码:

  1. ### 密码模式,请求AuthParamsDto参数
  2. POST {{auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username={"username":"t1","password":"111111","authType":"password"}

第3步:在xuecheng-plus-auth的config下创建DaoAuthenticationProviderCustom下面,加入下面的代码:

  1. //重写了DaoAuthenticationProvider的校验密码的方法,因为我们统一了认证入口,有一些认证方式不需要校验密码
  2. @Component
  3. public class DaoAuthenticationProviderCustom extends DaoAuthenticationProvider {
  4. @Autowired
  5. public void setUserDetailsService(UserDetailsService userDetailsService){
  6. super.setUserDetailsService(userDetailsService);
  7. }
  8. @Override
  9. protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  10. //子类重写父类方法,为空就好
  11. }
  12. }

不是所有的验证都要校验密码,手机验证码就不需要校验密码。

第4步:进入到config下的WebSecurityConfig中,写入如下代码:

  1. @Autowired
  2. DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;
  3. @Override
  4. protected void configure(AuthenticationManagerBuilder auth)throws Exception{
  5. auth.authenticationProvider(daoAuthenticationProviderCustom);
  6. }

测试:在下面的位置打上断点

第5步:在xuecheng-plus-auth的service下创建AuthService

  1. //统一的认证接口
  2. public interface AuthService {
  3. //认证方法
  4. XcUserExt execute(AuthParamsDto authParamsDto);
  5. }

第6步:策略模式,根据不同的认证方式进来有不同的策略。

  1. //账号名密码方式
  2. @Service("password_authservice")
  3. public class PasswordAuthServiceImpl implements AuthService {
  4. @Override
  5. public XcUserExt execute(AuthParamsDto authParamsDto) {
  6. return null;
  7. }
  8. }
  1. //微信扫码认证
  2. @Service("wx_authservice")
  3. public class WxAuthServiceImpl implements AuthService {
  4. @Override
  5. public XcUserExt execute(AuthParamsDto authParamsDto) {
  6. return null;
  7. }
  8. }

第7步:可以在UserServiceImpl中根据认证类型从spring容器中取出指定的bean。在如下位置添加如下代码:

  1. //认证类型,有password,wx...
  2. String authType = authParamsDto.getAuthType();
  3. //根据认证类型从spring容器中取出指定的bean
  4. String beanName = authType+"_authservice";
  5. AuthService authService = applicationContext.getBean(beanName,AuthService.class);
  6. //调用
  7. XcUserExt execute = authService.execute(authParamsDto);
  1. @Autowired
  2. ApplicationContext applicationContext;

5.10 统一账号密码认证 P117

在xuecheng-plus-auth的service的impl下的PasswordAuthServiceImpl中,写入如下代码:

  1. //账号名密码方式
  2. @Service("password_authservice")
  3. public class PasswordAuthServiceImpl implements AuthService {
  4. @Autowired
  5. XcUserMapper xcUserMapper;
  6. @Autowired
  7. PasswordEncoder passwordEncoder;
  8. @Override
  9. public XcUserExt execute(AuthParamsDto authParamsDto) {
  10. //账号
  11. String username = authParamsDto.getUsername();
  12. //todo:校验验证码
  13. //账号是否存在,根据username账号查询数据库
  14. XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername,username));
  15. //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
  16. if(xcUser==null){
  17. throw new RuntimeException("账号不存在");
  18. }
  19. //校验密码是否正确
  20. //如果查到了用户拿到正确的密码
  21. String passwordDb = xcUser.getPassword();
  22. //拿到用户输入的密码
  23. String passwordForm = authParamsDto.getPassword();
  24. //校验密码
  25. boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
  26. if(!matches){
  27. throw new RuntimeException("账号或密码错误");
  28. }
  29. XcUserExt xcUserExt = new XcUserExt();
  30. BeanUtils.copyProperties(xcUser,xcUserExt);
  31. return xcUserExt;
  32. }
  33. }

在xuecheng-plus-auth的service的impl下的UserServiceImpl中写入如下代码:

  1. @Slf4j
  2. @Component
  3. public class UserServiceImpl implements UserDetailsService {
  4. @Autowired
  5. XcUserMapper xcUserMapper;
  6. @Autowired
  7. ApplicationContext applicationContext;
  8. @Override
  9. public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
  10. //将传入的json转成AuthParamsDto对象
  11. AuthParamsDto authParamsDto = null;
  12. try {
  13. authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
  14. } catch (Exception e) {
  15. throw new RuntimeException("请求认证的参数不符合要求");
  16. }
  17. //认证类型,有password,wx...
  18. String authType = authParamsDto.getAuthType();
  19. //根据认证类型从spring容器中取出指定的bean
  20. String beanName = authType+"_authservice";
  21. AuthService authService = applicationContext.getBean(beanName,AuthService.class);
  22. //调用统一execute方法完成认证
  23. XcUserExt xcUserExt = authService.execute(authParamsDto);
  24. //封装xcUserExt用户信息为UserDetails
  25. UserDetails userPrincipal = getUserPrincipal(xcUserExt);
  26. return userPrincipal;
  27. }
  28. //查询用户信息
  29. public UserDetails getUserPrincipal(XcUserExt xcUser){
  30. String password = xcUser.getPassword();
  31. //权限
  32. String[] authorities = {"test"};
  33. xcUser.setPassword(null);
  34. String userJson = JSON.toJSONString(xcUser);
  35. UserDetails userDetails = User.withUsername(userJson).password(password).authorities(authorities).build();
  36. return userDetails;
  37. }
  38. }

测试:在xc-auth-api.http发送如下请求:

在下面位置打上断点,逐步跟踪,看看最终结果是否正确。

5.11 部署验证码服务 P118

验证码可以防止恶性攻击。具有认证、找回密码、支付验证、人机判断等功能。

解压下面的zip包,然后拷贝到项目当中:

 修改yaml配置文件中的配置:

在nacos中配置redis:

在api-test下创建xc-checkcode-api.http,写入如下代码:

  1. ### 申请验证码
  2. POST {{checkcode_host}}/checkcode/pic

生成的效果如下: 

key会被存储在redis中:

复制图片链接,打开即可看到校验码:

发送下面的代码来看是否正确: 

  1. ### 校验验证码
  2. POST {{checkcode_host}}/checkcode/verify?key=checkcode:2d6c2fa5124641fc83339a6e16718cbc&code=bwst

5.12 账号密码认证测试 P119

启动网关和验证码服务,redis也要启动,记得前端的nginx和项目的serve也要启动!

然后打开前端的登录页面,是401状态。可以到网关服务的security-whitelist.properties中临时把所有页面放行:

可以看到验证码出现:

然后可以将xuecheng-plus-auth服务也断点启动,在如下位置打上断点:

如果出现下面的问题:

输入用户名、密码、验证码后点击确定,跳转到下面:

在xuecheng-plus-auth的ucenter下面创建feignclient包。在feignclient包下创建CheckCodeClient接口,写入如下代码:

  1. @FeignClient(value = "checkcode",fallbackFactory = CheckCodeClientFactory.class)
  2. @RequestMapping("/checkcode")
  3. public interface CheckCodeClient {
  4. @PostMapping(value="/verify")
  5. public Boolean verify(@RequestParam("key") String key, @RequestParam("code") String code);
  6. }

同样在feignclient包系创建CheckCodeClientFactory类,写入如下代码:

  1. @Slf4j
  2. @Component
  3. public class CheckCodeClientFactory implements FallbackFactory<CheckCodeClient> {
  4. @Override
  5. public CheckCodeClient create(Throwable throwable) {
  6. return new CheckCodeClient() {
  7. @Override
  8. public Boolean verify(String key, String code) {
  9. log.debug("调用验证码服务熔断异常:{}",throwable.getMessage());
  10. return null;
  11. }
  12. };
  13. }
  14. }

 在bootstrap.yml中写入如下配置:

完善之后的PasswordAuthServiceImpl代码如下:

  1. //账号名密码方式
  2. @Service("password_authservice")
  3. public class PasswordAuthServiceImpl implements AuthService {
  4. @Autowired
  5. XcUserMapper xcUserMapper;
  6. @Autowired
  7. PasswordEncoder passwordEncoder;
  8. @Autowired
  9. CheckCodeClient checkCodeClient;
  10. @Override
  11. public XcUserExt execute(AuthParamsDto authParamsDto) {
  12. //账号
  13. String username = authParamsDto.getUsername();
  14. //输入的验证码
  15. String checkcode = authParamsDto.getCheckcode();
  16. //验证码对应的key
  17. String checkcodekey = authParamsDto.getCheckcodekey();
  18. if(StringUtils.isEmpty(checkcode)||StringUtils.isEmpty(checkcode)){
  19. throw new RuntimeException("请输入验证码");
  20. }
  21. //远程调用验证码服务接口去校验验证码
  22. Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
  23. if(verify == null || !verify){
  24. throw new RuntimeException("验证码输入错误");
  25. }
  26. //账号是否存在,根据username账号查询数据库
  27. XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername,username));
  28. //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
  29. if(xcUser==null){
  30. throw new RuntimeException("账号不存在");
  31. }
  32. //校验密码是否正确
  33. //如果查到了用户拿到正确的密码
  34. String passwordDb = xcUser.getPassword();
  35. //拿到用户输入的密码
  36. String passwordForm = authParamsDto.getPassword();
  37. //校验密码
  38. boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
  39. if(!matches){
  40. throw new RuntimeException("账号或密码错误");
  41. }
  42. XcUserExt xcUserExt = new XcUserExt();
  43. BeanUtils.copyProperties(xcUser,xcUserExt);
  44. return xcUserExt;
  45. }
  46. }

在下面打上断点: 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/280672?site
推荐阅读
相关标签
  

闽ICP备14008679号