开源项目Cloud-Admin分析与学习

前一段时间在“感性认识JWT”一博文中分享了,很火的开源项目Cloud-Admin中鉴权中心和网关的实现。今天再来看看其他各个部分源码

提示

老规矩,放开源项目地址:https://gitee.com/minull/ace-security

目录结构

1543763543823

架构

下面是官方提供的架构模型。

image.png

项目的运行步骤

  • 先启动rabbitmq、redis、mysql以及consul注册中心
  • 运行数据库脚本:依次运行数据库:ace-admin/db/init.sql、ace-auth-server/db/init.sql、ace-trace
  • 修改配置数据库配置:ace-admin/src/main/resources/application.yml、ace-gate/src/main/resources/application.yml
  • 顺序运行main类:CenterBootstrap(ace-center)、AuthBootstrap(ace-auth-server)、AdminBootstrap(ace-admin)、GatewayServerBootstrap(ace-gateway-v2)

Admin模块

数据库设计

先看看数据库设计,admin模块负责所有权限的管理。第一张表base_element,定义了各个资源的code,类型,uri,每一个特定资源对应一种请求路径,如图👇

base_element

第二张,base_group定义了角色和请求路径的关系。👇

base_element

第三张,base_group_type定义了类型👇

base_element

第四张,base_menu定义了菜单👇

base_element

第五张,记录了网关日志相关信息

base_element

第六张,用户表

base_element

业务逻辑

接口这部分作者写的有些乱,将很多业务逻辑的有关代码放到接口层了(吐槽)。这部分代码没什么好说的,就是根据上面的数据库CRUD。接口的路径分为

