文章目录 一 数据库访问接口 1 MyBatis 2 Spring Data JPA 3 Spring Data MongoDB 二 数据库 三 开发规范化、响应格式与异常处理 四 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 > { List < Teachplan > findByCourseidAndParentid ( String courseId, String parentId) ;
}
需要对数据表对应的类 加一系列注解(不止如下所示)
3 Spring Data MongoDB
使用方法类似于 Spring Data JPA,需要通过对类的注释,实现模型-表的映射
@Document ( collection = "filesystem" )
public class FileSystem {
}
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 的完善的事务特性,在本项目中用于存储非核心数据
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) ; }
}
@ControllerAdvice
public class ExceptionCatch { private static final Logger LOGGER = LoggerFactory . getLogger ( CustomException . class ) ; private static ImmutableMap < Class < ? extends Throwable > , ResultCode > EXCEPTIONS; 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 ( ) ; } ResultCode resultCode = ExceptionCatch . EXCEPTIONS. get ( e. getClass ( ) ) ; if ( resultCode != null ) { return new ResponseResult ( resultCode) ; } else { return new ResponseResult ( CommonCode . SERVER_ERROR) ; } }
}
四 RabbitMQ
项目中简单用到了 routing 模式,生产者发送消息时需要指定 routing key,交换机会根据 routing key 将消息投递到指定的队列 使用消息队列的主要优点:解耦、异步、削峰(因为 MQ 短时间积压数据是可以接受的) RabbitMQ 结构:
五 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
注解 3、服务名代替 IP 地址+端口号 ,发挥注册中心的作用(不用手动写入 IP 地址+端口号 而交给注册中心管理),后面拼接相应 Controller 方法路径
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 网关
网关在微服务前边设置一道屏障,请求先到服务网关,网关会对请求进行过滤、校验、路由等处理。有了服务网关可以提高微服务的安全性,网关校验请求的合法性,请求不合法将被拦截,拒绝访问 用网关的好处是,无论调用哪个微服务,客户端所有的请求都访问网关的 IP 地址和端口号 ,由网关使用 Eureka 负责将请求路由到指定的微服务(因为 Zuul 的配置了 Eureka 中各种微服务的 ID)
zuul : routes : manage‐course : path : /course/** serviceId : xc‐service‐manage‐course strip‐prefix : false sensitiveHeaders :
六 搜索服务
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 ( ) { String [ ] split = hostlist. split ( "," ) ; 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" ) ; } return new RestHighLevelClient ( RestClient . builder ( httpHostArray) ) ; } }
2 Logstash
Logstash 的功能是,将 MySQL 的数据表同步到索引库 配置并启动 Logstash,如果 document 的时间戳大于上次采集的时间,就更新索引库 项目中为了便于搜索服务的开发,将课程的一系列信息汇总为一张数据表,将该表同步到索引库,并使用 JPA 操作数据表 因为课程服务是从 ES 的索引库而非数据库搜索,所以 Service 层没有自动注入 DAO
七 用户认证与授权
1 Spring Security OAuth2
常用的有:授权码模式(主要用于第三方登录)和密码模式。项目采用密码模式 授权码模式流程:
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,而不用传递给认证服务器
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; @Override public String filterType ( ) { return "pre" ; } @Override public int filterOrder ( ) { return 0 ; } @Override public boolean shouldFilter ( ) { return true ; } @Override public Object run ( ) throws ZuulException { RequestContext context = RequestContext . getCurrentContext ( ) ; HttpServletRequest request = context. getRequest ( ) ; HttpServletResponse response = context. getResponse ( ) ; String accessTokenFromCookie = this . authService. getAccessTokenFromCookie ( request) ; if ( StringUtils . isEmpty ( accessTokenFromCookie) ) { this . accessDenied ( ) ; return null ; } String jwtTokenFromHeader = this . authService. getJwtTokenFromHeader ( request) ; if ( StringUtils . isEmpty ( jwtTokenFromHeader) ) { this . accessDenied ( ) ; return null ; } 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 权限的用户调用