概述
在jpaas 平台中,微服务的认证和授权的实现方式,采用了统一接入的机制,之后系统的接入都需要遵循这套机制来实现。
平台的安全认证是基于Spring Security 5 上实现的认证,一旦用户登录完成后,会把用户信息进行相应的存储,以方便后面的不同的微应用进行使用,那么网关如何知道当前用户登录已经登录了,并且可以访问哪些应用与哪些功能。
【说明】
平台中的角色如下
前端应用
前端负责从从后端获取数据,在前端进行展示。网关
负责请求转发,负载均衡,限流,检查TOKEN是否合法认证服务器
负责认证用户,分发访问令牌微应用
最终的数据返回者,微应用需要负责token是否有权限访问接口,另外如果通过feign 访问访问其他的微服务的时候,为了性能考虑,就不要在进行授权认证了。
安全管理配置入口
平台是由网关统一对外访问的,因此网关承担所有的访问的入口的安全拦截的第一道关口,因此所有的访问均需要通过网关进行转发,因此,我们配置了Spring Securityr的 安全拦截串。
package com.redxun.gateway.config;
import com.redxun.gateway.auth.*;
import com.redxun.oauth2.common.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
/**
* 资源服务器配置
*
* @author yjy
* @date 2019/10/5
* <p>
*/
@Configuration
public class ResourceServerConfiguration {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private TokenStore tokenStore;
@Autowired
private PermissionAuthManager permissionAuthManager;
/**
* Spring Security的安全认证配置入口
*
* @param http
* @return
*/
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
//认证处理器
ReactiveAuthenticationManager customAuthenticationManager = new CustomAuthenticationManager(tokenStore);
JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint();
//token转换器
ServerBearerTokenAuthenticationConverter tokenAuthenticationConverter = new ServerBearerTokenAuthenticationConverter();
tokenAuthenticationConverter.setAllowUriQueryParameter(true);
//oauth2认证过滤器
AuthenticationWebFilter oauth2Filter = new AuthenticationWebFilter(customAuthenticationManager);
oauth2Filter.setServerAuthenticationConverter(tokenAuthenticationConverter);
oauth2Filter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
oauth2Filter.setAuthenticationSuccessHandler(new Oauth2AuthSuccessHandler());
http.addFilterAt(oauth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);
//Security的安全配置
ServerHttpSecurity.AuthorizeExchangeSpec authorizeExchange = http.authorizeExchange();
//加上授权的访问地址
if (securityProperties.getAuth().getHttpUrls().length > 0) {
authorizeExchange.pathMatchers(securityProperties.getAuth().getHttpUrls()).authenticated();
}
//加入忽略的访问地址
if (securityProperties.getIgnore().getUrls().length > 0) {
authorizeExchange.pathMatchers(securityProperties.getIgnore().getUrls()).permitAll();
}
authorizeExchange
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange()
// 任何资源除了忽略与认证可访问的外,都是需要进行授权判断才能放行,放行的认证处理器为permissionAuthManager
.access(permissionAuthManager)
.and()
.exceptionHandling()
.accessDeniedHandler(new JsonAccessDeniedHandler())
.authenticationEntryPoint(entryPoint)
.and()
.headers()
.frameOptions()
.disable()
.and()
.httpBasic().disable()
.csrf().disable();
return http.build();
}
}
【说明】
AuthenticationWebFilter为平台的对需要授权认证的地址的拦截器,平台的URL授权自定义的认证实现即是基于其内部的认证处理器customAuthenticationManager,令牌的处理器为tokenAuthenticationConverter
平台的访问认证及资源授权原理
登录判断
AUTHENTICATION(登录认证)表示平台如何判断用户是否登录了,其是通过AuthenticationWebFilter来实现的。
而AuthenticationWebFilter则借助customAuthenticationManager来判断当前用户是否登录认证。因此只需要了解关注customAuthenticationManager的实现即可。
customAuthenticationManager的实现方法如下所示:
package com.redxun.gateway.auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import reactor.core.publisher.Mono;
/**
* 用于进行网关的用户登录认证判断
* @author yjy
* @date 2019/10/6
* <p>
*/
@Slf4j
public class CustomAuthenticationManager implements ReactiveAuthenticationManager {
private TokenStore tokenStore;
public CustomAuthenticationManager(TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.justOrEmpty(authentication)
.filter(a -> a instanceof BearerTokenAuthenticationToken) // 平台登录时,使用了该类型的令牌实现
.cast(BearerTokenAuthenticationToken.class)
.map(BearerTokenAuthenticationToken::getToken)
.flatMap((accessTokenValue -> {
//从缓存中读取该令牌
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
//无令牌则表示没有登录或令牌已经失效
if (accessToken == null) {
return Mono.error(new InvalidTokenException("tokenInvalid"));
} else if (accessToken.isExpired()) {//令牌过期则需要从缓存中移除
tokenStore.removeAccessToken(accessToken);
return Mono.error(new InvalidTokenException("tokenExpried"));
}
// 从令牌中获取认证对象
OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
// 检查是否已经认证了
if (result == null || !result.isAuthenticated()) {
return Mono.error(new InvalidTokenException("tokenInvalid"));
}
return Mono.just(result);
}))
.cast(Authentication.class);
}
}
url 拦截认证
在以上配置中有一块是以下代码的配置,表示访问的任何URL是需要认证访问:
authorizeExchange
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange()
// 任何资源除了忽略与认证可访问的外,都是需要进行授权判断才能放行,放行的认证处理器为permissionAuthManager
.access(permissionAuthManager)
因此我们只需要了解permissionAuthManager是如何判断url的是合法访问即可。
/**
* url拦截权限认证
*
* @author yjy
* @author csx
* @date 2019/10/6
* <p>
*/
@Slf4j
@Component
public class PermissionAuthManager extends DefaultPermissionServiceImpl implements ReactiveAuthorizationManager<AuthorizationContext> {
/**
* 构建URL_GROUP映射
*/
public static final String apiUrlGroup = "apiMap_";
@Resource
private RemoteApiService remoteApiService;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
return authentication.map(auth -> {
ServerWebExchange exchange = authorizationContext.getExchange();
ServerHttpRequest request = exchange.getRequest();
//检查当前的认证对象是否具有当前访问路径的访问权限
boolean isPermission = super.hasPermission(auth, request.getMethodValue(), request.getURI().getPath());
return new AuthorizationDecision(isPermission);
}).defaultIfEmpty(new AuthorizationDecision(false));
}
/**
* 获取URL与用户组Id的映射,并且放置于缓存中
* @return
*/
@Override
public Map<String, Set<String>> selectApisByGroupIdsAndRedis(){
Map<String, Set<String>> menuMap=(Map<String, Set<String>>) CacheUtil.get("api",apiUrlGroup);
if(menuMap==null){
menuMap = remoteApiService.getUrlGroupIdMap();
CacheUtil.set("api",apiUrlGroup,menuMap);
}
return menuMap;
}
}
【说明】
以上的DefaultPermissionServiceImpl类的hasPermission为关键判断,原理比较简单,就是判断在缓存中URL-Group的映射中找到该URL对应的用户组Id,与当前登录的用户所属的用户组(如部门、岗位、职务等)匹配对比,若存在,则表示可授权访问。为了实现这个业务逻辑,平台需要在用户组与资源映射上做一些数据结构的逻辑处理。
平台的安全管理是基于组的权限授权策略,即平台是对组进行授权的,组包括: 机构类型,用户类型,角色,职务,部门或其他组授权,而用户可分别属于多个不同的组。用户在登录时,即会根据其所在的组进行所有的权限合并,从而决定用户可访问的系统资源有哪些。其关系如下所示: