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

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

四、课程发布模块

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

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

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

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

4.2 (课程发布)freemarker P93

freemarker是模板引擎。

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

复制代码
<!-- Spring Boot 对结果视图 Freemarker 集成 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

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

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

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

复制代码
@Slf4j
@RestController
public class FreemarkerController {
    @GetMapping("/testfreemarker")
    public ModelAndView test(){
        ModelAndView modelAndView = new ModelAndView();
        //指定模型
        modelAndView.addObject("name","小明");
        //指定模板
        modelAndView.setViewName("test");//根据视图名称加.ftl找到模板
        return modelAndView;
    }
}

测试没啥问题:

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中写入如下门户地址:

完整的配置如下:

复制代码
server {
    listen       80;
    server_name  www.51xuecheng.cn localhost;
    #rewrite ^(.*) https://$server_name$1 permanent;
    #charset koi8-r;
    ssi on;
    ssi_silent_errors on;
    #access_log  logs/host.access.log  main;

    location / { 
        alias   C:/xuechengzaixian/xc-ui-pc-static-portal/;
        index  index.html index.htm;
    }
    #静态资源
    location /static/img/ {  
            alias  C:/xuechengzaixian/xc-ui-pc-static-portal/img/;
    } 
    location /static/css/ {  
            alias   C:/xuechengzaixian/xc-ui-pc-static-portal/css/;
    } 
    location /static/js/ {  
            alias   C:/xuechengzaixian/xc-ui-pc-static-portal/js/;
    } 
    location /static/plugins/ {  
            alias   C:/xuechengzaixian/xc-ui-pc-static-portal/plugins/;
            add_header Access-Control-Allow-Origin http://ucenter.51xuecheng.cn;  
            add_header Access-Control-Allow-Credentials true;  
            add_header Access-Control-Allow-Methods GET;
    } 
    location /plugins/ {  
            alias   C:/xuechengzaixian/xc-ui-pc-static-portal/plugins/;
    }
}

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

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

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

复制代码
upstream fileserver{
    server 192.168.101.65:9000 weight=10;
}

server {
    listen       80;
    server_name  file.51xuecheng.cn;
    #charset koi8-r;
    ssi on;
    ssi_silent_errors on;
    #access_log  logs/host.access.log  main;
    location /video {
        proxy_pass   http://fileserver;
    }

    location /mediafiles {
        proxy_pass   http://fileserver;
    }
}

效果如下:

下面测试一下:

复制代码
http://file.51xuecheng.cn/mediafiles/2022/09/13/a16da7a132559daf9e1193166b3e7f52.jpg

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

复制代码
        location /course/preview/learning.html {
                alias D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal/course/learning.html;
        } 
        location /course/search.html {  
                root   D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal;
        } 
        location /course/learning.html {  
                root   D:/itcast2022/xc_edu3.0/code_1/xc-ui-pc-static-portal;
        } 

进入到播放详情页面:

复制代码
http://www.51xuecheng.cn/course/course_template.html

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

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

完整配置如下:

复制代码
worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    upstream fileserver{
        server 192.168.101.65:9000 weight=10;
    }
    server {
        listen       80;
        server_name  file.51xuecheng.cn;
        ssi on;
        ssi_silent_errors on;
        location /video {
            proxy_pass   http://fileserver;
        }

        location /mediafiles {
            proxy_pass   http://fileserver;
        }
    }
    server {
        listen       80;
        server_name  www.51xuecheng.cn localhost;
        ssi on;
        ssi_silent_errors on;

        location / { 
            alias   C:/xuechengzaixian/xc-ui-pc-static-portal/;
            index  index.html index.htm;
        }
        location /static/img/ {  
                alias  C:/xuechengzaixian/xc-ui-pc-static-portal/img/;
        } 
        location /static/css/ {  
                alias   C:/xuechengzaixian/xc-ui-pc-static-portal/css/;
        } 
        location /static/js/ {  
                alias   C:/xuechengzaixian/xc-ui-pc-static-portal/js/;
        } 
        location /static/plugins/ {  
                alias   C:/xuechengzaixian/xc-ui-pc-static-portal/plugins/;
                add_header Access-Control-Allow-Origin http://ucenter.51xuecheng.cn;  
                add_header Access-Control-Allow-Credentials true;  
                add_header Access-Control-Allow-Methods GET;
        } 
        location /plugins/ {  
                alias   C:/xuechengzaixian/xc-ui-pc-static-portal/plugins/;
        }
        location /course/preview/learning.html {
                alias C:/xuechengzaixian/xc-ui-pc-static-portal/course/learning.html;
        } 
        location /course/search.html {  
                root   C:/xuechengzaixian/xc-ui-pc-static-portal;
        } 
        location /course/learning.html {  
                root   C:/xuechengzaixian/xc-ui-pc-static-portal;
        } 
    }
}

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

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

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

