Nop入门:集成使用SpringSecurity

与Spring集成的时候有些人希望沿用以前的登录认证机制,不想引入Nop平台的用户、角色表等,此时可以不引入nop-auth-service服务,然后适配少数几个接口即可。

示例工程可以参见 nop-spring-security-demo。

一. 引入nop-spring-web-starter依赖

这个包会使用SpringBoot的配置自发现机制启动Nop平台,并注册SpringGraphWebService。

<dependencies>
  <dependency>
    <groupId>io.github.entropy-cloud</groupId>
    <artifactId>nop-spring-web-starter</artifactId>
  </dependency>

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

然后在application.yaml配置文件中启用如下配置:

nop:
  auth:
    enable-data-auth: true
    enable-action-auth: true
    http-server-filter:
      enabled: false

这里启用了数据权限和操作权限校验,并禁用了内置的AuthHttpServerFilter,原先是由这个Filter负责检查登录状态。

二. 实现IActionAuthChecker接口

Nop平台内部使用IActionAuthChecker接口来检查操作权限,而SpringSecurity则是使用PermissionEvaluator接口

public class SpringActionAuthChecker implements IActionAuthChecker {
  static final Logger LOG = LoggerFactory.getLogger(SpringActionAuthChecker.class);

  @Inject
  PermissionEvaluator permissionEvaluator;

  @Override
  public boolean isPermitted(String permission, ISecurityContext context) {
    LOG.info("nop.check-action-auth:permission={},user={}", permission, context.getUserContext());

    if (context.getUserContext() == null)
      return false;
    IUserContext userContext = context.getUserContext();

    // 这里如何构造token需要根据具体授权框架的要求来
    UserIdToken token = new UserIdToken(userContext.getUserId());
    return permissionEvaluator.hasPermission(token, "BizObject", permission);
  }
}

一般情况下Nop平台中的permission格式为 {bizObjName}:{method},代码生成时会生成NopAuthUser:queryNopAuthUser:mutation这种权限标识,它们分别对应读操作(GraphQL的query)和写操作(GraphQL的mutation)。 自己根据业务情况可以进一步细化,比如将upload和download独立出来等。

当然,具体permission的格式并没有硬性要求,都是自己内部约定的,只要底层的PermissionEvaluator能够识别即可。

三. SpringSecurity登录成功后初始化Nop平台的用户上下文

 void onLoginSuccess(IContext context, HttpServletRequest request) {
      String userId = "nop";
      String userName = "123";

      // 这里应该按照具体框架要求设置token,这里仅仅是
      SecurityContext secureContext = SecurityContextHolder.getContext();
      secureContext.setAuthentication(new UserIdToken(userId));
      request.setAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME, secureContext);

      // context上保存了一些最基本的信息,IUserContext是更多的用户相关信息
      context.setUserId(userId);
      context.setUserName(userName);

      // 这里模拟登录成功后设置user上下文
      UserContextImpl userContext = new UserContextImpl();
      userContext.setAccessToken("aaa");
      userContext.setLastAccessTime(CoreMetrics.currentTimeMillis());
      userContext.setUserId(userId);
      userContext.setUserName(userName);
      userContext.setRoles(Set.of("manager", "checker"));

      IUserContext.set(userContext);
  }
  • SecurityContextHolder是SpringSecurity登录验证成功后保存安全上下文的地方。这里使用的UserIdToken仅仅是一个示例,具体保存什么内容要看具体登录框架的要求。
  • 为了支持异步调用,这里还需要把安全上下文保存到HttpServletRequest上,否则Controller返回异步结果时会出现上下文丢失的情况。

四. 调整SpringSecurity的配置

Nop平台的ContextHttpServerFilter需要在SpringSecurity的认证Filter之前执行,在认证Filter中会需要调用上面的onLoginSuccess函数注册用户上下文,此时要求Nop平台的IContext已经初始化完毕。

@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SpringSecurityConfig {
    @Inject
    SpringHttpServerFilterConfiguration config;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .requestMatchers("/r/**").authenticated()
                .requestMatchers("/p/**").authenticated()
                .requestMatchers("/f/**").authenticated()
                .requestMatchers("/graphql").authenticated()
                .requestMatchers("/error").permitAll();

        http
                .csrf(Customizer.withDefaults())
                .addFilterAfter(authFilter(), CsrfFilter.class)
                // 将Nop Context的初始化放到authFilter之前
                .addFilterBefore(config.registerSysFilter().getFilter(), WebAuthFilter.class);
        return http.build();
    }

    // 其他配置
}

五. 使用@Auth注解

在业务代码中我们可以使用@Auth注解来声明权限约束。

@BizModel("Demo")
public class DemoBizModel {
  @BizQuery
  @Auth(permissions = "Demo:hello")
  public String hello(@Name("message") String message) {
    return "hello:" + message;
  }
}

在Spring的Controller中可以使用@PreAuthorize注解

@RestController
public class DemoController {

  @GetMapping("/hello")
  @PreAuthorize("hasPermission('Biz','Demo:hello')")
  public String hello(@RequestParam("message") String message) {
    return "Hi," + message;
  }
}