分布式工程搭建--spring cloud alibaba(spring security网关鉴权)

  • Nacos-注册中心搭建

1.1 注册中心Nacos与Eureka对比

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在此项目中naocs服务器是通过mysql来进行连接的,nacos不用手动搭建服务器,对于开发者来说,上手很快。

1.2 Nacos安装和启动

nacos 的下载和启动方法请参考Nacos 官网。
在启动nacos2.01的时候,有个坑,默认启动方式是以集群的方式启动,需要修改, 直接使用命令启动
startup.sh -m standalone
1.3 注册中心客户端搭建
Pom依赖导入:

<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

注解添加:
注解添加,交给nacos注册中心管理服务@EnableDiscoveryClient.

Yml文件配置添加:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8000

交给注册中心管理之后的服务会在nacos服务器上的服务列表多出一个服务,可以实时查看服务状态。
在这里插入图片描述
2. Nacos-配置中心搭建

2.1 配置中心Nacos与Config区别
搭建:config需要手动搭建服务,nacos不需要。
动态变更:spring cloud config大部分场景结合git 使用, 动态变更还需要依赖Spring Cloud Bus 消息总线来通过所有的客户端变化.
nacos config使用长连接更新配置, 一旦配置有变动后,通知Provider的过程非常的迅速, 从速度上秒杀springcloud原来的config几条街.

2.2 配置中心客户端搭建
pom依赖:

<!--nacos config client 依赖-->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

客户端不需要添加注解,导入依赖之后会自动加载配置文件中配置中的的地址。
客户端Yml文件配置:

spring:
  cloud:
    nacos:
      config:
        enabled: true # 如果不想使用 Nacos 进行配置管理,设置为 false 即可
        server-addr: 127.0.0.1:8000 # Nacos Server 地址
        prefix: test-application
        file-extension: yml # 配置内容的数据格式,默认为 properties
        group: pull # 组,默认为 DEFAULT_GROUP

存储在配置中心的文件:
在这里插入图片描述
3. Gateway-网关搭建

3.1 网关Gateway与Zuul区别
在这里插入图片描述
3.2 网关gateway-server搭建

  1. 将此服务交给nacos管理
  2. 配置信息从配置中心拉取
  3. 网关依赖导入:
<!--GateWay 网关-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  1. 配置各个服务的路由:
spring:
  cloud:
    gateway:
      routes:
        - id: test1-server
          uri: http://localhost:9100
          predicates:
            - Path=/test1/**
          filters:
            - StripPrefix=1
        - id: test2-server
          uri: http://localhost:9101
          predicates:
            - Path=/test2/**
            -           filters:
            - StripPrefix=1
        - id: test3-server
          uri: http://localhost:9102
          predicates:
            - Path=/test3/**
          filters:
            - StripPrefix=1
  1. Dubbo-服务间调用搭建
    pom文件依赖导入:
<!-- Dubbo Spring Cloud Starter -->
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>

dubbo服务提供者+注解@service(此注解为dubbo的注解):

dubbo:
  scan:
    base-packages: com.dc.dubbo.provider
  protocol:
    name: dubbo
    port: -1
  registry:
    address: nacos://127.0.0.1:8000

Nacos配置文件服务消费者(gateway):

dubbo:
  cloud:
    subscribed-services: test-server
  registry:
    address: nacos://127.0.0.1:8000
  consumer:
    check: false
    timeout: 20000

使用dubbo产生的服务列表:
在这里插入图片描述
5. Spring Cloud Security
鉴权解决方案:在网关(gateway)集成security实现微服务统一鉴权.
优势:简单,省去了微服务之间的鉴权操作.
引入security包:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-security</artifactId>
</dependency>

5.1. 配置security安全策略

@EnableWebFluxSecurity
public class SecurityConfig {
  // 登录接口地址
  private static final String LOGIN_URI = "/login";
  // 放行的uri地址(所有请求方式)
  private static final String[] EXCLUDED_AUTH_URI_ALL_METHOD = {
      "/plugins/api/v1/test"
  };
  // 放行的uri地址(GET)
  private static final String[] EXCLUDED_AUTH_URI_GET_METHOD = {
      "/plugins/api/v1/test2"
  };

  @Resource
  private MyAuthorizationManager authorizationManager;
  @Resource
  private MyServerAuthenticationSuccessHandler serverAuthenticationSuccessHandler;
  @Resource
  private MyServerAuthenticationFailureHandler serverAuthenticationFailureHandler;
  @Resource
  private MyServerAuthenticationEntryPoint serverAuthenticationEntryPoint;
  @Resource
  private MyServerAccessDeniedHandler serverAccessDeniedHandler;
  @Resource
  private MyReactiveAuthenticationManager reactiveAuthenticationManager;
  @Resource
  private MyCorsConfigurationSource corsConfigurationSource;
  @Resource
  private MyServerSecurityContextRepository securityContextRepository;

  /**
   * 配置安全策略
   *
   * @param http http安全请求配置对象
   * @return org.springframework.security.web.server.SecurityWebFilterChain
   * @author Reagan 
   */
  @Bean
  SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
        //无需进行权限过滤的请求路径
        .pathMatchers(EXCLUDED_AUTH_URI_ALL_METHOD).permitAll()
        .pathMatchers(HttpMethod.GET, EXCLUDED_AUTH_URI_GET_METHOD).permitAll()
        //option 请求默认放行
        .pathMatchers(HttpMethod.OPTIONS).permitAll()
        //自定义的鉴权服务,通过鉴权的才能继续访问某个请求
        .anyExchange()
        .access(authorizationManager)
        .and()
        .httpBasic()
        // 登录接口地址配置
        .and().formLogin().loginPage(LOGIN_URI)
        // 认证管理器
        .authenticationManager(reactiveAuthenticationManager)
        //认证成功
        .authenticationSuccessHandler(serverAuthenticationSuccessHandler)
        //认证失败
        .authenticationFailureHandler(serverAuthenticationFailureHandler)
        .and()
        .exceptionHandling()
        //基于http的接口请求鉴权失败
        .authenticationEntryPoint(serverAuthenticationEntryPoint)
        .accessDeniedHandler(serverAccessDeniedHandler)
        .and()
        .securityContextRepository(securityContextRepository)
        .logout().disable()
        .csrf().disable().cors().configurationSource(corsConfigurationSource)
        .and()
        .headers().cache().disable();
    return http.build();
  }M
}

  1. @EnableWebFluxSecurity注解:开启security.
  2. MyReactiveAuthenticationManager: 自定义认证管理器,查询用户,校验密码.
  3. MyServerAuthenticationSuccessHandler: 自定义认证成功处理器,认证成功返回token.
  4. MyServerAuthenticationFailureHandler: 自定义认证失败处理器, 认证失败返回异常信息.
  5. MyServerAuthenticationEntryPoint: 自定义认证入口,没有认证的用户访问资源接口,引导用户前往登录(前后分离,直接返回401由交给前端引导登录).
  6. MyAuthorizationManager: 自定义鉴权管理, 用户权限决策.
  7. MyServerAccessDeniedHandler: 自定义鉴权失败处理器,用户无权访问资源时的处理方式(提示用户无权限).
  8. MyCorsConfigurationSource: 跨域管理.
  9. MyServerSecurityContextRepository:自定义security上下文管理仓库.

5.2. MyReactiveAuthenticationManager认证管理

@Component
public class MyReactiveAuthenticationManager implements ReactiveAuthenticationManager {

  @Resource
  private SecurityUserService securityUserService;
  @Resource
  private AbstractAuthenticationCheck authenticationCheck;

  @Override
  public Mono<Authentication> authenticate(Authentication authentication) {
    // 获取表单提交的用户名和密码
    String username= authentication.getName();
    String password = authentication.getCredentials().toString();
    return securityUserService.findByUsername(username).publishOn(Schedulers.parallel())
        // 过滤掉为null的
        .filter(Objects::nonNull)
        // 如果没有Mono里面元素了就说明没有查询到用户,抛出错误信息
        .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("用户没有找到:" + username))))
        // 校验用户信息, 并返回验证信息
        .map(userDetails -> {
          // 用户信息校验
          authenticationCheck.check(userDetails, password);
          // 校验通过,组装验证信息
          return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        });
  }
}
  1. securityUserService.findByUsername(email) 根据用户名查询用户信息.
  2. authenticationCheck.check(userDetails, password); 自定义用户信息校验.

5.3. MyServerAuthenticationSuccessHandler认证成功处理

@Component
public class MyServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {

  @Reference
  private UserService userService;
  @Resource
  private RedisTemplate<String, Authentication> redisTemplate;
  @Resource
  private LoginConfigProperties loginConfigProperties;

