显示导航

当前登录用户的自定义租户解析器

了解如何创建自定义租户解析器,并使用 Grails 多租户功能根据当前登录用户或 JWT 切换租户。

作者:Sergio del Amo

Grails 版本 3.3.8

1 Grails 培训

Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和提供!。

2 开始使用

GORM 多租户支持包括 多个内置租户解析器。在本指南中,您将创建自己的租户解析器,以根据使用 Spring Security REST 插件 的 Grails rest-api 应用程序中的登录用户切换租户。

2.1 您需要了解的内容

为了完成本指南,您需要具备以下条件:

  • 有些时间

  • 不错的文本编辑器或 IDE

  • 已安装 JDK 1.7 或更高版本,并正确配置了 JAVA_HOME

2.2 如何完成指南

若要开始,请执行以下操作

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用程序,它提供了一些其他代码以为您领先一步。

  • complete完成的示例。它是逐步完成指南中提出的步骤并对initial文件夹应用这些更改的结果。

要完成指南,转到initial文件夹

  • cd进入grails-guides/grails-custom-security-tenant-resolver/initial

并按照下一节的说明进行操作。

如果cd进入grails-guides/grails-custom-security-tenant-resolver/complete,你可以直接转到完成的示例

3 编写应用程序

本指南使用多租户鉴别符模式。要了解更多信息,请阅读单一数据库多租户鉴别符列指南。

3.1 创建域类

grails-app/domain/demo/Plan.groovy
package demo

import grails.gorm.MultiTenant

class Plan implements MultiTenant<Plan> { (1)
    String title
    String username

    static mapping = {
        tenantId name: 'username' (2)
    }
}
1 实现MultiTenant特性,将此域类视为多租户。
2 将租户标识符设置为名为username的列

创建一个接口PlanService,它是GORM Data Service

grails-app/services/demo/PlanDataService.groovy
package demo

import grails.gorm.multitenancy.CurrentTenant
import grails.gorm.services.Service
import groovy.transform.CompileStatic

@CompileStatic
@Service(Plan) (1)
@CurrentTenant (2)
interface PlanDataService {
    List<Plan> findAll()
    Plan save(String title)
    void deleteByTitle(String title)
}
1 使用grails.gorm.services.Service注释对其进行注释,其中包含服务适用的域类。
2 解析此类的上下文的当前租户

3.2 控制器

创建一个简单控制器,它使用先前定义的PlanService服务。

grails-app/controllers/demo/PlanController.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class PlanController {
    PlanDataService planDataService

    def index() {
        [planList: planDataService.findAll()]
    }
}

我们使用JSON 视图Plan实例呈现为 JSON

grails-app/views/plan/index.gson
import demo.Plan

model {
    Iterable<Plan> planList
}

json tmpl.plan('plan', planList)
grails-app/views/plan/_plan.gson
import demo.Plan

model {
    Plan plan
}

json {
    title plan.title
}

3.3 连接安全性

创建安全域类。可以使用 Spring Security Core s2-quickstart创建这些类。

