一、简述
权限系统的设计一般分为:权限设计 = 功能权限 + 数据权限
本文主要对pigx平台在认证与授权方面的功能权限进行解析,而对于数据权限,一般是根据业务场景具体做特殊的设计,且必须在项目前期就做好规划,不像功能权限那样可以在后期完成,pigx对数据权限做了一定支持,具体请参考pigx数据权限设计
那么对于pigx的功能权限:
我们把请求按来源分为:外部请求和内部请求,其中外部请求分为登录请求和非登录请求
按目标资源分为:无注解、@Inner注解(仅内部请求)、@PreAuthorize注解(带权限控制)
下面是来源与资源的对应关系:
(请求类型与资源控制之间的关系)
对于请求而言,主要是外部与内部的区别,而外部请求必须经过网关(Gateway),网关对于请求的处理主要有登录与非登录的区别,因此上面表格中将外部请求细分为登录与非登录,这样对比资源的控制就更清晰、更细化
对于目标资源而言,主要有无注解、@Inner注解、@PreAuthorize注解三种:
无注解:一般用于对外公开资源,如商品浏览、官网等互联网接口。
对应的服务需要添加白名单配置:security.oauth2.client.ignore-urls后接口才可访问(或不引入依赖pigx-common-security、或采用@Inner(false)注解)
@Inner注解:一般用于被内部应用请求的接口,如日志、定时任务、文件存储等支持型服务,被注解后该接口将无法被外部请求访问到(需要网关提供保护,后面会讲到网关是如何保护内部应用请求的)
@PreAuthorize注解:用于外部请求非登录请求,该类请求须带token,因此是登录后对用户访问资源接口的权限控制,微服务依赖pigx-common-security以后就有认证(spring security oauth2)控制了,认证控制负责的是对token的鉴定,而对接口本身是否有权限访问是由pigx中的用户权限系统所控制,该注解就是在token鉴定成功以后,pigx用户权限系统再基于token内容进行的权限控制
下面通过以下部分对pigx平台认证与授权系统进行分析:
- 与网关相关的权限功能设计
- 与外部请求相关的权限功能设计
- 与内部请求相关的权限功能设计
二、与网关相关的权限功能设计
网关服务(Gateway)是所有服务的入口,起到了重要的作用,目前在pigx系统架构中主要有以下特殊作用的过滤器(Filter),他们都对权限系统的工作起到了一定的作用:
过滤器 | 作用 |
---|---|
HttpBasicGatewayFilter | 自定义basic认证,针对特殊场景使用 |
JiyupRequestGlobalFilter | 清洗请求头中from 参数,用于防止外部模拟内部请求 |
PasswordDecoderFilter | 对登录请求的密码参数进行解密处理 |
PreviewGatewayFilter | 提供测试环境的支持 |
ValidateCodeGatewayFilter | 对登录请求进行验证码检验 |
PigxRequestGlobalFilter分析:
内部服务请求通常不需要再通过auth服务进行一次鉴权,如A请求B时,如果B需要对A请求鉴权的话,A就需要拿到token,且B接送token后还需要请求auth服务鉴定token有效性,如果B在处理过程中还需要请求C,则C同样需要如此过程,不但复杂且给auth服务增添不少压力,一般的做法是网关请求A时,A进行一次鉴权,A到B,B到C的内部请求过程不需要再鉴权
为了实现B接口不鉴权,一般会将B所在服务中配置security.oauth2.client.ignore-urls,接口地址将不会鉴权
但单纯添加白名单是不行的,因为网关外部请求就可以直接获取到该接口资源
为了实现接口内部请求允许请求,外部请求不允许请求的目的,pigx引入了注解@Inner
该注解的接口请被切面PigxSecurityInnerAspect控制,控制逻辑很简单,只有请求头部带"from"标志时才允许访问:
@SneakyThrows
@Around("@annotation(inner)")
public Object around(ProceedingJoinPoint point, Inner inner) {
String header = request.getHeader(SecurityConstants.FROM);
if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
log.warn("访问接口 {} 没有权限", point.getSignature().getName());
throw new AccessDeniedException("Access is denied");
}
return point.proceed();
}
(PigxSecurityInnerAspect关键源码)
由此一来,内部请求时就需要添加SecurityConstants.FROM_IN参数,保证不会被PigxSecurityInnerAspect切面所拒绝,比如下面这段用户授权(Auth)的代码,请求用户服务(upms)时带上了此参数来获取用户信息:
而用户信息接口上对应加入了@Inner注解:
但外部请求可以通过网关访问白名单接口,同样也可以模拟头部带“from”的内部请求
因此PigxRequestGlobalFilter的作用就是防止外部模拟头部带“from”的请求来访问内部资源,从源码中可以看到将请求头部对“from”统一进行了去除:
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.remove(SecurityConstants.FROM))
.build();
PasswordDecoderFilter分析:
考虑到登录请求密码参数在传输过程中的安全性,前端对密码文本进行了加密处理:
PasswordDecoderFilter用于对登录密码中的密码参数进行解密处理:
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 不是登录请求,直接向下执行
if (!StrUtil.containsAnyIgnoreCase(request.getURI().getPath(), SecurityConstants.OAUTH_TOKEN_URL)) {
return chain.filter(exchange);
}
// 刷新token,直接向下执行
String grantType = request.getQueryParams().getFirst("grant_type");
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {
return chain.filter(exchange);
}
Class inClass = String.class;
Class outClass = String.class;
ServerRequest serverRequest = ServerRequest.create(exchange,
messageReaders);
// 解密生成新的报文
Mono> modifiedBody = serverRequest.bodyToMono(inClass)
.flatMap(decryptAES());
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(
exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequest decorator = decorate(exchange, headers,
outputMessage);
return chain
.filter(exchange.mutate().request(decorator).build());
}));
};
}
(PasswordDecoderFilter关键源码)
ValidateCodeGatewayFilter分析:
网关提供了验证码的实现,在RouterFunctionConfiguration中对/code接口提供了imageCodeHandler对象,用于生成验证码:
@Bean
public RouterFunction routerFunction() {
return RouterFunctions.route(
RequestPredicates.path("/code")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), imageCodeHandler)
.andRoute(RequestPredicates.GET("/swagger-resources")
.and(RequestPredicates.accept(MediaType.ALL)), swaggerResourceHandler)
.andRoute(RequestPredicates.GET("/swagger-resources/configuration/ui")
.and(RequestPredicates.accept(MediaType.ALL)), swaggerUiHandler)
.andRoute(RequestPredicates.GET("/swagger-resources/configuration/security")
.and(RequestPredicates.accept(MediaType.ALL)), swaggerSecurityHandler);
}
(RouterFunctionConfiguration关键源码)
ValidateCodeGatewayFilter的作用是在登录请求中获取用户输入的验证证参数,验证用户输入是否正确
三、与外部请求相关的权限功能设计
3.1 服务层面的外部请求权限控制
对于外部请求内部资源,除非是不需要权限控制的资源接口,否则我们开发的新微服务模块都应该依赖平台的pigx-common-security组件:
com.pig4cloud
pigx-common-security
该组件结合pigx-upms-api与spring security oauth2框架进行了封装,从而实现系统用户与权限的通关:
com.pig4cloud
pigx-common-core
org.springframework.cloud
spring-cloud-starter-oauth2
io.github.openfeign
feign-core
com.pig4cloud
pigx-upms-api
(pigx-common-security的pom.xml依赖)
引入pigx-common-security组件以后,nacos配置中心需要配置client-id、client-secret、scope:
## spring security 配置
security:
oauth2:
client:
client-id: ENC(gPFcUOmJm8WqM3k3eSqS0Q==)
client-secret: ENC(gPFcUOmJm8WqM3k3eSqS0Q==)
scope: server
我们所写的每个微服务都是一个client,对应在后台“终端管理”中进行设置:
因此每个微服务在引入pigx-common-security依赖以后,处理外部、非登录请求时,除非请求地址已加入白名单,否则都需要在Auth中认证请求访问者的身份:
(API访问过程中,token的认证过程)
以访问Service服务请求为例,过程如下:
- 客户端通过带token字符串的请求通过网关(Gateway)访问后端API
- 网关将请求路由到具体对应业务服务(Service)
- 业务服务(Service)首先会请求认证服务(Auth)来验证token
- token验证成功后请求进入具体接口请求逻辑中
我们系统中有不同的服务会拿token去访问Auth服务进行认证,来判断请求是否合法:
而判断请求是否合法(即/oauth/check_token)的过程中,不同服务中配置的不同client_id与client_secret,就起到了目标应用认证用户请求时本身目标应用认证的作用,这是因为Auth服务是OAuth2协议的实现,OAuth2协议把所有对自身的请求做为不同的client来源来对待,可以在sys_oauth_client_details表中看到client分布情况:
如此一来,pigx中不同目标应用对应与Auth中client的关系如下(注:以上只列出部分应用):
值得一提的是,前端登录时的认证请求通过网关直接访问Auth服务也是属于一种client来源(client_id : pigx)
3.2 功能层面的外部请求权限控制
外部请求通过了服务层面的权限控制以后,还有更细化的功能(接口)层面的权限控制
在pigx可设置用户->角色->菜单(权限)关系:
在“用户管理”功能中,可对用户“编辑”操作,进行角色设定:
在"角色管理"功能中,可对角色“+权限”操作,进行权限设置:
每个权限菜单(sys_menu)对应有一个“permission”字段,用于功能层面的权限控制
因为Spring Security Oauth2是基于Spring Security的,因此自然采用了Spring Security中的@PreAuthorize注解完成对接口访问权限的控制
@PreAuthorize通过指定PermissionService类的hasPermission()方法进行具体访问控制:
PermissionService关键代码片段:
public boolean hasPermission(String... permissions) {
if (ArrayUtil.isEmpty(permissions)) {
return false;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return false;
}
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
return authorities.stream()
.map(GrantedAuthority::getAuthority)
.filter(StringUtils::hasText)
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
}
因此在以上基础之上,只需接口方法添加@PreAuthorize注解,即可实现功能层面的权限控制,如:
hasPermission()方法在第5行从SecurityContextHolder.getContext().getAuthentication()中取得了用户信息,该信息是由OAuth2AuthenticationProcessingFilter过滤器放入其中的,追溯操作权限获取过程如下:
OAuth2AuthenticationProcessingFilter过滤器就是实现3.1节中讲到服务层面鉴权时的主要逻辑:
通过doFilter()方法对请求过滤处理,处理逻辑会访问OAuth2AuthenticationManager.authenticate()方法,authenticate()方法实际是访问RemoteTokenServices的loadAuthentication()方法,RemoteTokenServices是ResourceServerTokenServices接口的远程访问方式实现,实际请求到了Auth服务的/oauath/check_token接口,该接口专用于对token验证的支持
(RemoteTokenServices的loadAuthentication()方法)
/oauth/check_token 接口的checkToken()方法实现中:
- ResourceServerTokenServices接口采用DefaultTokenServices类实现,该类中包含TokenStore接口对象,该对象使用RedisTokenStore实现
- 通过跟踪发得在验证token后,会从redis中拿出authentication相关的信息,其中就附带了authorities信息,该信息是用户token对应的接口访问控制权限(80条)
由此可见,3.1节中服务层面的权限鉴定操作(/oauth/check_token)完成后,从用户会话的上下文中便可以取得功能(接口)层面的权限信息(SecurityContextHolder.getContext().getAuthentication()),即功能层面的权限控制是基于服务层面权限控制之上的,其条件为:用户已登录、请求带token并验证通过、用户角色权限已添加
四、与内部请求相关的权限功能设计
假如我们当前开发的业务主体为某个微服务模块,那么我们编写的接口服务将会接受到两类请求:
- 从外部经过网关路由而来的外部请求
- 从内部其它服务通过RestTemplate、Netty等方式而来的内部请求
其中外部请求是最常规的权限控制,以上第三节已经进行说明
而内部请求,在pigx中有结合网关对此做专门的设计,其中网关设计部分主要是PigxRequestGlobalFilter,已经进行说明,除此之外就是@Inner注解与PigxSecurityInnerAspect切面,下面进行说明:
@Inner注解定义如下:
/**
* @author Pigx
* @date 2019/4/13
*
* 服务调用鉴权注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {
/**
* 是否AOP统一处理
*
* @return false, true
*/
boolean value() default true;
/**
* 需要特殊判空的字段(预留)
*
* @return {}
*/
String[] field() default {};
}
对于只允许内部系统访问的接口,应添加@Inner注解:
此时如果我们通过外部调用此接口,将会被拒绝:
PigxSecurityInnerAspect负责对切面处理外部访问带有@Inner注解的接口时,做权限拒绝处理:
@Slf4j
@Aspect
@AllArgsConstructor
public class PigxSecurityInnerAspect {
private final HttpServletRequest request;
@SneakyThrows
@Around("@annotation(inner)")
public Object around(ProceedingJoinPoint point, Inner inner) {
String header = request.getHeader(SecurityConstants.FROM);
if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
log.warn("访问接口 {} 没有权限", point.getSignature().getName());
throw new AccessDeniedException("Access is denied");
}
return point.proceed();
}
}
利用@Inner注解添加接口权限白名单(ignore urls)
@Inner注解除了可以用于防止外部请求访问,还可以为接口起到添加白名单的作用,只需在注解中加入false参数:
@Inner(false)
该参数默认为true时,做为内部调用接口,反之为false时,做可为外部调用无须鉴权的接口(对外公开资源,如商品浏览、官网等互联网接口)
五、登录认证功能设计
以上都是授权以后的权限控制逻辑,Spring Security提出了两个概念:认证与授权,其中授权可以理解为认证成功以后为client颁发证明(token)以及鉴定证明,而下面介绍的认证就是client为了获取证明(token)向Auth服务请求认证的过程:
认证过程场景
pigx的网关(Gateway)并没有对认证与授权过程做太多业务处理,只是简单的将登录请求进行了特殊的对待,配合内部请求权限去除SecurityConstants.FROM参数,其它请求处理都是一视同仁
Auth服务在基于Spring Security OAuth2基础上对/oauth/authorize做了处理,大部分情况下只需要提供配置及一部分简单实现就能实现授权与认证,Spring Security OAuth2的使用基本上是按官方标准方式来实现的,这里就不再赘述了,有兴趣的可自行研究
六、总结
总结一下pigx平台中的权限体系,按应用功能分为有以下三部分:
- Spring Security:认证与授权
- Spring Security OAuth2:基于Spring Security之上实现OAuth2协议
- pigx-common-security:基于Spring Security OAuth2之上,封装成pigx平台专用安全组件,并提供@Inner @PreAuthorizet等更细精的权限控制
或者按鉴权的不同分为三类:
- 其它应用请求Auth服务功能时,Auth服务要求的应用提供的client级别鉴权(对应后台“终端管理”中添加)
- 应用从Auth认证成功拿到授权以后,再来请求后台服务接口时的会话级鉴权(token),/oauth/check_token接口
- 应用顺利通过上一步会话级鉴权以后,进入pigx提供的应用级鉴权(对应后台“用户、角色、权限”的配置、代码中@PreAuthorize、@Inner的编写)