  @Override
  public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
    // 取用户信息
    SecurityUser securityUser = ((SecurityUser) authentication.getPrincipal());
    String userName= securityUser.getUserName();
    String relName= securityUser.getRelName();
    // 取角色列表
    String roleCodes = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining());
    // 生成token
    String token = SecurityUtils.makeToken();
    // 存储身份信息
    redisTemplate.opsForValue().set(token, authentication, loginConfigProperties.getTimeout(), TimeUnit.MINUTES);

    // 查询角色有权限的菜单
    List<Integer> roleIdList = securityUser.getShopifyRoleList().stream().map(ShopifyRole::getId).collect(Collectors.toList());
    List<ShopifyMenu> shopifyMenuList = userService.findMenuByRoleIds(roleIdList);

    // 组装响应的数据
    JSONObject result = new JSONObject();
    result.put("token", token);
    result.put("username", userName);
    result.put("relName", relName);
    result.put("role", roleCodes);
    result.put("menu", shopifyMenuList);
    // 写响应并返回
    return ResponseUtils.write(webFilterExchange, BaseVo.success(result));
  }
}
  1. loginConfigProperties.getTimeout() 读取登录过期时间
  2. userService.findMenuByRoleIds 查询角色对应的菜单
  3. ResponseUtils.write() 写响应的工具类
  4. SecurityUser 是UserDetail(security框架中的用户信息类)的子类

5.4. MyServerAuthenticationFailureHandler认证失败处理

@Component
public class MyServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {

  @Override
  public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
    BaseVo<?> fail = BaseVo.fail(exception.getMessage());
    // 取响应对象
    return ResponseUtils.write(webFilterExchange, fail);
  }
}
  1. ResponseUtils.write() 写响应的工具类
  2. 认证失败直接返回错误信息

5.5. MyServerAuthenticationEntryPoint认证入口

@Component
public class MyServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

  @Override
  public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
    return Mono.fromRunnable(() -> exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED));
  }
}
  1. 直接返回401交给前端引导用户登录.

5.6. MyAuthorizationManager鉴权管理

@Component
public class MyAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

  private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

  @Reference
  private UserService userService;

  @Override
  public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
    return authentication
        // 过滤掉没有认证过的
        .filter(Authentication::isAuthenticated)
        // 授权抉择
        .map(auth -> decision(auth, authorizationContext));
  }

  /**
   * 决策
   *
   * @param authentication       认证信息
   * @param authorizationContext 认证上下文(请求+响应【)
   * @return org.springframework.security.authorization.AuthorizationDecision
   * @author Reangan 下午
   */
  private AuthorizationDecision decision(Authentication authentication, AuthorizationContext authorizationContext) {
    // 取请求对象
    ServerHttpRequest request = authorizationContext.getExchange().getRequest();
    // 取地址
    String requestUrl = request.getURI().getPath();
    // 取权限列表(角色代码)
    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
    // 转成角色代码
    List<String> roleCodeList = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
    // 权限验证
    boolean isGranted = userService.findPermissionByRole(roleCodeList)
        .stream()
        // 取permission uri
        .map(ShopifyPermission::getPermissionUrl)
        // 有一个匹配就算是鉴权通过
        .anyMatch(uri -> ANT_PATH_MATCHER.match(uri, requestUrl));
    // 如果授权通过,把用户信息设置到request
    if (isGranted) {
      // 取用户信息
      SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
      addUserInfo(authorizationContext, securityUser);
    }
    // 返回授权对象
    return new AuthorizationDecision(isGranted);
  }

  /**
   * 添加用户信息
   *
   * @param authorizationContext 认证上下文
   * @param securityUser         用户信息
   * @author Reagan 下午
   */
  private void addUserInfo(AuthorizationContext authorizationContext, SecurityUser securityUser) {
    ServerWebExchange exchange = authorizationContext.getExchange();
    ServerHttpRequest request = exchange.getRequest();
    ServerHttpRequest.Builder mutate = request.mutate();
    String userInfo = Base64.getEncoder().encodeToString(ByteUtils.objToByte(toUserModel(securityUser)));
    mutate.header(UserUtils.USERINFO, new String[]{userInfo});
    ServerHttpRequest build = mutate.build();
    exchange.mutate().request(build).build();
  }

  /**
   * 对象转换,SecurityUser -> UserModel
   *
   * @param securityUser security用户
   * @return com.witemedia.model.UserModel
   * @author Reagan  下午
   */
  private UserModel toUserModel(SecurityUser securityUser) {
    UserModel userModel = new UserModel();
    userModel.setShopifyRoleList(securityUser.getShopifyRoleList());
    userModel.setId(securityUser.getId());
    userModel.setEmail(securityUser.getEmail());
    userModel.setUserStatus(securityUser.getUserStatus());
    userModel.setUsername(securityUser.getUsername());
    userModel.setEncryptedPassword(securityUser.getEncryptedPassword());
    userModel.setMobilePhone(securityUser.getMobilePhone());
    userModel.setLastPasswordChangeTime(securityUser.getLastPasswordChangeTime());
    userModel.setPasswordStatusint(securityUser.getPasswordStatusint());
    userModel.setUpdateTime(securityUser.getUpdateTime());
    return userModel;
  }
  1. userService.findPermissionByRole(roleCodeList) 权限查询
  2. UserModel是自定义的远程传输对象,用于传输用户信息的

