文章目录
- 一 数据库访问接口
-
- 1 MyBatis
- 2 Spring Data JPA
- 3 Spring Data MongoDB
- 二 数据库
-
- 1 MySQL
- 2 MongoDB
- 3 Redis
- 三 开发规范化、响应格式与异常处理
-
- 四 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 权限的用户调用