在线教育后端开发项目总结

文章目录

  • 一 数据库访问接口
    • 1 MyBatis
    • 2 Spring Data JPA
    • 3 Spring Data MongoDB
  • 二 数据库
    • 1 MySQL
    • 2 MongoDB
    • 3 Redis
  • 三 开发规范化、响应格式与异常处理
    • 1 开发规范
    • 2 响应格式
    • 3 异常处理
  • 四 RabbitMQ
  • 五 Spring Cloud 相关工具
    • 1 Eureka
    • 2 Ribbon
    • 3 Feign
    • 4 Zuul 网关
  • 六 搜索服务
    • 1 ElasticSearch
    • 2 Logstash
  • 七 用户认证与授权
    • 1 Spring Security OAuth2
    • 2 JWT(Json Web Token)
    • 3 单点登录和身份校验
    • 4 用户授权 RBAC(Role-Based Access Control)


一 数据库访问接口

1 MyBatis

  • 用于写相对复杂(多表连接的) SQL 命令
  • 使用时,用 @Mapper 注释 xxMapper 接口,在接口中定义方法,并创建同名的 xxMapper.xml 文件,在方法对应的标签存放 SQL 命令
  • 更多关于 MyBatis

2 Spring Data JPA

  • Spring 提供的操作 MySQL 单张数据表的 API,无需自行编写 SQL
  • 接口一般命名为 xxRepository
public interface TeachplanRepository extends JpaRepository<Teachplan, String> {
    /**
     * 根据课程id 和 parent id 查询课程计划
     */
    List<Teachplan> findByCourseidAndParentid(String courseId, String parentId);
}
  • 需要对数据表对应的类加一系列注解(不止如下所示)
    在线教育后端开发项目总结_第1张图片

3 Spring Data MongoDB

  • 使用方法类似于 Spring Data JPA,需要通过对类的注释,实现模型-表的映射
// 数据表对应的类
@Document(collection = "filesystem")
public class FileSystem {
	// 省略表的一系列属性...
}

// dao
public interface FileSystemRepository extends MongoRepository<FileSystem, String> {
}

二 数据库