复制代码
@Controller
public class CoursePublishController {
    @Autowired
    CoursePublishService coursePublishService;
    @GetMapping("/coursepreview/{courseId}")
    public ModelAndView preview(@PathVariable("courseId")Long courseId){
        ModelAndView modelAndView = new ModelAndView();
        //查询课程的信息作为模板数据
        CoursePreviewDto coursePreviewDto = coursePublishService.getCoursePreviewInfo(courseId);
        modelAndView.addObject("model",coursePreviewDto);
        modelAndView.setViewName("course_template");
        return modelAndView;
    }
}

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

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

复制代码
http://localhost:63040/content/coursepreview/12

会出现下面界面:

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

-

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

复制代码
upstream gatewayserver{
    server 127.0.0.1:63010 weight=10;
}
#api
location /api/ {
    proxy_pass   http://gatewayserver/;
} 

解析:比如现在我输入: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,然后写入如下代码:

复制代码
@Data
public class CoursePreviewDto {
    //课程基本信息,营销信息
    private CourseBaseInfoDto courseBase;
    //课程计划信息
    private List<TeachplanDto> teachplans;
    //课程师资信息..
}

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

复制代码
//课程发布相关接口
public interface CoursePublishService {
    /**
     * @param courseId 课程id
     * @return
     */
    public CoursePreviewDto getCoursePreviewInfo(Long courseId);
}

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

复制代码
//课程发布相关接口实现
@Slf4j
@Service
public class CoursePublishServiceImpl implements CoursePublishService {
    @Autowired
    CourseBaseInfoService courseBaseInfoService;
    @Autowired
    TeachplanService teachplanService;
    @Override
    public CoursePreviewDto getCoursePreviewInfo(Long courseId) {
        CoursePreviewDto coursePreviewDto = new CoursePreviewDto();
        //课程基本信息,营销信息
        CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
        //课程计划信息
        List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
        coursePreviewDto.setTeachplans(teachplanTree);
        return coursePreviewDto;
    }
}

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

访问下面的地址:

复制代码
http://localhost:8601/

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

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

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

举例修改下面2个地方:

有一个现成的course_template.ftl文件

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

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

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

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

复制代码
@Api(value = "课程公开查询接口",tags = "课程公开查询接口")
@RestController
@RequestMapping("/open")
public class CourseOpenController {
    @Autowired
    private CourseBaseInfoService courseBaseInfoService;
    @Autowired
    private CoursePublishService coursePublishService;
    @GetMapping("/course/whole/{courseId}")
    public CoursePreviewDto getPreviewInfo(@PathVariable("courseId") Long courseId) {
        //获取课程预览信息
        CoursePreviewDto coursePreviewInfo = coursePublishService.getCoursePreviewInfo(courseId);
        return coursePreviewInfo;
    }
}

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