grails-app/domain/demo/User.groovy
package demo

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {

    private static final long serialVersionUID = 1

    String username
    String password
    boolean enabled = true
    boolean accountExpired
    boolean accountLocked
    boolean passwordExpired

    Set<Role> getAuthorities() {
        (UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<Role>
    }

    static constraints = {
        password nullable: false, blank: false, password: true
        username nullable: false, blank: false, unique: true
    }

    static mapping = {
            password column: '`password`'
    }
}
grails-app/domain/demo/Role.groovy
package demo

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {

        private static final long serialVersionUID = 1

        String authority

        static constraints = {
                authority nullable: false, blank: false, unique: true
        }

        static mapping = {
                cache true
        }
}

创建一个处理用户密码编码的类UserPasswordEncoderListener

它使用@Listener注释来同步监听GORM 的事件

src/main/groovy/demo/UserPasswordEncoderListener.groovy
package demo

import grails.plugin.springsecurity.SpringSecurityService
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.PreInsertEvent
import org.grails.datastore.mapping.engine.event.PreUpdateEvent
import org.springframework.beans.factory.annotation.Autowired
import grails.events.annotation.gorm.Listener
import groovy.transform.CompileStatic

@CompileStatic
class UserPasswordEncoderListener {

    @Autowired
    SpringSecurityService springSecurityService

    @Listener(User)
    void onPreInsertEvent(PreInsertEvent event) {
        encodePasswordForEvent(event)
    }

    @Listener(User)
    void onPreUpdateEvent(PreUpdateEvent event) {
        encodePasswordForEvent(event)
    }

    private void encodePasswordForEvent(AbstractPersistenceEvent event) {
        if (event.entityObject instanceof User) {
            User u = event.entityObject as User
            if (u.password && ((event instanceof PreInsertEvent) || (event instanceof PreUpdateEvent && u.isDirty('password')))) {
                event.getEntityAccess().setProperty('password', encodePassword(u.password))
            }
        }
    }

    private String encodePassword(String password) {
        springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
    }
}

将以前的监听器定义为一个 bean

grails-app/conf/spring/resources.groovy
import demo.UserPasswordEncoderListener
beans = {
    userPasswordEncoderListener(UserPasswordEncoderListener)
}

application.yml 中配置 Spring Security Core

grails-app/conf/application.yml
grails
grails:
    plugin:
        springsecurity:
            securityConfigType: InterceptUrlMap
            userLookup:
                userDomainClassName: demo.User
                authorityJoinClassName: demo.UserRole
            authority:
                className: demo.Role
            filterChain:
                chainMap:
                    - # Stateless chain
                        pattern: /**
                        filters: 'JOINED_FILTERS,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'
            interceptUrlMap:
                -
                    pattern: /
                    access:
                        - ROLE_VILLAIN
                -
                    pattern: /error
                    access:
                        - permitAll
                -
                    pattern: /api/login
                    access:
                        - ROLE_ANONYMOUS
                -
                    pattern: /api/validate
                    access:
                        - ROLE_ANONYMOUS
                -
                    pattern: /oauth/access_token
                    access:
                        - ROLE_ANONYMOUS
                -
                    pattern: /plan
                    access:
                        - ROLE_VILLAIN

创建多个GORM Data Services来处理与我们应用程序安全相关联的域类。

grails-app/services/demo/RoleDataService.groovy
package demo

import grails.gorm.services.Service
import groovy.transform.CompileStatic

@CompileStatic
@Service(Role)
interface RoleDataService {
    void delete(Serializable id)
    void deleteByAuthority(String authority)
    Role findByAuthority(String authority)
    Role saveByAuthority(String authority)
}
grails-app/services/demo/UserDataService.groovy
package demo

import grails.gorm.services.Service
import groovy.transform.CompileStatic

@CompileStatic
@Service(User)
interface UserDataService {
    User save(String username, String password)
    void delete(Serializable id)
    User findByUsername(String username)
}
grails-app/services/demo/UserRoleDataService.groovy
package demo

import grails.gorm.services.Query
import grails.gorm.services.Service
import groovy.transform.CompileStatic

@Service(UserRole)
@CompileStatic
interface UserRoleDataService {

    @Query("""select $user.username from ${UserRole userRole}
    inner join ${User user = userRole.user}
    inner join ${Role role = userRole.role}
    where $role.authority = $authority""")
    List<String> findAllUsernameByAuthority(String authority)

    UserRole save(User user, Role role)

    void delete(User user, Role role)

    void deleteByUser(User user)
}

创建一个服务来处理具有特定角色的用户创建。

grails-app/services/demo/UserService.groovy
package demo

import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
class UserService {
    RoleDataService roleDataService

    UserRoleDataService userRoleDataService

    UserDataService userDataService

    @Transactional
    User save(String username, String password, String authority) {
        Role role = roleDataService.findByAuthority(authority)
        if ( !role ) {
            role = roleDataService.saveByAuthority(authority)
        }
        User user = userDataService.save(username, password)
        userRoleDataService.save(user, role)
        user
    }
}

3.4 自定义租户解析器

下表包含随 GORM 一起提供的且开箱即用的所有TenantResolver实现。为简洁起见,包名已从org.grails.datastore.mapping缩短为o.g.d.m

名称 说明

o.g.d.m.multitenancy.resolvers.FixedTenantResolver

基于一个固定的租户 ID 进行解析

o.g.d.m.multitenancy.resolvers.SystemPropertyTenantResolver

从一个名为 `gorm.tenantId` 的系统属性中解析租户 ID

o.g.d.m.multitenancy.web.SubDomainTenantResolver

通过 DNS 从子域中解析租户 ID

o.g.d.m.multitenancy.web.CookieTenantResolver

通过一个默认名为 `gorm.tenantId` 的 HTTP Cookie 中解析当前租户

o.g.d.m.multitenancy.web.SessionTenantResolver

通过一个默认使用属性 `gorm.tenantId` 的 HTTP 会话中解析当前租户

但是,你可以灵活地轻松创建自定义租户解析器。要进行此操作,创建一个实现 `org.grails.datastore.mapping.multitenancy.TenantResolver` 的类。

接下来的部分将向你展示如何创建两个自定义租户解析器。

3.5 通过 JWT 的租户解析器

Grails Spring Security REST 插件 允许你通过 JWT 令牌对你的应用程序进行身份验证,该令牌存在于 “授权” 头中。

示例:对 `/plan` 的请求在 “授权” 头中包含一个 JWT 令牌。

## Request Duplicate
curl "http://localhost:8080/plan" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9....." \
     -H "Accept: application/json"

我们可以创建一个可以解出 JWT 令牌、重新生成和解出用户名时期的自定义租户解析器。

src/main/groovy/demo/CurrentUserByJwtTenantResolver.groovy
package demo

import grails.gorm.DetachedCriteria
import grails.plugin.springsecurity.rest.token.storage.TokenStorageService
import groovy.transform.CompileStatic
import org.grails.datastore.mapping.multitenancy.AllTenantsResolver
import org.grails.datastore.mapping.multitenancy.TenantResolver
import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.web.context.request.RequestAttributes
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletWebRequest

import javax.servlet.http.HttpServletRequest

@CompileStatic
class CurrentUserByJwtTenantResolver implements AllTenantsResolver { (1)

    public static final String HEADER_NAME = 'Authorization'
    public static final String HEADER_VALUE_PREFFIX = 'Bearer '

    String headerName = HEADER_NAME
    String headerValuePreffix = HEADER_VALUE_PREFFIX

    @Autowired
    TokenStorageService tokenStorageService

    @Override
    Serializable resolveTenantIdentifier() throws TenantNotFoundException {

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes()
        if(requestAttributes instanceof ServletWebRequest) {

            HttpServletRequest httpServletRequest = ((ServletWebRequest) requestAttributes).getRequest()
            String token = httpServletRequest.getHeader(headerName.toLowerCase())
            if ( !token ) {
                throw new TenantNotFoundException("Tenant could not be resolved from HTTP Header: ${headerName}")
            }

            if (token.startsWith(headerValuePreffix)) {
                token = token.substring(headerValuePreffix.length())
            }
            UserDetails userDetails = tokenStorageService.loadUserByToken(token)
            String username = userDetails?.username
            if ( username ) {
                return username
            }
            throw new TenantNotFoundException("Tenant could not be resolved from HTTP Header: ${headerName}")
        }
        throw new TenantNotFoundException("Tenant could not be resolved outside a web request")
    }

    @Override
    Iterable<Serializable> resolveTenantIds() {
        new DetachedCriteria(User)
                .distinct('username')
                .list()
    }
}
1 在使用基于鉴别器的多租户时,如果想要在任何时候迭代所有可用的租户,则可能需要在你的 `TenantResolver` 实现中实现 AllTenantsResolver 接口。`AllTenantsResolver 扩展 `TenantResolver`。要创建你自己的租户解析器实现 `org.grails.datastore.mapping.multitenancy.TenantResolver`。

将自定义租户解析器定义为一个 bean

grails-app/conf/spring/resources.groovy
import demo.CurrentUserByJwtTenantResolver
beans = {
    currentUserByJwtTenantResolver(CurrentUserByJwtTenantResolver)
}

创建一个集成测试来测试自定义租户解析器。

src/integration-test/groovy/demo/CurrentUserByJwtTenantResolverSpec.groovy
package demo

import grails.testing.mixin.integration.Integration
import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletWebRequest
import spock.lang.Specification
import spock.lang.IgnoreIf

@IgnoreIf( { System.getenv('TRAVIS') as boolean } )
@Integration
class CurrentUserByJwtTenantResolverSpec extends Specification {

    @Autowired
    CurrentUserByJwtTenantResolver currentUserTenantResolver

    void "Test HttpHeader resolver throws an exception outside a web request"() {
        when:
        currentUserTenantResolver.resolveTenantIdentifier()

        then:
        def e = thrown(TenantNotFoundException)
        e.message == "Tenant could not be resolved outside a web request"
    }


    void "Test not tenant id found"() {
        setup:
        def request = new MockHttpServletRequest("GET", "/foo")
        RequestContextHolder.setRequestAttributes(new ServletWebRequest(request))

        when:
        currentUserTenantResolver.resolveTenantIdentifier()

        then:
        def e = thrown(TenantNotFoundException)
        e.message == "Tenant could not be resolved from HTTP Header: ${CurrentUserByJwtTenantResolver.HEADER_NAME}"

        cleanup:
        RequestContextHolder.setRequestAttributes(null)
    }

    void "Test HttpHeader value is the tenant id when a request is present"() {

        setup:
        MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo")
        String jwt = '''\
eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTdjA4VVFSUitleHhDZ2xF\
d2tjUUNHN0V6ZXdtV1Z3RkJBcGtjeHZNc01OSE03VDZXZ2RtWmRXYjJ1R3ZJVlZKUVFGUVNFbHBMXC9oT\
nRcL0FPTUZMVFV0THhaT1BhMElVNjFlZlB0OSt2TjZRV01XZ012RThPRnRHRW04MFNvMEdaR3FNUmlsQn\
ZoZW1GdTBjVG9Dc1J5QVd6UkJLNVBVSUdBUVVYRURoNnhMZDdoTmNsVlVsdHJiMkhrNmwwRGM5b2tONHd\
iaHFlNG84MTJlTXNkYVlOXC9DWlRVd2ZjS2pLM0RGSThpblN2WDBHcXBtd21EOFRwTWxqT21vMjBcL2Vo\
elJEU29udUxURDBERlV2QzB4WmpEQmM3ZXBTVldnZGZEdzJtenVoS3cxMGRVWmpHZmNXbkwzVDVLbTg5Yj\
l2YmVwS01FbjJJVnFOd3ZvVUhmUFBUVDBQT0dpbHBKU0M2M3NiRXVsT2hZYndvc1RmM1wvbXk2K0RrMzZy\
QWtDZHZMajduM0wrWkFINlB6NWNQaTJLRGlJSDAwUFdTMWk5bTVHYnFaTDVyVUd2XC9QdjQ5ZGVqaTczM0\
k2VHNFYVwvK2Z4K3o4emZOOVJaMW1uSERuUjdhRWRIdVZQMDNrU1wvY1RUN1lRaTlzaWpTVFNDOUtPWXh2\
SlVwaWlsczFXZzc2ZG5EXC96UnBiK3ZodWhiSDVsWVlmM090UWRtMUkrRUdSMnk4c1pKcld0WDkrK1BQZ\
zJSOGlXWVhSRHBjNVV1MlRKYWlScDIwMG4wK1BaaWErbmUwWElRWVArZ3JPZUdRVkZBTUFBQT09Iiwic3\
ViIjoidmVjdG9yIiwicm9sZXMiOlsiUk9MRV9WSUxMQUlOIl0sImlhdCI6MTUwNDc4MTEyNX0.cf5rGNrNolchQ3QyMsPB544fwzYGiihBkRF8KU6soxc'''

        request.addHeader('Authorization', "Bearer $jwt")
        RequestContextHolder.setRequestAttributes(new ServletWebRequest(request))


        when:
        def tenantId = currentUserTenantResolver.resolveTenantIdentifier()

        then:
        tenantId == "vector"

        cleanup:
        RequestContextHolder.setRequestAttributes(null)
    }
}

接下来,我们将配置我们的应用程序以使用这个自定义租户解析器。

通过修改 `application.yml` 配置多租户。

grails-app/conf/application.yml
grails
    gorm:
        multiTenancy:
            mode: DISCRIMINATOR (1)
            tenantResolverClass: demo.CurrentUserByJwtTenantResolver (2)
        reactor:
            # Whether to translate GORM events into Reactor events
            # Disabled by default for performance reasons
            events: false
1 将多租户模式定义为 `DISCRIMINATOR`
2 使用一个自定义类(你将在本指南的后面编写)设置租户解析器类。

3.6 按用户进行的租户解析器

或者,你可能想将由 Spring Security REST 保护的无状态端点和由 Spring Security Core 保护的有状态端点结合起来使用。你将会感兴趣于创建按当前已登录用户进行的自定义解析器。

src/main/groovy/demo/CurrentUserTenantResolver.groovy
package demo

import grails.gorm.DetachedCriteria
import grails.plugin.springsecurity.SpringSecurityService
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.grails.datastore.mapping.multitenancy.AllTenantsResolver
import org.grails.datastore.mapping.multitenancy.TenantResolver
import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.security.core.userdetails.UserDetails

@CompileStatic
class CurrentUserTenantResolver implements AllTenantsResolver { (1)

    @Autowired
    @Lazy
    SpringSecurityService springSecurityService

    @Override
    Serializable resolveTenantIdentifier() throws TenantNotFoundException {
        String username = loggedUsername()
        if ( username ) {
            return username
        }
        throw new TenantNotFoundException("Tenant could not be resolved from Spring Security Principal")
    }

    String loggedUsername() {
        if ( springSecurityService.principal instanceof String ) {
            return springSecurityService.principal
        }
        if (springSecurityService.principal instanceof UserDetails) {
            return ((UserDetails) springSecurityService.principal).username
        }
        null
    }

    @Override
    Iterable<Serializable> resolveTenantIds() {
        new DetachedCriteria(User)
                .distinct('username')
                .list()
    }
}
1 在使用基于鉴别器的多租户时,如果想要在任何时候迭代所有可用的租户,则可能需要在你的 `TenantResolver` 实现中实现 AllTenantsResolver 接口。`AllTenantsResolver 扩展 `TenantResolver`。要创建你自己的租户解析器实现 `org.grails.datastore.mapping.multitenancy.TenantResolver`。

将自定义租户解析器定义为一个 bean

grails-app/conf/spring/resources.groovy
import demo.CurrentUserTenantResolver
beans = {
    currentUserTenantResolver(CurrentUserTenantResolver)
}

创建一个集成测试来测试自定义租户解析器。

src/integration-test/groovy/demo/CurrentUserTenantResolverSpec.groovy
package demo

import grails.testing.mixin.integration.Integration
import org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.test.annotation.Rollback
import spock.lang.Specification

@Integration
class CurrentUserTenantResolverSpec extends Specification {

    UserDataService userDataService
    RoleDataService roleDataService
    UserRoleDataService userRoleDataService

    @Autowired
    CurrentUserTenantResolver currentUserTenantResolver

    void "Test Current User throws a TenantNotFoundException if not logged in"() {
        when:
        currentUserTenantResolver.resolveTenantIdentifier()

        then:
        def e = thrown(TenantNotFoundException)
        e.message == "Tenant could not be resolved from Spring Security Principal"
    }

    void "Test current logged in user is resolved "() {
        given:
        Role role = roleDataService.saveByAuthority('ROLE_USER')
        User user = userDataService.save('admin', 'admin')
        userRoleDataService.save(user, role)

        when:
        loginAs('admin', 'ROLE_USER')
        Serializable username = currentUserTenantResolver.resolveTenantIdentifier()

        then:
        username == user.username

        when: "verify AllTenantsResolver::resolveTenantIds"
        Iterable<Serializable> tenantIds
        demo.User.withNewSession {
            tenantIds = currentUserTenantResolver.resolveTenantIds()
        }

        then:
        tenantIds.toList().size() == 1
        tenantIds.toList().get(0) == 'admin'

        cleanup:
        userRoleDataService.delete(user, role)
        roleDataService.delete(role)
        userDataService.delete(user.id)
    }

    void loginAs(String username, String authority) {
        User user = userDataService.findByUsername(username)
        if ( user ) {
            // have to be authenticated as an admin to create ACLs
            List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(authority)
            SecurityContextHolder.context.authentication = new UsernamePasswordAuthenticationToken(user.username,
                    user.password,
                    authorityList)
        }
    }


}

接下来,我们将配置我们的应用程序以使用这个自定义租户解析器。

通过修改 `application.yml` 配置多租户。

grails-app/conf/application.yml
grails
    gorm:
        multiTenancy:
            mode: DISCRIMINATOR (1)
            tenantResolverClass: demo.CurrentUserTenantResolver (2)
        reactor:
            # Whether to translate GORM events into Reactor events
            # Disabled by default for performance reasons
            events: false
1 将多租户模式定义为 `DISCRIMINATOR`
2 使用一个自定义类(你将在本指南的后面编写)设置租户解析器类。

3.7 功能测试

创建一个功能测试,验证我们可以使用切换用户的 API。

src/integration-test/groovy/demo/PlanControllerSpec.groovy
package demo

import grails.gorm.multitenancy.Tenants
import grails.plugins.rest.client.RestBuilder
import grails.testing.mixin.integration.Integration
import spock.lang.Specification
import spock.lang.IgnoreIf

@IgnoreIf( { System.getenv('TRAVIS') as boolean } )
@Integration
class PlanControllerSpec extends Specification {
    PlanDataService planDataService
    UserDataService userDataService
    UserRoleDataService userRoleDataService
    UserService userService
    RoleDataService roleDataService

    RestBuilder rest = new RestBuilder()

    String accessToken(String u, String p) {
        def resp = rest.post("http://localhost:${serverPort}/api/login") {
            accept('application/json')
            contentType('application/json')
            json {
                username = u
                password = p
            }
        }
        if ( resp.status == 200 ) {
            return resp.json.access_token
        }
        null
    }

    def "Plans for current logged user are retrieved"() {
        given:
        User vector = userService.save('vector', 'secret', 'ROLE_VILLAIN')
        User gru = userService.save('gru', 'secret', 'ROLE_VILLAIN')
        Tenants.withId("gru") { (1)
            planDataService.save('Steal the Moon')
        }
        Tenants.withId("vector") {
            planDataService.save('Steal a Pyramid')
        }

        when: 'login with the gru'
        String gruAccessToken = accessToken('gru', 'secret')

        then:
        gruAccessToken

        when:
        def resp = rest.get("http://localhost:${serverPort}/plan") {
            accept('application/json')
            header('Authorization', "Bearer ${gruAccessToken}")
        }

        then:
        resp.status == 200
        resp.json.toString() == '[{"title":"Steal the Moon"}]'

        when: 'login with the vector'
        String vectorAccessToken = accessToken('vector', 'secret')

        then:
        vectorAccessToken

        when:
        resp = rest.get("http://localhost:${serverPort}/plan") {
            accept('application/json')
            header('Authorization', "Bearer ${vectorAccessToken}")
        }

        then:
        resp.status == 200
        resp.json.toString() == '[{"title":"Steal a Pyramid"}]'

        cleanup:
        Tenants.withId("gru") { (1)
            planDataService.deleteByTitle('Steal the Moon')
        }
        Tenants.withId("vector") {
            planDataService.deleteByTitle('Steal the Pyramid')
        }
        userRoleDataService.deleteByUser(gru)
        userDataService.delete(gru.id)
        userRoleDataService.deleteByUser(vector)
        userDataService.delete(vector.id)
        roleDataService.deleteByAuthority('ROLE_VILLAIN')
    }
}
1 你可以使用 `withId` 方法使用特定租户 ID

4 运行测试

要运行测试

./grailsw
grails> test-app
grails> open test-report

./gradlew check
open build/reports/tests/index.html

5 Grails 帮助

Object Computing, Inc. (OCI) 赞助了本指南的创建。提供各种咨询和支持服务。

OCI 是 Grails 的主页

结识团队