1
2
3
4
5
6
7
8
9
/api/user/validate
/element/**"
/gateLog/**"
/group/**"
/groupType/**"
/menu/**"
/user/**"
/api/permissions"
/api/user/un/**"

这几类,并且走每一层都会走鉴权中心来鉴别,具体逻辑是使用springboot的addInterceptors()方法添加两层拦截器组成一个拦截器链,如下👇

1
2
3
4
5
6
7
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getServiceAuthRestInterceptor()).
addPathPatterns(getIncludePathPatterns()).addPathPatterns("/api/user/validate");
registry.addInterceptor(getUserAuthRestInterceptor()).
addPathPatterns(getIncludePathPatterns());
}

第一层拦截器是鉴权中心的ServiceAuthRestInterceptor拦截器,判断访问的客户端是否有权限访问;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//第一层拦截器 
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 配置该注解,说明不进行服务拦截
IgnoreClientToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class);
if (annotation == null) {
annotation = handlerMethod.getMethodAnnotation(IgnoreClientToken.class);
}
if(annotation!=null) {
return super.preHandle(request, response, handler);
}

String token = request.getHeader(serviceAuthConfig.getTokenHeader());
IJWTInfo infoFromToken = serviceAuthUtil.getInfoFromToken(token);
String uniqueName = infoFromToken.getUniqueName();
for(String client:serviceAuthUtil.getAllowedClient()){
if(client.equals(uniqueName)){
return super.preHandle(request, response, handler);
}
}
throw new ClientForbiddenException("Client is Forbidden!");
}

第二层拦截器是鉴权中心的UserAuthRestInterceptor拦截器,拦截非法用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 配置该注解,说明不进行用户拦截
IgnoreUserToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreUserToken.class);
if (annotation == null) {
annotation = handlerMethod.getMethodAnnotation(IgnoreUserToken.class);
}
if (annotation != null) {
return super.preHandle(request, response, handler);
}
String token = request.getHeader(userAuthConfig.getTokenHeader());
if (StringUtils.isEmpty(token)) {
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (cookie.getName().equals(userAuthConfig.getTokenHeader())) {
token = cookie.getValue();
}
}
}
}
IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(token);
BaseContextHandler.setUsername(infoFromToken.getUniqueName());
BaseContextHandler.setName(infoFromToken.getName());
BaseContextHandler.setUserID(infoFromToken.getId());
return super.preHandle(request, response, handler);
}

缓存中心

在上面的admin模块中,作者在user接口上,使用了自定义的缓存注解@Cache,用来保存用户的权限信息,减小数据库的访问压力👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
@RequestMapping("api")
public class UserRest {
@Autowired
private PermissionService permissionService;

@Cache(key="permission")
@RequestMapping(value = "/permissions", method = RequestMethod.GET)
public @ResponseBody
List<PermissionInfo> getAllPermission(){
return permissionService.getAllPermission();
}

@Cache(key="permission:u{1}")
@RequestMapping(value = "/user/un/{username}/permissions", method = RequestMethod.GET)
public @ResponseBody List<PermissionInfo> getPermissionByUsername(@PathVariable("username") String username){
return permissionService.getPermissionByUsername(username);
}

@RequestMapping(value = "/user/validate", method = RequestMethod.POST)
public @ResponseBody UserInfo validate(@RequestBody Map<String,String> body){
return permissionService.validate(body.get("username"),body.get("password"));
}
}

下面我们就分析一下缓存中心的设计。

目录结构

base_element

缓存中心是作者通过maven方式添加的,并没有通过直接项目代码展现。因此我使用idea的反编码工具进行分析

入口

入口为EnableAceCache,开启这个就可以自动配置缓存相关事项

缓存实体

先看缓存实体,作者定义了key,描述信息desc,以及过期时间

1
2
3
4
5
6
7
8
9
public class CacheBean {
private String key = "";
private String desc = "";
@JsonFormat(
timezone = "GMT+8",
pattern = "yyyy-MM-dd HH:mm:ss"
)
private Date expireTime;
..................}

配置类

一共有两个配置

base_element

RedisConfig

先看RedisConfig,使用@PostConstruct注解,意思是会在服务器加载Servlet的时候,将服务端yml中有关redis的配置加载到JedisPool中,这个方法只会被服务器调用一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Configuration
public class RedisConfig {
@Autowired
private Environment env;
private JedisPool pool;
private String maxActive;
private String maxIdle;
private String maxWait;
private String host;
private String password;
private String timeout;
private String database;
private String port;
private String enable;
private String sysName;

public RedisConfig() {
}

@PostConstruct
public void init() {
PropertiesLoaderUtils prop = new PropertiesLoaderUtils(new String[]{"application.properties"});
this.host = prop.getProperty("redis.host");
if (StringUtils.isBlank(this.host)) {
this.host = this.env.getProperty("redis.host");
this.maxActive = this.env.getProperty("redis.pool.maxActive");
this.maxIdle = this.env.getProperty("redis.pool.maxIdle");
this.maxWait = this.env.getProperty("redis.pool.maxWait");
this.password = this.env.getProperty("redis.password");
this.timeout = this.env.getProperty("redis.timeout");
this.database = this.env.getProperty("redis.database");
this.port = this.env.getProperty("redis.port");
this.sysName = this.env.getProperty("redis.sysName");
this.enable = this.env.getProperty("redis.enable");
} else {
this.maxActive = prop.getProperty("redis.pool.maxActive");
this.maxIdle = prop.getProperty("redis.pool.maxIdle");
this.maxWait = prop.getProperty("redis.pool.maxWait");
this.password = prop.getProperty("redis.password");
this.timeout = prop.getProperty("redis.timeout");
this.database = prop.getProperty("redis.database");
this.port = prop.getProperty("redis.port");
this.sysName = prop.getProperty("redis.sysName");
this.enable = prop.getProperty("redis.enable");
}

}

@Bean
public JedisPoolConfig constructJedisPoolConfig() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(Integer.parseInt(this.maxActive));
config.setMaxIdle(Integer.parseInt(this.maxIdle));
config.setMaxWaitMillis((long)Integer.parseInt(this.maxWait));
config.setTestOnBorrow(true);
return config;
}

@Bean(
name = {"pool"}
)
public JedisPool constructJedisPool() {
String ip = this.host;
int port = Integer.parseInt(this.port);
String password = this.password;
int timeout = Integer.parseInt(this.timeout);
int database = Integer.parseInt(this.database);
if (null == this.pool) {
if (StringUtils.isBlank(password)) {
this.pool = new JedisPool(this.constructJedisPoolConfig(), ip, port, timeout);
} else {
this.pool = new JedisPool(this.constructJedisPoolConfig(), ip, port, timeout, password, database);
}
}

return this.pool;
}

CacheWebConfig

第二个配置,就是使用springboot拦截器,将作者自己写的缓存管理中心视图界面展示出来(这个操作太神奇了,第一次看到)👇

1
2
3
4
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler(new String[]{"/static/cache/**"}).addResourceLocations(new String[]{"classpath:/META-INF/static/"});
super.addResourceHandlers(registry);
}

base_element

使用缓存

RedisServiceImpl和CacheRedis

RedisServiceImpl使用了JedisPool那几个方法实现了增删改查操作,代码省略。

CacheRedisRedisServiceImpl是加入一些增删改查逻辑,譬如什么是什么设置缓存。

切面加入缓存

核心方法

核心方法interceptor如下,使用ProceedingJoinPoint拿到被@Cache标记的的方法中的参数,用getKey()方法拿到具体缓存的key,使用CacheRedis的get()方法查找对应的key,如果key找不到则用CacheRedis的set()方法添加新的缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Around("aspect()&&@annotation(anno)")
public Object interceptor(ProceedingJoinPoint invocation, Cache anno) throws Throwable {
MethodSignature signature = (MethodSignature)invocation.getSignature();
Method method = signature.getMethod();
Object result = null;
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] arguments = invocation.getArgs();
String key = "";
String value = "";

try {
key = this.getKey(anno, parameterTypes, arguments);
value = this.cacheAPI.get(key);
Type returnType = method.getGenericReturnType();
result = this.getResult(anno, result, value, returnType);
} catch (Exception var14) {
this.log.error("获取缓存失败:" + key, var14);
} finally {
if (result == null) {
result = invocation.proceed();
if (StringUtils.isNotBlank(key)) {
this.cacheAPI.set(key, result, anno.expire());
}
}
}
return result;
}
取得key

getKey()方法的逻辑为,判断key生成器是否是默认生成器(可以使用多种生成器),然后根据默认生成器的规则生成一个key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private String getKey(Cache anno, Class<?>[] parameterTypes, Object[] arguments) throws InstantiationException, IllegalAccessException {
String generatorClsName = anno.generator().getName();
IKeyGenerator keyGenerator = null;
if (anno.generator().equals(DefaultKeyGenerator.class)) {
keyGenerator = this.keyParser;
} else if (this.generatorMap.contains(generatorClsName)) {
keyGenerator = (IKeyGenerator)this.generatorMap.get(generatorClsName);
} else {
keyGenerator = (IKeyGenerator)anno.generator().newInstance();
this.generatorMap.put(generatorClsName, keyGenerator);
}

String key = keyGenerator.getKey(anno.key(), anno.scope(), parameterTypes, arguments);
return key;
}
key生成器

默认key生成器的代码如下(生成规则我看不懂😭),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public String buildKey(String key, CacheScope scope, Class<?>[] parameterTypes, Object[] arguments) {
boolean isFirst = true;
if (key.indexOf("{") > 0) {
key = key.replace("{", ":{");
Pattern pattern = Pattern.compile("\\d+\\.?[\\w]*");
Matcher matcher = pattern.matcher(key);

while(matcher.find()) {
String tmp = matcher.group();
String[] express = matcher.group().split("\\.");
String i = express[0];
int index = Integer.parseInt(i) - 1;
Object value = arguments[index];
if (parameterTypes[index].isAssignableFrom(List.class)) {
List result = (List)arguments[index];
value = result.get(0);
}

if (value == null || value.equals("null")) {
value = "";
}

if (express.length > 1) {
String field = express[1];
value = ReflectionUtils.getFieldValue(value, field);
}

if (isFirst) {
key = key.replace("{" + tmp + "}", value.toString());
} else {
key = key.replace("{" + tmp + "}", "_" + value.toString());
}
}
}

return key;
}
key解析

因为作者的key生成器抖了很多机灵,因此,拿到key以后,要将生成key和value之前的数值找到才能进行比对,下面时解析key的代码👇大概逻辑是根据不同的value类型返回json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object parse(String value, Type type, Class... origins) {
Object result = null;
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType)type;
Type rawType = parameterizedType.getRawType();
if (((Class)rawType).isAssignableFrom(List.class)) {
result = JSON.parseArray(value, (Class)parameterizedType.getActualTypeArguments()[0]);
}
} else if (origins == null) {
result = JSON.parseObject(value, (Class)type);
} else {
result = JSON.parseObject(value, origins[0]);
}

return result;
}

切面清除缓存

大概意思和上面差不多,只要服务端上加入@CacheClear注解就可以清除对应缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Around("aspect()&&@annotation(anno)")
public Object interceptor(ProceedingJoinPoint invocation, CacheClear anno) throws Throwable {
MethodSignature signature = (MethodSignature)invocation.getSignature();
Method method = signature.getMethod();
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] arguments = invocation.getArgs();
String key = "";
if (StringUtils.isNotBlank(anno.key())) {
key = this.getKey(anno, anno.key(), CacheScope.application, parameterTypes, arguments);
this.cacheAPI.remove(key);
} else if (StringUtils.isNotBlank(anno.pre())) {
key = this.getKey(anno, anno.pre(), CacheScope.application, parameterTypes, arguments);
this.cacheAPI.removeByPre(key);
} else if (anno.keys().length > 1) {
String[] arr$ = anno.keys();
int len$ = arr$.length;

for(int i$ = 0; i$ < len$; ++i$) {
String tmp = arr$[i$];
tmp = this.getKey(anno, tmp, CacheScope.application, parameterTypes, arguments);
this.cacheAPI.removeByPre(tmp);
}
}

return invocation.proceed();
}
-------------本稿が終わる感谢您的阅读-------------