复制代码
@Api(value = "媒资文件管理接口",tags = "媒资文件管理接口")
@RestController
@RequestMapping("/open")
public class MediaOpenController {
    @Autowired
    MediaFileService mediaFileService;
    @ApiOperation("预览文件")
    @GetMapping("/preview/{mediaId}")
    public RestResponse<String> getPlayUrlByMediaId(@PathVariable String mediaId){
        MediaFiles mediaFiles = mediaFileService.getFileById(mediaId);
        if (mediaFiles==null) {
            return RestResponse.validfail("找不到视频");
        }
        String url = mediaFiles.getUrl();
        if (StringUtils.isEmpty(url)) {
            return RestResponse.validfail("该视频正在处理中");
        }
        return RestResponse.success(mediaFiles.getUrl());
    }
}

启动下面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下写入如下代码:

复制代码
@Override
public void commitAudit(Long companyId, Long courseId) {
    CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
    if(courseBaseInfo == null){
        XueChengPlusException.cast("课程找不到");
    }
    //审核状态
    String auditStatus = courseBaseInfo.getAuditStatus();
    //如果课程的审核状态为已提交则不允许提交
    if(auditStatus.equals("202003")){
        XueChengPlusException.cast("课程已提交请等待审核");
    }
    //课程的图片、计划信息没有填写也不允许提交
    String pic = courseBaseInfo.getPic();
    if(StringUtils.isEmpty(pic)){
        XueChengPlusException.cast("请求上传课程图片");
    }
    //查询课程计划
    //课程计划信息
    List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
    if(teachplanTree==null || teachplanTree.size()==0){
        XueChengPlusException.cast("请编写课程计划");
    }
    //查询到课程基本信息、营销信息。计划等信息插入到课程预发布表
    CoursePublishPre coursePublishPre = new CoursePublishPre();
    BeanUtils.copyProperties(courseBaseInfo,coursePublishPre);
    //营销信息
    CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
    //转JSON
    String courseMarketJson = JSON.toJSONString(courseMarket);
    coursePublishPre.setMarket(courseMarketJson);
    //计划信息
    //转json
    String teachplanTreeJson = JSON.toJSONString(teachplanTree);
    coursePublishPre.setTeachplan(teachplanTreeJson);
    //状态为已提交
    coursePublishPre.setStatus("202003");
    //提交时间
    coursePublishPre.setCreateDate(LocalDateTime.now());
    //查询预发布表,如果有记录则更新,没有则插入
    CoursePublishPre coursePublishPreObj = coursePublishPreMapper.selectById(courseId);
    if(coursePublishPreObj==null){
        //插入
        coursePublishPreMapper.insert(coursePublishPre);
    }else{
        //更新
        coursePublishPreMapper.updateById(coursePublishPre);
    }
    //更新课程基本信息表的审核状态为已提交
    CourseBase courseBase = courseBaseMapper.selectById(courseId);
    courseBase.setAuditStatus("202003");//审核状态为已提交
    courseBaseMapper.updateById(courseBase);
}

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

复制代码
CourseCategory mtObj = courseCategoryMapper.selectById(courseBase.getMt());
String mtName = mtObj.getName();//大分类名称
courseBaseInfoDto.setMtName(mtName);
CourseCategory stObj = courseCategoryMapper.selectById(courseBase.getSt());
String stName = stObj.getName();//小分类名称
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文件中写入依赖:

复制代码
<dependency>
    <groupId>com.xuecheng</groupId>
    <artifactId>xuecheng-plus-message-sdk</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

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

复制代码
@ApiOperation("课程发布")
@ResponseBody
@PostMapping("/coursepublish/{courseId}")
public void coursepublish(@PathVariable("courseId") Long courseId){
    Long companyId = 1232141425L;
    coursePublishService.publish(companyId,courseId);
}

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

复制代码
public void publish(Long companyId,Long courseId);

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