5.7 MyServerAccessDeniedHandler鉴权失败处理

@Component
public class MyServerAccessDeniedHandler implements ServerAccessDeniedHandler {
  @Override
  public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
    BaseVo<?> fail = BaseVo.result(RtnCode.AUTH_VALID_ERROR.getCode(), RtnCode.AUTH_VALID_ERROR.getMessage());
    return ResponseUtils.write(exchange.getResponse(), fail);
  }
}
  1. 直接返回鉴权失败的信息(权限不足)

6.8. MyCorsConfigurationSource跨域管理
实现CorsConfigurationSource接口,然后自定义配置,示例:

@Component
public class MyCorsConfigurationSource implements CorsConfigurationSource {

  // 允许的源
  private static final List<String> ALLOWED_ORIGINS;
  // 允许的请求方式
  private static final List<String> ALLOWED_METHODS;
  // 暴露给客户端的响应头
  private static final List<String> EXPOSED_HEADERS;
  // 允许的请求头,用户预检请求
  private static final List<String> ALLOWED_HEADERS;

  static {
    // 允许的源
    ALLOWED_ORIGINS = Arrays.asList(“”);
    // 允许的请求方式
    ALLOWED_METHODS = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS");
    // 暴露给客户端的响应头
    EXPOSED_HEADERS = Arrays.asList("Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma");
    // 允许的请求头,用户预检请求
    ALLOWED_HEADERS = Arrays.asList("Accept", "Authorization", "Content-Type", "Origin", "X-Requested-With");
  }

  @Override
  public CorsConfiguration getCorsConfiguration(@NonNull ServerWebExchange exchange) {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.setAllowedOrigins(ALLOWED_ORIGINS);
    corsConfiguration.setAllowedMethods(ALLOWED_METHODS);
    corsConfiguration.setExposedHeaders(EXPOSED_HEADERS);
    corsConfiguration.setAllowedHeaders(ALLOWED_HEADERS);
    corsConfiguration.setMaxAge(3600L);
    return corsConfiguration;
  }
}

5.9. MyServerSecurityContextRepository上下文仓库

@Component
public class MyServerSecurityContextRepository implements ServerSecurityContextRepository {

  private static final String AUTHORIZATION = "Authorization";

  @Resource
  private RedisTemplate<String, Authentication> redisTemplate;
  @Resource
  private LoginConfigProperties loginConfigProperties;

  @Override
  public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
    return Mono.empty();
  }

  @Override
  public Mono<SecurityContext> load(ServerWebExchange exchange) {
    ServerHttpRequest request = exchange.getRequest();
    exchange.getResponse().setStatusCode(HttpStatus.OK);
    // 获取请求头中的token
    String token = request.getHeaders().getFirst(AUTHORIZATION);
    if (token != null && !"".equals(token)) {
      // 取用户信息(redis)
      Boolean isExpire = redisTemplate.hasKey(token);
      // 判断token是否过期
      if (Boolean.TRUE.equals(isExpire)) {
        // 用token去redis取令牌
        Authentication authentication = redisTemplate.opsForValue().get(token);
        // 刷新缓存时间
        redisTemplate.expire(token, loginConfigProperties.getTimeout(), TimeUnit.MINUTES);
        // 把令牌传给Security
        SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
        emptyContext.setAuthentication(authentication);
        return Mono.just(emptyContext);
      } else {
        // 返回token已过期
        return Mono.error(new LoadContextException(RtnCode.UNLOGIN_ERROR.getCode(), RtnCode.UNLOGIN_ERROR.getMessage()));
      }
    }
    return Mono.error(new LoadContextException("401", "找不到身份凭据"));
  }
}

从redis中加载用户信息,用于鉴权

  1. new LoadContextException() 自定义异常,在全局异常中拦截处理,表示加载用户信息失败(也算是鉴权失败)

暂时写到这里,这里的spring security鉴权摸索了一下,webflux框架可以深度再研究一下。

  • 13
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值