概述

在jpaas 平台中,微服务的认证和授权的实现方式,采用了统一接入的机制,之后系统的接入都需要遵循这套机制来实现。
平台的安全认证是基于Spring Security 5 上实现的认证,一旦用户登录完成后,会把用户信息进行相应的存储,以方便后面的不同的微应用进行使用,那么网关如何知道当前用户登录已经登录了,并且可以访问哪些应用与哪些功能。

【说明】
平台中的角色如下

  • 前端应用
    前端负责从从后端获取数据,在前端进行展示。

  • 网关
    负责请求转发,负载均衡,限流,检查TOKEN是否合法

  • 认证服务器
    负责认证用户,分发访问令牌

  • 微应用
    最终的数据返回者,微应用需要负责token是否有权限访问接口,另外如果通过feign 访问访问其他的微服务的时候,为了性能考虑,就不要在进行授权认证了。

安全管理配置入口

平台是由网关统一对外访问的,因此网关承担所有的访问的入口的安全拦截的第一道关口,因此所有的访问均需要通过网关进行转发,因此,我们配置了Spring Securityr的 安全拦截串。

  1. package com.redxun.gateway.config;
  2. import com.redxun.gateway.auth.*;
  3. import com.redxun.oauth2.common.properties.SecurityProperties;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.http.HttpMethod;
  8. import org.springframework.security.authentication.ReactiveAuthenticationManager;
  9. import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
  10. import org.springframework.security.config.web.server.ServerHttpSecurity;
  11. import org.springframework.security.oauth2.provider.token.TokenStore;
  12. import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
  13. import org.springframework.security.web.server.SecurityWebFilterChain;
  14. import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
  15. import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
  16. /**
  17. * 资源服务器配置
  18. *
  19. * @author yjy
  20. * @date 2019/10/5
  21. * <p>
  22. */
  23. @Configuration
  24. public class ResourceServerConfiguration {
  25. @Autowired
  26. private SecurityProperties securityProperties;
  27. @Autowired
  28. private TokenStore tokenStore;
  29. @Autowired
  30. private PermissionAuthManager permissionAuthManager;
  31. /**
  32. * Spring Security的安全认证配置入口
  33. *
  34. * @param http
  35. * @return
  36. */
  37. @Bean
  38. SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
  39. //认证处理器
  40. ReactiveAuthenticationManager customAuthenticationManager = new CustomAuthenticationManager(tokenStore);
  41. JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint();
  42. //token转换器
  43. ServerBearerTokenAuthenticationConverter tokenAuthenticationConverter = new ServerBearerTokenAuthenticationConverter();
  44. tokenAuthenticationConverter.setAllowUriQueryParameter(true);
  45. //oauth2认证过滤器
  46. AuthenticationWebFilter oauth2Filter = new AuthenticationWebFilter(customAuthenticationManager);
  47. oauth2Filter.setServerAuthenticationConverter(tokenAuthenticationConverter);
  48. oauth2Filter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
  49. oauth2Filter.setAuthenticationSuccessHandler(new Oauth2AuthSuccessHandler());
  50. http.addFilterAt(oauth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);
  51. //Security的安全配置
  52. ServerHttpSecurity.AuthorizeExchangeSpec authorizeExchange = http.authorizeExchange();
  53. //加上授权的访问地址
  54. if (securityProperties.getAuth().getHttpUrls().length > 0) {
  55. authorizeExchange.pathMatchers(securityProperties.getAuth().getHttpUrls()).authenticated();
  56. }
  57. //加入忽略的访问地址
  58. if (securityProperties.getIgnore().getUrls().length > 0) {
  59. authorizeExchange.pathMatchers(securityProperties.getIgnore().getUrls()).permitAll();
  60. }
  61. authorizeExchange
  62. .pathMatchers(HttpMethod.OPTIONS).permitAll()
  63. .anyExchange()
  64. // 任何资源除了忽略与认证可访问的外,都是需要进行授权判断才能放行,放行的认证处理器为permissionAuthManager
  65. .access(permissionAuthManager)
  66. .and()
  67. .exceptionHandling()
  68. .accessDeniedHandler(new JsonAccessDeniedHandler())
  69. .authenticationEntryPoint(entryPoint)
  70. .and()
  71. .headers()
  72. .frameOptions()
  73. .disable()
  74. .and()
  75. .httpBasic().disable()
  76. .csrf().disable();
  77. return http.build();
  78. }
  79. }

【说明】
AuthenticationWebFilter为平台的对需要授权认证的地址的拦截器,平台的URL授权自定义的认证实现即是基于其内部的认证处理器customAuthenticationManager,令牌的处理器为tokenAuthenticationConverter

平台的访问认证及资源授权原理

登录判断

AUTHENTICATION(登录认证)表示平台如何判断用户是否登录了,其是通过AuthenticationWebFilter来实现的。
而AuthenticationWebFilter则借助customAuthenticationManager来判断当前用户是否登录认证。因此只需要了解关注customAuthenticationManager的实现即可。