复制代码
@Autowired
CoursePublishMapper coursePublishMapper;
@Autowired
MqMessageService mqMessageService;
@Transactional
@Override
public void publish(Long companyId, Long courseId) {
    //查询预发布表
    CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
    if(coursePublishPre==null){
        XueChengPlusException.cast("课程没有审核记录,无法发布");
    }
    //状态
    String status = coursePublishPre.getStatus();
    //课程如果没有审核通过不允许发布
    if(!status.equals("202004")){
        XueChengPlusException.cast("课程没有审核通过不允许发布");
    }
    //向课程发布表写入数据
    CoursePublish coursePublish = new CoursePublish();
    BeanUtils.copyProperties(coursePublishPre,coursePublish);
    //先查询课程发布表,有则更新,没有再添加
    CoursePublish coursePublishObj = coursePublishMapper.selectById(courseId);
    if(coursePublishObj==null){
        coursePublishMapper.insert(coursePublish);
    }else{
        coursePublishMapper.updateById(coursePublish);
    }
    //向消息表写入数据
    MqMessage mqMessage = mqMessageService.addMessage("course_publish", String.valueOf(courseId), null, null);
    if(mqMessage==null){
        XueChengPlusException.cast(CommonError.UNKOWN_ERROR);
    }

    //将预发布表数据删除
    coursePublishPreMapper.deleteById(courseId);
}

启动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下面写入依赖:

复制代码
<dependency>
      <groupId>com.xuxueli</groupId>
      <artifactId>xxl-job-core</artifactId>
</dependency>

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

复制代码
xxl:
  job:
    admin: 
      addresses: http://192.168.101.65:8088/xxl-job-admin
    executor:
      appname: coursepublish-job
      address: 
      ip: 
      port: 8999
      logpath: /data/applogs/xxl-job/jobhandler
      logretentiondays: 30
    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中添加如下依赖:

复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

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

复制代码
@SpringBootTest
public class FreemarkerTests {
    @Autowired
    CoursePublishService coursePublishService;
    //测试页面静态化
    @Test
    public void testGenerateHtmlByTemplate() throws IOException, TemplateException {
        //配置freemarker
        Configuration configuration = new Configuration(Configuration.getVersion());
        //加载模板
        //选指定模板路径,classpath下templates下
        //得到classpath路径
        //String classpath = this.getClass().getResource("/").getPath();
        String classpath = "C:\\xuechengzaixian\\xuecheng-plus-project\\xuecheng-plus-content\\xuecheng-plus-content-service\\target\\test-classes";
        configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
        //设置字符编码
        configuration.setDefaultEncoding("utf-8");
        //指定模板文件名称
        Template template = configuration.getTemplate("course_template.ftl");
        //准备数据
        CoursePreviewDto coursePreviewInfo = coursePublishService.getCoursePreviewInfo(2L);
        Map<String, Object> map = new HashMap<>();
        map.put("model", coursePreviewInfo);
        //静态化
        //参数1:模板,参数2:数据模型
        String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
        System.out.println(content);
        //将静态化内容输出到文件中
        InputStream inputStream = IOUtils.toInputStream(content);
        //输出流
        FileOutputStream outputStream = new FileOutputStream("C:\\software\\test.html");
        IOUtils.copy(inputStream, outputStream);
    }
}

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

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

复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud 微服务远程调用 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
<!--feign支持Multipart格式传参-->
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.8.0</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form-spring</artifactId>
    <version>3.8.0</version>
</dependency>

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

复制代码
feign:
  hystrix:
    enabled: true
  circuitbreaker:
    enabled: true
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000  #熔断超时时间
ribbon:
  ConnectTimeout: 60000 #连接超时时间
  ReadTimeout: 60000 #读超时时间
  MaxAutoRetries: 0 #重试次数
  MaxAutoRetriesNextServer: 1 #切换实例的重试次数

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

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

复制代码
shared-configs:
  - data-id: feign-${spring.profiles.active}.yaml
    group: xuecheng-plus-common
    refresh: true

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

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

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

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

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

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

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

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

复制代码
@SpringBootTest
public class FeignUploadTest {
    @Autowired
    MediaServiceClient mediaServiceClient;
    @Test
    public void test() {
        //将file转MultipartFile
        File file = new File("C:\\software\\test.html");
        MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);
        mediaServiceClient.upload(multipartFile,"course/test.html");
    }
}

在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接口下面写入如下代码:

复制代码
@Component
@Slf4j
public class MediaServiceClientFallbackFactory implements FallbackFactory<MediaServiceClient> {
    //拿到了熔断的异常信息throwable
    @Override
    public MediaServiceClient create(Throwable throwable) {
        return new MediaServiceClient() {
            //发生熔断上传服务调用此方法执行降级逻辑
            @Override
            public String upload(MultipartFile filedata, String objectName) {
                log.debug("远程调用上传文件的接口发生熔断:{}",throwable.toString(),throwable);
                return null;
            }
        };
    }
}

结构如下:

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

复制代码
feign:
  hystrix:
    enabled: true
  circuitbreaker:
    enabled: true
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000  #熔断超时时间
ribbon:
  ConnectTimeout: 60000 #连接超时时间
  ReadTimeout: 60000 #读超时时间
  MaxAutoRetries: 0 #重试次数
  MaxAutoRetriesNextServer: 1 #切换实例的重试次数

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

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

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

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

复制代码
//课程静态化
public File generateCourseHtml(Long courseId);
//上传课程静态化页面
public void uploadCourseHtml(Long courseId,File file);

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

复制代码
@Autowired
CoursePublishService coursePublishService;
@Override
public File generateCourseHtml(Long courseId) {
    //配置freemarker,加载模板
    Configuration configuration = new Configuration(Configuration.getVersion());
    //最终的静态文件
    File htmlFile = null;
    try{
        //得到classpath路径
        String classpath = this.getClass().getResource("/").getPath();
        //选指定模板路径,classpath下templates下
        configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
        //设置字符编码
        configuration.setDefaultEncoding("utf-8");
        //指定模板文件名称
        Template template = configuration.getTemplate("course_template.ftl");
        //准备数据
        CoursePreviewDto coursePreviewInfo = this.coursePublishService.getCoursePreviewInfo(courseId);
        Map<String, Object> map = new HashMap<>();
        map.put("model", coursePreviewInfo);
        //静态化
        //参数1:模板,参数2:数据模型
        String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
        //输入流
        InputStream inputStream = IOUtils.toInputStream(html,"utf-8");
        htmlFile = File.createTempFile("coursepublish",".html");
        //输出流
        FileOutputStream outputStream = new FileOutputStream(htmlFile);
        IOUtils.copy(inputStream, outputStream);
    }catch (Exception ex){
        log.error("页面静态化出现问题,课程id:{}",courseId,ex);
        ex.printStackTrace();
    }
    return htmlFile;
}
@Autowired
MediaServiceClient mediaServiceClient;
@Override
public void uploadCourseHtml(Long courseId, File file) {
    try {
        //将file转成MultipartFile
        MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);
        //远程调用得到返回值
        String upload = mediaServiceClient.upload(multipartFile, "course/" + courseId + ".html");
        if(upload==null){
            log.debug("远程调用走降级逻辑得到上传的结果为null,课程id:{}",courseId);
            XueChengPlusException.cast("上传静态文件过程中存在异常");
        }
    }catch(Exception ex){
        ex.printStackTrace();
        XueChengPlusException.cast("上传静态文件过程中存在异常");
    }
}

把下面这行代码复制粘贴到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行命令启动:

复制代码
docker stop elasticsearch
docker stop kibana
docker start elasticsearch
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接口,写如下代码:

复制代码
@FeignClient(value="search",fallbackFactory = SearchServiceClientFallbackFactory.class)
public interface SearchServiceClient {
    @PostMapping("/search/index/course")
    public Boolean add(@RequestBody CourseIndex courseIndex);
}

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

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

复制代码
@Slf4j
@Component
public class SearchServiceClientFallbackFactory implements FallbackFactory<SearchServiceClient> {
    @Override
    public SearchServiceClient create(Throwable throwable) {
        return new SearchServiceClient() {
            @Override
            public Boolean add(CourseIndex courseIndex) {
                log.error("添加课程索引发生熔断,索引信息:{},熔断异常:{}",courseIndex,throwable.toString(),throwable);
                //走降级了返回false
                return false;
            }
        };
    }
}

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

复制代码
@Slf4j
@Component
public class CoursePublishTask extends MessageProcessAbstract {
    @Autowired
    CoursePublishService coursePublishService;
    @Autowired
    SearchServiceClient searchServiceClient;
    @Autowired
    CoursePublishMapper coursePublishMapper;
    //任务调度入口
    @XxlJob("CoursePublishJobHandler")
    public void coursePublishJobHandler() throws Exception{
        //分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();
        //调用抽象类的方法执行任务
        process(shardIndex,shardTotal,"course_publish",30,60);
    }
    //执行课程发布任务的逻辑,如果此方法抛出异常说明任务执行失败
    @Override
    public boolean execute(MqMessage mqMessage) {
        //从mqMessage拿到课程id
        Long courseId = Long.parseLong(mqMessage.getBusinessKey1());
        //课程静态化上传到minio
        generateCourseHtml(mqMessage,courseId);
        //向elasticsearch写索引数据
        saveCourseIndex(mqMessage,courseId);
        //向redis写缓存
        //课程静态化上传到minio
        //返回true任务完成
        return true;
    }
    //生成课程静态化页面并上传至文件系统
    private void generateCourseHtml(MqMessage mqMessage,long courseId){
        //消息id
        Long taskId = mqMessage.getId();
        MqMessageService mqMessageService = this.getMqMessageService();
        //做任务幂等性处理
        //取出该阶段执行状态
        int stageOne = mqMessageService.getStageOne(taskId);
        if(stageOne>0){
            log.debug("课程静态化任务完成,无须处理...");
            return;
        }
        //开始进行课程静态化,生成html页面
        File file = coursePublishService.generateCourseHtml(courseId);
        if(file==null){
            XueChengPlusException.cast("生成的静态页面为空");
        }
        //将html上传到minio
        coursePublishService.uploadCourseHtml(courseId,file);
        //任务处理完成写任务状态为完成
        mqMessageService.completedStageOne(taskId);
    }
    //保存课程索引信息 第二个阶段任务
    private void saveCourseIndex(MqMessage mqMessage,long courseId){
        //任务id
        Long taskId = mqMessage.getId();
        MqMessageService mqMessageService = this.getMqMessageService();
        //取出第二个阶段状态
        int stageTwo = mqMessageService.getStageTwo(taskId);
        //任务幂等性处理
        if(stageTwo>0){
            log.debug("课程索引信息已写入,无需执行...");
            return;
        }
        //查询课程信息,调用搜索服务添加索引接口
        //从课程发布表查询课程信息
        CoursePublish coursePublish = coursePublishMapper.selectById(courseId);
        CourseIndex courseIndex = new CourseIndex();
        BeanUtils.copyProperties(coursePublish,courseIndex);
        //远程调用
        Boolean add = searchServiceClient.add(courseIndex);
        if(!add){
            XueChengPlusException.cast("远程调用搜索服务添加课程索引失败");
        }
        //完成本阶段的任务
        mqMessageService.completedStageTwo(courseId);
    }
}

把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配置文件中的如下内容:

复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</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,登录成功。反之提示登录失败。

加入如下注解:

复制代码
@PreAuthorize("hasAuthority('p1')")
@PreAuthorize("hasAuthority('p2')")

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

下面logout:

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

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

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

5.2 OAuth2协议测试 P109

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

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

把下面这个bean复制到WebSecurityConfig中

复制代码
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
     return super.authenticationManagerBean();
}

重新启动项目。 如果启动失败注意一定要用老师配套的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。

复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

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

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

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

启动content服务。

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

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

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

5.5 网关认证 P112

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

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

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

复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</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类,写入如下代码:

复制代码
@Slf4j
@Component
public class UserServiceImpl implements UserDetailsService {
    @Autowired
    XcUserMapper xcUserMapper;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //账号
        String username = s;
        //根据username账号查询数据库
        XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s));
        //查询到用户不存在,返回null即可
        if(xcUser==null){
            return null;
        }
        //如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由
        String password = xcUser.getPassword();
        //权限
        String[] authorities = {"test"};
        UserDetails userDetails = User.withUsername(username).password(password).authorities(authorities).build();
        return userDetails;
    }
}