1 MySQL

  • InnoDB 存储引擎:
    索引结构是B+树(和B树相比查询效率更稳定,I/O次数少,对范围查找的支持更好
    实现了行级锁
    支持外键
  • 一些优化措施:
    查询操作时,只返回需要的行或列
    尽量不在数据表中存储 NULL,不在 SQL 命令的条件中判断 NULL
    字段的数据类型定义准确
    设计数据表时遵循范式规则 1/2/3NF
    对数据表进行适当的行、列拆分
  • 索引分类:
    主索引/二级索引
    聚簇索引/非聚簇索引…
  • 如果没有主键也没有合适的唯一索引,那么 InnoDB 内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,该列的值会随着数据的插入自增

2 MongoDB

  • NoSQL 的一种,以类 Json 的文档格式存储数据,文档型数据库
  • 本项目中负责存放一系列的页面信息
  • 索引结构是B树
  • 不具有 MySQL 的完善的事务特性,在本项目中用于存储非核心数据

在线教育后端开发项目总结_第2张图片

3 Redis

  • NoSQL 的一种,一般作为缓存数据库辅助持久化的数据库
  • 本项目中负责存放登录用户的 (access_token, JWT + refresh_token) 的键值对
  • 拥有两种持久化机制:RDB 和 AOF,其特性查阅链接
  • 项目中使用 StringRedisTemplate 操作 Redis
  • 更多关于 Redis

三 开发规范化、响应格式与异常处理

1 开发规范

  • get 请求时,采用 key/value 格式请求,SpringMVC 可采用基本类型的变量接收,也可以采用对象接收
  • post 请求时,可以提交 form 表单数据(application/x-www-form-urlencoded)和 Json 数据(application/json),文件等多部件类型(multipart/formdata)三种数据格式,SpringMVC 接收 Json 数据
  • Controller 主要作为响应 URL 的接口(粘合剂的作用),业务逻辑实现放在 Service 层,DAO 仅仅负责与数据库交互而不考虑任何业务逻辑

2 响应格式

  • 设置枚举类型的响应代码
public enum CommonCode implements ResultCode {
    SUCCESS(true,10000,"操作成功!"),
    FAIL(false,11111,"操作失败!"),
    UNAUTHENTICATED(false,10001,"此操作需要登陆系统!"),
    UNAUTHORISE(false,10002,"权限不足,无权操作!"),
    SERVER_ERROR(false,99999,"抱歉,系统繁忙,请稍后重试!"),
    INVALIDPARAM(false, 10003, "参数非法");
	// ...
}
  • 针对不同的客户端请求,返回对应的一致的类型:查询返回 QueryResponseResult,添加返回 ResponseResult

3 异常处理

  • 设置枚举类型的异常代码,写入异常编号和异常信息
  • 使用统一的类抛出(使用静态方法包装)和捕获异常(使用 SpringMVC 提供的 @ControllerAdvice 注解类 和 @ExceptrionHandler 注解方法)
  • 自定义异常类型继承自 RuntimeException,属于 unchecked 异常,不捕获仍然可以运行,实际交给增强控制器完成捕获
// 异常抛出:使用静态方法包装
public class ExceptionCast {
    public static void cast(ResultCode resultCode) {
        throw new CustomException(resultCode);
    }
}

// 异常捕获:使用Spring提供的 @ControllerAdvice注解类 和 @ExceptrionHandler注解方法
@ControllerAdvice
public class ExceptionCatch {
    private static final Logger LOGGER = LoggerFactory.getLogger(CustomException.class);
    private static ImmutableMap<Class<? extends Throwable>, ResultCode> EXCEPTIONS;
    //使用builder来构建一个异常类型和错误代码的异常
    protected static ImmutableMap.Builder<Class<? extends Throwable>,ResultCode> builder = ImmutableMap.builder();

    static{
        // 加入一些基础的异常类型判断...
        builder.put(HttpMessageNotReadableException.class, CommonCode.INVALIDPARAM);
    }

    // 处理自定义异常,形参为捕获异常类型
    @ExceptionHandler
    @ResponseBody
    public ResponseResult customException(CustomException customException) {
        ExceptionCatch.LOGGER.error("捕获到自定义异常");
        ResultCode resultCode = customException.getResultCode();
        return new ResponseResult(resultCode);
    }

    // 处理不可预料异常
    @ExceptionHandler
    @ResponseBody
    public ResponseResult exception(Exception e) {
        if (ExceptionCatch.EXCEPTIONS == null) {
            EXCEPTIONS = ExceptionCatch.builder.build();  // 构建map
        }
        ResultCode resultCode = ExceptionCatch.EXCEPTIONS.get(e.getClass());

        if (resultCode != null) {
            // map中有该异常类型
            return new ResponseResult(resultCode);
        } else {
            // 没有,返回99999
            return new ResponseResult(CommonCode.SERVER_ERROR);
        }
    }
}

四 RabbitMQ

  • 项目中简单用到了 routing 模式,生产者发送消息时需要指定 routing key,交换机会根据 routing key 将消息投递到指定的队列
  • 使用消息队列的主要优点:解耦、异步、削峰(因为 MQ 短时间积压数据是可以接受的)
  • RabbitMQ 结构:
    在线教育后端开发项目总结_第3张图片

五 Spring Cloud 相关工具

1 Eureka

  • 注册中心
    微服务数量众多,要进行远程调用就需要知道服务端的 IP 地址和端口,注册中心帮助管理这些服务的 IP 和端口
    注册的微服务会向注册中心实时上报自己的状态,注册中心统一管理这些微服务的状态
    将存在问题的服务踢出服务列表,客户端获取可用的服务进行调用
  • 注册中心同样属于一个微服务,它的启动类要加上注解 @EnableEurekaServer
  • 对于要向注册中心注册的微服务,需要在启动类加注解 @EnableDiscoveryClient,表示它是一个 Eureka 的客户端,用于发现其它微服务
  • 高可用 Eureka 需要两个注册中心相互注册,即使其中一台停机也不会影响服务的注册和发现
  • 启动后访问指定端口就可以便捷地检测微服务情况,本项目为 50101 和 50102

2 Ribbon

  • 执行客户端的负载均衡(而非 Nginx 这种服务端的负载均衡),Ribbon 先从 EurekaServer 中获取服务列表,根据负载均衡的算法去调用微服务
  • Spring Cloud 引入 Ribbon 配合 RestTemplate (本项目使用 okhttp 进行远程调用)实现客户端负载均衡
  • RestTemplate 是 Spring 提供的一个访问 HTTP 服务的客户端类,微服务之间的调用需要使用 RestTemplate
  • 使用方法:
    1、添加相关依赖并配置
    2、创建 resttemplate 的 bean,使用 @LoadBalanced 注解
    在线教育后端开发项目总结_第4张图片
    3、服务名代替 IP 地址+端口号,发挥注册中心的作用(不用手动写入 IP 地址+端口号 而交给注册中心管理),后面拼接相应 Controller 方法路径
    在线教育后端开发项目总结_第5张图片

3 Feign

  • 可以实现 像调用本地接口一样调用远程接口 ,这个远程接口指的是在注册中心注册过的某个微服务提供的 Controller 方法
  • 内部集成了 Ribbon,可以实现客户端负载均衡
  • 使用方法:
    使用@EnableFeignClients 注解启动类,Spring 会扫描标记了 @FeignClient 注解的接口,生成代理对象
    使用 @FeignClient 注解客户端接口,并指定该接口绑定的微服务名(指定后 Feign 会从注册中心获取服务列表,并通过负载均衡算法进行服务调用
    如果接口的方法返回类型为对象,则该类型必须有无参构造器
    Feign 根据接口方法的注解的 URL,进行远程调用
@FeignClient(name = "XC-SERVICE-MANAGE-CMS")  // 指定该接口绑定的微服务名
public interface CmsPageClient {
    @GetMapping("/cms/page/get/{id}")
    CmsPage findById(@PathVariable("id") String id);
}

4 Zuul 网关

在线教育后端开发项目总结_第6张图片

  • 网关在微服务前边设置一道屏障,请求先到服务网关,网关会对请求进行过滤、校验、路由等处理。有了服务网关可以提高微服务的安全性,网关校验请求的合法性,请求不合法将被拦截,拒绝访问
  • 用网关的好处是,无论调用哪个微服务,客户端所有的请求都访问网关的 IP 地址和端口号,由网关使用 Eureka 负责将请求路由到指定的微服务(因为 Zuul 的配置了 Eureka 中各种微服务的 ID)
zuul:
  routes:
    manage‐course: #路由名称,名称任意,保持所有路由名称唯一
      path: /course/**
      serviceId: xc‐service‐manage‐course # 从Eureka中找到服务的ip和端口
      # url: http://localhost:31200 # 也可转发到指定url
      strip‐prefix: false #true:代理转发时去掉前缀
      sensitiveHeaders: # 默认zuul会屏蔽cookie,不会传到下游服务,设置为空则取消屏蔽,如果设置了具体的头信息则不会传到下游服务
      # ignoredHeaders: Authorization  # 可以设置过滤的头信息,默认为空表示不过滤任何头,jwt要放在header中所以要关闭该功能

六 搜索服务

1 ElasticSearch

  • 在项目中实现课程搜索的功能,应用流程:
    1、用户在前端搜索关键字,前端通过 HTTP 方式请求项目服务端
    2、项目服务端通过 RESTful 方式请求 ES 集群进行搜索(需要手动构建 HTTP Request
    3、ES 集群从索引库检索数据并返回
  • 另外用到了 ES 的可视化插件 Head 和 IK 分词器(中文分词)
  • 6.0之前的版本有 type(类型)概念,type 相当于关系数据库的表,但 ES 官方建议索引库只存放相同类型的文档,即用关系数据库的表概念类比ES的索引库。项目中将 type 指定为 doc,只是无意义的占位符
  • 可以使用 Postman 发送各种类型的 HTTP Request 操作 ES
  • 项目中需要在配置类生成 RestClient 的 Bean,用来操作 ES
@Configuration
public class ElasticsearchConfig {

    @Value("${dino.elasticsearch.hostlist}")
    private String hostlist;

    @Bean
    public RestHighLevelClient restHighLevelClient(){
        //解析hostlist配置信息
        String[] split = hostlist.split(",");
        //创建HttpHost数组,其中存放es主机和端口的配置信息
        HttpHost[] httpHostArray = new HttpHost[split.length];
        for(int i=0;i<split.length;i++){
            String item = split[i];
            httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http");
        }
        //创建RestHighLevelClient客户端
        return new RestHighLevelClient(RestClient.builder(httpHostArray));
    }
    
}

2 Logstash

  • Logstash 的功能是,将 MySQL 的数据表同步到索引库
  • 配置并启动 Logstash,如果 document 的时间戳大于上次采集的时间,就更新索引库
  • 项目中为了便于搜索服务的开发,将课程的一系列信息汇总为一张数据表,将该表同步到索引库,并使用 JPA 操作数据表
  • 因为课程服务是从 ES 的索引库而非数据库搜索,所以 Service 层没有自动注入 DAO

七 用户认证与授权

1 Spring Security OAuth2

  • 常用的有:授权码模式(主要用于第三方登录)和密码模式。项目采用密码模式
  • 授权码模式流程:
    在线教育后端开发项目总结_第7张图片
1、客户端请求第三方授权 GET localhost:40400/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost
- client_id:客户端 id,和授权配置类中设置的客户端 id 一致
- response_type:授权码模式固定为 code
- scop:客户端范围,和授权配置类中设置的 scop 一致
- redirect_uri:跳转 uri,当授权码申请成功后会跳转到此地址,并在后边带上 code参数(授权码)

2、用户(资源拥有者)同意给客户端授权

3、客户端获取到授权码 7bjwZ5,请求认证服务器申请令牌 POST http://localhost:40400/auth/oauth/token,需要在请求体中添加:
- grant_type:授权类型,填写 authorization_code,表示授权码模式
- code:授权码,就是刚刚获取的授权码,授权码只使用一次就无效了,需要重新申请
- redirect_uri:申请授权码时的跳转 url,和申请授权码时用的 redirect_uri 一致

4、认证服务器向客户端响应令牌 (私钥生成令牌)
	{
		"access_token": "eyJhbGciOiJSUzI1NiI...",
		"token_type": "bearer",
		"refresh_token": "eyJhbGciOiTtaBGa6LRtIhzz3Eyjw...",
		"expires_in": 43199,
		"scope": "app",
		"jti": "a8d04b91-57d16..."
	}

5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权(公钥校验令牌)
   资源服务器也要引入相应的依赖,创建 config 等,才能具有验证令牌合法性的功能

6、资源服务器返回受保护资源
  • 密码模式:密码模式与授权码模式的区别是申请令牌不再使用授权码,而是直接通过正确的用户名和密码即可申请令牌

2 JWT(Json Web Token)

  • JWT = Header + Payload + Signature
    Header:描述 JWT 的元数据,包括令牌的类型(即 JWT)及使用的哈希算法,内容用 Base64Url 编码
    Payload:负载是存放有效信息的地方,它可以存放 JWT 提供的字段,比如 iss(签发者)、exp(过期时间戳)、sub(面向的用户)等,也可自定义字段
    Signature:签名部分,防止 JWT 被篡改 指定的哈希算法( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
  • 考虑到 Cookie 的容量比较小,在 Cookie 中存放 access_token,HTTP Header 中存放 JWT,Redis 中存放 (access_token, JWT + refresh_token) 的键值对以供查询
  • 使用 JWT 的一个显著优点是,资源服务器可以自行认证 JWT,而不用传递给认证服务器
    在线教育后端开发项目总结_第8张图片

3 单点登录和身份校验

  • 单点登录:微服务之间相互调用,需要验证 JWT 的合法性,使用 Feign Interceptor
  • 网关在将请求转发到指定微服务前,负责检验 JWT 的合法性:
    1、从 Cookie 查询 access_token 令牌是否存在,不存在则拒绝访问
    2、从 HTTP Header 查询 JWT 令牌是否存在,不存在则拒绝访问
    3、从 Redis 中查询 JWT 令牌是否过期,过期则拒绝访问
  • 具体实现:在网关微服务添加 @Component
@Component
public class LoginFilter extends ZuulFilter {

    @Autowired
    AuthService authService;

    /**
     * 过滤器类型:pre、routing、post、error
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器序号
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 是否生效
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 执行过滤操作
     * 1、从cookie查询用户身份令牌是否存在,不存在则拒绝访问
     * 2、从http header查询jwt令牌是否存在,不存在则拒绝访问
     * 3、验证redis中的jwt是否过期,过期则拒绝访问
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        HttpServletResponse response = context.getResponse();
        // 1.查询cookie的access_token
        String accessTokenFromCookie = this.authService.getAccessTokenFromCookie(request);
        if (StringUtils.isEmpty(accessTokenFromCookie)) {
            this.accessDenied();
            return null;
        }
        // 2.查询header的jwt
        String jwtTokenFromHeader = this.authService.getJwtTokenFromHeader(request);
        if (StringUtils.isEmpty(jwtTokenFromHeader)) {
            this.accessDenied();
            return null;
        }
        // 3.验证redis中的jwt是否过期
        Long expire = this.authService.checkJwtFromRedis(accessTokenFromCookie);
        if (expire <= 0) {
            this.accessDenied();
            return null;
        }
        return null;
    }

4 用户授权 RBAC(Role-Based Access Control)

  • 权限就是对资源的控制,对 web 应用来说就是对 url 的控制
  • 核心为五张数据表:用户表、用户角色表、角色表、角色权限表、权限表,使用时的查询流程:
    根据用户ID查询用户角色表,获取角色ID
    根据角色ID查询角色权限表,获取权限ID
    根据权限ID查询权限表,获取权限
    <select id="selectPermissionByUserId" resultType="com.framework.domain.ucenter.Menu">
        select id, code, p_id pId, menu_name menuName, url, is_menu isMenu, level, sort, status, icon,
               create_time createTime, update_time updateTime from Menu where id in (
                    select menu_id from Permission where role_id in (
                        select role_id from User_role where user_id = #{userId})
            )
    select>
  • 获取到的权限被写入 JWT 中,在访问微服务前需要校验用户权限是否够
  • 在 config 类上注解 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true),然后Controller 方法注解@PreAuthorize("hasAuthority('course_find_list')"),指定该方法只能被 JWT 中含有 course_find_list 权限的用户调用

你可能感兴趣的:(Java开发,后端,java,spring,boot,mvc)