customAuthenticationManager的实现方法如下所示:

  1. package com.redxun.gateway.auth;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.security.authentication.ReactiveAuthenticationManager;
  4. import org.springframework.security.core.Authentication;
  5. import org.springframework.security.oauth2.common.OAuth2AccessToken;
  6. import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
  7. import org.springframework.security.oauth2.provider.OAuth2Authentication;
  8. import org.springframework.security.oauth2.provider.token.TokenStore;
  9. import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
  10. import reactor.core.publisher.Mono;
  11. /**
  12. * 用于进行网关的用户登录认证判断
  13. * @author yjy
  14. * @date 2019/10/6
  15. * <p>
  16. */
  17. @Slf4j
  18. public class CustomAuthenticationManager implements ReactiveAuthenticationManager {
  19. private TokenStore tokenStore;
  20. public CustomAuthenticationManager(TokenStore tokenStore) {
  21. this.tokenStore = tokenStore;
  22. }
  23. @Override
  24. public Mono<Authentication> authenticate(Authentication authentication) {
  25. return Mono.justOrEmpty(authentication)
  26. .filter(a -> a instanceof BearerTokenAuthenticationToken) // 平台登录时,使用了该类型的令牌实现
  27. .cast(BearerTokenAuthenticationToken.class)
  28. .map(BearerTokenAuthenticationToken::getToken)
  29. .flatMap((accessTokenValue -> {
  30. //从缓存中读取该令牌
  31. OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
  32. //无令牌则表示没有登录或令牌已经失效
  33. if (accessToken == null) {
  34. return Mono.error(new InvalidTokenException("tokenInvalid"));
  35. } else if (accessToken.isExpired()) {//令牌过期则需要从缓存中移除
  36. tokenStore.removeAccessToken(accessToken);
  37. return Mono.error(new InvalidTokenException("tokenExpried"));
  38. }
  39. // 从令牌中获取认证对象
  40. OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
  41. // 检查是否已经认证了
  42. if (result == null || !result.isAuthenticated()) {
  43. return Mono.error(new InvalidTokenException("tokenInvalid"));
  44. }
  45. return Mono.just(result);
  46. }))
  47. .cast(Authentication.class);
  48. }
  49. }

url 拦截认证

在以上配置中有一块是以下代码的配置,表示访问的任何URL是需要认证访问:

  1. authorizeExchange
  2. .pathMatchers(HttpMethod.OPTIONS).permitAll()
  3. .anyExchange()
  4. // 任何资源除了忽略与认证可访问的外,都是需要进行授权判断才能放行,放行的认证处理器为permissionAuthManager
  5. .access(permissionAuthManager)

因此我们只需要了解permissionAuthManager是如何判断url的是合法访问即可。

  1. /**
  2. * url拦截权限认证
  3. *
  4. * @author yjy
  5. * @author csx
  6. * @date 2019/10/6
  7. * <p>
  8. */
  9. @Slf4j
  10. @Component
  11. public class PermissionAuthManager extends DefaultPermissionServiceImpl implements ReactiveAuthorizationManager<AuthorizationContext> {
  12. /**
  13. * 构建URL_GROUP映射
  14. */
  15. public static final String apiUrlGroup = "apiMap_";
  16. @Resource
  17. private RemoteApiService remoteApiService;
  18. @Override
  19. public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
  20. return authentication.map(auth -> {
  21. ServerWebExchange exchange = authorizationContext.getExchange();
  22. ServerHttpRequest request = exchange.getRequest();
  23. //检查当前的认证对象是否具有当前访问路径的访问权限
  24. boolean isPermission = super.hasPermission(auth, request.getMethodValue(), request.getURI().getPath());
  25. return new AuthorizationDecision(isPermission);
  26. }).defaultIfEmpty(new AuthorizationDecision(false));
  27. }
  28. /**
  29. * 获取URL与用户组Id的映射,并且放置于缓存中
  30. * @return
  31. */
  32. @Override
  33. public Map<String, Set<String>> selectApisByGroupIdsAndRedis(){
  34. Map<String, Set<String>> menuMap=(Map<String, Set<String>>) CacheUtil.get("api",apiUrlGroup);
  35. if(menuMap==null){
  36. menuMap = remoteApiService.getUrlGroupIdMap();
  37. CacheUtil.set("api",apiUrlGroup,menuMap);
  38. }
  39. return menuMap;
  40. }
  41. }

【说明】
以上的DefaultPermissionServiceImpl类的hasPermission为关键判断,原理比较简单,就是判断在缓存中URL-Group的映射中找到该URL对应的用户组Id,与当前登录的用户所属的用户组(如部门、岗位、职务等)匹配对比,若存在,则表示可授权访问。为了实现这个业务逻辑,平台需要在用户组与资源映射上做一些数据结构的逻辑处理。

平台的安全管理是基于组的权限授权策略,即平台是对组进行授权的,组包括: 机构类型,用户类型,角色,职务,部门或其他组授权,而用户可分别属于多个不同的组。用户在登录时,即会根据其所在的组进行所有的权限合并,从而决定用户可访问的系统资源有哪些。其关系如下所示:

文档更新时间: 2021-08-28 12:35   作者:csx