在WebSecurityConfig中写入如下代码:

复制代码
public static void main(String[] args) {
    String password="111111";
    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    //生成密码
    for(int i=0;i<5;i++){
        String encode = passwordEncoder.encode(password);
        System.out.println(encode);
    }
}

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

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

测试代码如下:

复制代码
public static void main(String[] args) {
    String password="111111";
    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    //生成密码
    for(int i=0;i<5;i++){
        String encode = passwordEncoder.encode(password);
        System.out.println(encode);
        boolean matches = passwordEncoder.matches(password, encode);
        System.out.println(matches);
    }
}

修改下面这个地方:

数据库如下:

测试:在如下位置打断点

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

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

5.7 扩展用户信息 P114

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

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

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

复制代码
###校验jwt令牌
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的开头写入如下代码:

复制代码
//将传入的json转成AuthParamsDto对象
AuthParamsDto authParamsDto = null;
try {
    authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
} catch (Exception e) {
    throw new RuntimeException("请求认证的参数不符合要求");
}
//账号
String username = authParamsDto.getUsername();

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

复制代码
### 密码模式,请求AuthParamsDto参数
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下面,加入下面的代码:

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

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

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

复制代码
@Autowired
DaoAuthenticationProviderCustom daoAuthenticationProviderCustom;

@Override
protected void configure(AuthenticationManagerBuilder auth)throws Exception{
    auth.authenticationProvider(daoAuthenticationProviderCustom);
}

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

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

复制代码
//统一的认证接口
public interface AuthService {
    //认证方法
    XcUserExt execute(AuthParamsDto authParamsDto);
}

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

复制代码
//账号名密码方式
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        return null;
    }
}

//微信扫码认证
@Service("wx_authservice")
public class WxAuthServiceImpl implements AuthService {
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        return null;
    }
}

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

复制代码
//认证类型,有password,wx...
String authType = authParamsDto.getAuthType();
//根据认证类型从spring容器中取出指定的bean
String beanName = authType+"_authservice";
AuthService authService = applicationContext.getBean(beanName,AuthService.class);
//调用
XcUserExt execute = authService.execute(authParamsDto);

@Autowired
ApplicationContext applicationContext;

5.10 统一账号密码认证 P117

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

复制代码
//账号名密码方式
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {
    @Autowired
    XcUserMapper xcUserMapper;
    @Autowired
    PasswordEncoder passwordEncoder;
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        //账号
        String username = authParamsDto.getUsername();
        //todo:校验验证码
        //账号是否存在,根据username账号查询数据库
        XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername,username));
        //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
        if(xcUser==null){
            throw new RuntimeException("账号不存在");
        }
        //校验密码是否正确
        //如果查到了用户拿到正确的密码
        String passwordDb = xcUser.getPassword();
        //拿到用户输入的密码
        String passwordForm = authParamsDto.getPassword();
        //校验密码
        boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
        if(!matches){
            throw new RuntimeException("账号或密码错误");
        }
        XcUserExt xcUserExt = new XcUserExt();
        BeanUtils.copyProperties(xcUser,xcUserExt);
        return xcUserExt;
    }
}

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

复制代码
@Slf4j
@Component
public class UserServiceImpl implements UserDetailsService {
    @Autowired
    XcUserMapper xcUserMapper;
    @Autowired
    ApplicationContext applicationContext;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    //将传入的json转成AuthParamsDto对象
    AuthParamsDto authParamsDto = null;
    try {
        authParamsDto = JSON.parseObject(s, AuthParamsDto.class);
    } catch (Exception e) {
        throw new RuntimeException("请求认证的参数不符合要求");
    }

    //认证类型,有password,wx...
    String authType = authParamsDto.getAuthType();
    //根据认证类型从spring容器中取出指定的bean
    String beanName = authType+"_authservice";
    AuthService authService = applicationContext.getBean(beanName,AuthService.class);
    //调用统一execute方法完成认证
    XcUserExt xcUserExt = authService.execute(authParamsDto);
    //封装xcUserExt用户信息为UserDetails
    UserDetails userPrincipal = getUserPrincipal(xcUserExt);
    return userPrincipal;

    }
    //查询用户信息
    public UserDetails getUserPrincipal(XcUserExt xcUser){
        String password = xcUser.getPassword();
        //权限
        String[] authorities = {"test"};
        xcUser.setPassword(null);
        String userJson = JSON.toJSONString(xcUser);
        UserDetails userDetails = User.withUsername(userJson).password(password).authorities(authorities).build();
        return userDetails;
    }
}

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

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

5.11 部署验证码服务 P118

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

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

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

在nacos中配置redis:

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

复制代码
### 申请验证码
POST {{checkcode_host}}/checkcode/pic

生成的效果如下:

key会被存储在redis中:

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

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

复制代码
### 校验验证码
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接口,写入如下代码:

复制代码
@FeignClient(value = "checkcode",fallbackFactory = CheckCodeClientFactory.class)
@RequestMapping("/checkcode")
public interface CheckCodeClient {
    @PostMapping(value="/verify")
    public Boolean verify(@RequestParam("key") String key, @RequestParam("code") String code);
}

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

复制代码
@Slf4j
@Component
public class CheckCodeClientFactory implements FallbackFactory<CheckCodeClient> {
    @Override
    public CheckCodeClient create(Throwable throwable) {
        return new CheckCodeClient() {
            @Override
            public Boolean verify(String key, String code) {
                log.debug("调用验证码服务熔断异常:{}",throwable.getMessage());
                return null;
            }
        };
    }
}

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

完善之后的PasswordAuthServiceImpl代码如下:

复制代码
//账号名密码方式
@Service("password_authservice")
public class PasswordAuthServiceImpl implements AuthService {
    @Autowired
    XcUserMapper xcUserMapper;
    @Autowired
    PasswordEncoder passwordEncoder;
    @Autowired
    CheckCodeClient checkCodeClient;
    @Override
    public XcUserExt execute(AuthParamsDto authParamsDto) {
        //账号
        String username = authParamsDto.getUsername();
        //输入的验证码
        String checkcode = authParamsDto.getCheckcode();
        //验证码对应的key
        String checkcodekey = authParamsDto.getCheckcodekey();
        if(StringUtils.isEmpty(checkcode)||StringUtils.isEmpty(checkcode)){
            throw new RuntimeException("请输入验证码");
        }
        //远程调用验证码服务接口去校验验证码
        Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
        if(verify == null || !verify){
            throw new RuntimeException("验证码输入错误");
        }
        //账号是否存在,根据username账号查询数据库
        XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername,username));
        //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
        if(xcUser==null){
            throw new RuntimeException("账号不存在");
        }
        //校验密码是否正确
        //如果查到了用户拿到正确的密码
        String passwordDb = xcUser.getPassword();
        //拿到用户输入的密码
        String passwordForm = authParamsDto.getPassword();
        //校验密码
        boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
        if(!matches){
            throw new RuntimeException("账号或密码错误");
        }
        XcUserExt xcUserExt = new XcUserExt();
        BeanUtils.copyProperties(xcUser,xcUserExt);
        return xcUserExt;
    }
}

在下面打上断点:

相关推荐
汇能感知2 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun2 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao2 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾3 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT3 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
ST.J4 小时前
前端笔记2025
前端·javascript·css·vue.js·笔记
Suckerbin4 小时前
LAMPSecurity: CTF5靶场渗透
笔记·安全·web安全·网络安全
眠りたいです4 小时前
基于脚手架微服务的视频点播系统-播放控制部分
c++·qt·ui·微服务·云原生·架构·播放器
叫我阿柒啊4 小时前
Java全栈开发工程师的实战面试经历:从基础到微服务
java·微服务·typescript·vue·springboot·前端开发·后端开发
小憩-4 小时前
【机器学习】吴恩达机器学习笔记
人工智能·笔记·机器学习