Grails 的插件开发

警告:本文还未整理,可读性差,还只是草稿

文档

Grails Plugin Develop Document

grails-spring-security-core 插件文档
grails-spring-security-rest插件 文档

创建插件

执行命令行

grails create-plugin <<PLUGIN NAME>>

即可。
如果不需要 web 环境,可以加参数:

grails create-plugin <<PLUGIN NAME>> --profile=plugin

插件的名字不能有多个连续的大写字母,否则不能正常工作,遵守 Camel case 约定就好。

如果遇到找不到 grails.plugin.testing 依赖的错误,直接删除这个包就可以,因为这个依赖以及被废弃不用了。

plugin 项目也是一个常规的 Grails 项目,这样你可以运行项目,立刻测试你的插件。
像这样:

grails run-app

当然一般我们都是在 Idea 里面直接运行项目的。

因为内容太多,下面我先记录一些阅读时的笔记,有时间再回来详细些成博客。

插件描述类:src/main/groovy/XXXGrailsPlugin

我们在给插件起名时,不要加 Plugin 字样了,因为会自动添加的。否则就会成为 TenantSecurityPluginGrailsPlugin 这种名称,有两个 Plugin 单词。

这个类主要是提供一些元信息,定义一些插件生命周期回调函数。

安装本地插件

执行

grails install 

就可以将插件安装到你的本地 maven 缓存中。然后在 application 中声明对插件的依赖,并且在 gradle 的 repository 配置块中,添加一个 mavenLocal(),如下:

...
repositories {
    ...
    mavenLocal()
}
...
compile "org.grails.plugins:quartz:0.1"

多项目的gradle模式开发

将插件项目和应用程序项目作为 multi project build,可以按下面的步骤配置。

第一步:创建应用和插件

$ grails create-app myapp
$ grails create-plugin myplugin

注意:如果你的 web application 项目下已经有一个 settings.gradle 文件,那么你需要删除它,否则会导致"项目依赖失败"问题。

第二步:创建一个 settings.gradle 文件

在包含两个项目的目录下,创建 settings.gradle 文件,文件包含下面的内容。

include "myapp", "myplugin"

文件的目录结构像这样:

PROJECT_DIR
  - settings.gradle
  - myapp
    - build.gradle
  - myplugin
    - build.gradle

第三步:声明对插件的项目依赖

在应用程序的 build.gradle 文件中,声明对插件的依赖,像这样:

grails {
    plugins {
        compile project(':myplugin')
    }
}

不能在 dependencies 中声明对插件的依赖,那样会导致子项目无法自动热重载。

第四步:配置插件,打开 reloading (重载)

在插件的目录下,新增一个 gradle.properties 文件,添加属性 exploded=true,以便将插件的*展开目录(exploded directories)*放入类路径。

第五步:运行应用

$ cd myapp
$ grails run-app -verbose

可以发现插件项目会被先编译,然后放到应用的类路径中了。

注意,如果用 gradle 执行构建工作的话,要在最顶层的根项目上执行 build 任务,不要在 application project 中执行 build,否则会报告找不到 插件项目的错误。

18.5 Hooking into Runtime Configuration

Adding New Servlet Filters

use Spring Boot’s FilterRegistrationBean:

myFilter(FilterRegistrationBean) {
    filter = bean(MyFilter)
    urlPatterns = ['/*']
    order = Ordered.HIGHEST_PRECEDENCE
}

18.9 Understanding Plugin Load Order

dependsOn
loadAfter
loadBefore

打包 plugin 时排除的文件

package-plugin 命令行,会自动排除下面的文件:

grails-app/conf/
/src/test/

如果要把代码中的某些包排除,需要在插件类中定义:

// resources that are excluded from plugin packaging
def pluginExcludes = [
        "grails-app/views/error.gsp",
        '**/com/demo/**'
]

同时也要告诉 gradle 从 jar 中去掉。

jar {
  exclude "com/demo/**/**"
}

执行 application build 时报告找不到 GORM 实现异常

在根项目中执行 gradle build 报告异常:

java.lang.IllegalStateException: No GORM implementations configured. Ensure GORM has been initialized correctly
	at org.grails.datastore.gorm.GormEnhancer.findSingleDatastore(GormEnhancer.groovy:378)
	at org.grails.datastore.gorm.GormEnhancer.findSingleTransactionManager(GormEnhancer.groovy:397)
	at com.telecwin.jinanyuan.ReportServiceSpec.测试 dashboardInfo(ReportServiceSpec.groovy:48)

但如果直接在 web application 项目中运行 test 是可以成功的。

原来是我搞错了 unit test、integration test 的概念。需要把访问数据库的测试作为 integration test 来创建,不能把带有 @integration 注解的测试类放在单元测试目录下,即 src/test/groovy 目录下,而应该放在 src/integration-test/groovy 目录下。

插件项目找不到的问题

删除掉主web application项目中的settings.gradle文件即可。一个多项目gradle目录结构中,只能有一个settings.gradle文件,且在顶层目录中。

如何修改多项目gradle环境下的子项目名?

方法说明看这里,摘要如下:

  1. 只能在顶层的 settings.gradle 设定子项目名称。

  2. 像这样

    // 设定根项目名
    rootProject.name = “bar”
    // 设定子项目名
    include “new-name”
    project(":new-name").projectDir = file(“directory-name”)

如何创建和管理 git submodule?

参考
git 命令 submodule 文档

文章都是对于从头生成的情况,而如果已经在本地工作目录下有 submodule 内容了,还能用 submodule add 命令吗?

但是可以安全地添加一个submodule,执行下面的命令:

git submodule add [email protected]:/home/gits/repo/lawyer/lawyer_site_service.git

本地工作文件不会被删除。
但是子模块的文件还是被认为是一个新的文件。

运行 git status,显示下面内容

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   .gitignore
        new file:   .gitmodules
        new file:   lawyer_site_service

这里的 lawyer_site_service 就是新加的子模块。所以说,是正确的。
查看一下新增的子模块对应的 file entry:

git diff --cached lawyer_site_service

diff --git a/lawyer_site_service b/lawyer_site_service
new file mode 160000
index 0000000..5d109da
--- /dev/null
+++ b/lawyer_site_service
@@ -0,0 +1 @@
+Subproject commit 5d109dadcdfada7085d1c74c27b8d7e9c36b118d

或者显示更友好的方式:

git diff --cached --submodule lawyer_site_service

Submodule lawyer_site_service 0000000...5d109da (new submodule)

可以看到 submodule 已经被git正确识别了。

对于测试性质的子项目,我们可以先在本地创建一个 repository,然后将临时子项目push到这个本地repo,再添加到submodule中。准备工作如下:

mkdir local_repo
cd local_repo
git init --bare
cd <临时子项目>
git remote add local < local repo path >
git push --set-upstream local master

然后可以添加子模块了:

git submodule add < local repo path >

注意:在windows下,仓库的路径名中反斜线要替换为正斜线

git submodule add d:\projects\产品\local_repo\multi-tenant-security-plugin

要替换成

git submodule add d:/projects/产品/local_repo/multi-tenant-security-plugin

才能正常工作,git 否则会报告要添加的目录被 ignore 了。

最后,我们提交 root 项目的git,得到下面的结果:

git commit -m "添加四个子模块"
[master (root-commit) 92ba70b] 添加四个子模块
 6 files changed, 17 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 .gitmodules
 create mode 160000 gcl_client
 create mode 160000 lawyer_site_h5
 create mode 160000 lawyer_site_service
 create mode 160000 multi-tenant-security-plugin

其中 mode 160000 表示这个目录是一个特殊的项,只是 directory entry,而不是普通的目录。

到这里我们的 git 子项目就全部设置好了。

如果clone根项目,需要对submodule目录执行额外的命令:

git submodule init
git submodule update
// 合二为一的方法:
git submodule update --init

或者

git clone --recurse-submodules https://github.com/chaconinc/MainProject

或者

git submodule update --init --recursive

用submodules进行开发工作

如果只是使用子项目,但不在根项目中修改子项目(submodule)那么你需要进入每一 submodule 的目录,执行 git fetch、git merge origin/master,再回到根目录下,用 git diff --submodule 查看根项目的情况。
此时提交根项目,那么其他人更新根项目时,就能取到子模块最新代码了。

也可以用 git submodule update --remote 命令一次性fetch、update所有的子模块。
它默认用子模块的 master 分支进行更新,可以用 submodule 命令设置,或者修改你自己本地的 config 。

// 修改本地 config 
git config -f .gitmodules submodule.DbConnector.branch stable
// 修改 submodule 配置
编辑 .submodules 文件,添加一行即可。
submodule.< name >.branch=想跟踪的分支名

如果需要在根项目中修改子项目的内容,那么需要如下操作。
TODO

开发一个支持多租户的安全插件 multi-tenant-security plugin

插件功能:

  • 与 spring-security-rest / core 插件协同工作,底层都使用 spring security 框架
  • 自动从请求header字段中读取租户的 uuid 并设置到线程本地变量中。
  • 让 spring-security-rest plugin 的 UserService 在查询用户信息时,基于租户查询;生成的 JWT token 中 claims 增加 tenantId 属性。
  • 让 GORM 的多租户功能自动使用正确的租户信息。

参考 extend-spring-security-to-protect-multi-tenant-saas-applications

第一步,让 web application 引入 spring-security-core 插件并配置好相关的Entity和设置

这一步,请参考 “grails_tutorials项目的spring_security_core_tutorials分支”

创建 Person、Authority 和 PersonAuthority 类

我们这里 Person 对应的是 TenantUser,Authority 对应的是 Role,去掉 Privilege 类。

在 BootStrap 类中初始化一些 用户(TenantUser) 和 角色(Role) 记录。

配置 security rest 特有的属性

grails:
  plugins:
    springsecurity:
      logout:
        postOnly: false
  plugin:
    springsecurity:
      rest:
        token:
          storage:
            jwt:
           	  # 至少 32 字节
              secret: "rest_api_key_2020rest_api_key_2020rest_api_key_2020"

执行 grails run-app 遇到 command line 太长的问题

在 build.gradle 中让 grails 插件使用jar封装路径即可,如下:

grails {
    plugins {
        // 对多租户安全插件的依赖
        compile project(":multi-tenant-security-plugin")
    }
    // 解决命令行过程问题
    pathingJar = true
}

因为管理后台与REST服务都在一个web application中,需要区分一下登录URL地址

为什么访问的是 LoginController.auth() 而不是 LoginController.index() ?
是被 302 重定向到 http://localhost:9090/login/auth 去了,可能是 rest 插件的作用。

通过配置 spring security core 的 loginFormUrl 来指定正确的登录页面显示URL。

---
# spring security core config
grails:
  plugin:
    springsecurity:
      logout:
        postOnly: false
      auth:
        # 把 普通 web page url 和 rest 接口的登录url分开
        loginFormUrl: "/login/index"
      rest:
        token:
          storage:
            jwt:
              # 至少 32 字节
              secret: "bg9cgcdO5PshWaTjOPClkk%UKUrXnAwMG@nEN!oTWJU8BD9W"

先来打开 Debug Filter 开关,记录 security filter 相关日志。

environments:
    development:
        grails:
            logging:
                jul:
                    usebridge: true
            plugin:
                springsecurity:
                    debug:
                        useFilter: true
    production:
        grails:
            logging:
                jul:
                    usebridge: true

grails-app/conf/logback.groovy

logger 'grails.plugin.springsecurity.web.filter.DebugFilter', INFO, ['STDOUT'], false

重启web应用程序后,可以看到下面的 filter chain 信息:

Security filter chain: [
  SecurityRequestHolderFilter
  SecurityContextPersistenceFilter
  MutableLogoutFilter
  GrailsUsernamePasswordAuthenticationFilter
  RestAuthenticationFilter
  SecurityContextHolderAwareRequestFilter
  GrailsRememberMeAuthenticationFilter
  GrailsAnonymousAuthenticationFilter
  GrailsHttpPutFormContentFilter
  UpdateRequestContextHolderExceptionTranslationFilter
  FilterSecurityInterceptor
]

这些都是 servlet filter 而不是 grails 的 interceptor。

第二步,定制化 spring security core plugin

有几个东西可以定制化:

  • Authentication Provider,插件提供的是 DaoAuthenticationProvider
  • UserDetailsService,插件提供的是 GormUserDetailsService 类
  • UserDetails,插件提供的是 GrailsUser

首先需要决定在哪个层面定制化,是提供全新的 auth provider,还是定制化 UserDetailsService 和 UserDetails。

因为我们的数据结构和 spring security core 的很近似,只是多了一个 tenantId 而已,所以我们只需要定制化 UserDetailsService 和 UserDetails 就可以了。

定制化 UserDetails 和 UserDetailsService 类

1、创建定制的 UserDetails、UserDetailsService 类
要在 src/groovy 目录下创建自定义的 UserDetailsService 类而不要在 grails-app/services 目录下。多参考 GormUserDetailsService 类的代码。

把 TenantUserDetailsService 注册为 spring bean,在 resources.groovy 中添加下面内容:

import com.telecwin.jinanyuan.security.TenantUserDetailsService

// Place your Spring DSL code here
beans = {
    userDetailsService(TenantUserDetailsService)
}

2、配置 User 和 Authority 属性

修改文件 application.yml :

3、需要重新定义 User 类和 UserAuthority 类
User 类中不要有 Role 关联关系,因为效率低。而是使用额外的查询函数来实现,如下:

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

static mapping 中 version false 被认为是语法错误

用另外一种写法即可:

import static grails.gorm.hibernate.mapping.MappingBuilder.orm

static final mapping = orm {
    version false
    id composite: ['user', 'role']
}

调试修改创建日期总是失败的问题

问题代码如下:

(0..10).each { index ->
     def c = new Client(phone: '18612117197', tenantId: 'jingyun', lawyerId: 'wxh', userId: '123' + index, status: ClientStatus.VISITING)
     // 先保存一下,让 dateCreated 生成好,以便后面我们修改
     Client.withNewTransaction {
         c.save(flush: true)
     }
     Client.withNewTransaction {
         // 修改一下创建日期
         c = Client.get(c.id)
         c.dateCreated = new Date() - index
         c.save(flush: true)
     }
}

日期总是没有变化, new Date() - index 不起作用了,之前在单元测试中还能正常工作,移动到 integration-test 下面就不能正常工作了。

原来是需要用 AutoTimestampEventListener.withoutTimestamps() 函数来操作,代码如下:

void executeWithoutTimestamps(Class domainClass, Closure closure) {
    ApplicationContext applicationContext = Holders.findApplicationContext()
    HibernateDatastore mainBean = applicationContext.getBean(HibernateDatastore)
    AutoTimestampEventListener listener = mainBean.getAutoTimestampEventListener()

    listener.withoutTimestamps(domainClass, closure)
}

def setup() {
    (0..10).each { index ->

        Client.withTransaction {
            executeWithoutTimestamps(Client, {
                Date someValidDate = new Date() - index
                Client client = new Client(phone: '18612117197', tenantId: 'jingyun', lawyerId: 'wxh', userId: '123' + index, status: ClientStatus.VISITING)
                client.dateCreated = someValidDate
                client.lastUpdated = new Date()
                client.save(flush: true)
            })
        }
   }
}

让 LoginController 使用 spring security 服务进行登录验证

参考 grails.plugin.springsecurity.LoginController 的实现。
注入一个 SpringSecurityService,就可以进行登录认证了。

spring security 的 filter UsernamePasswordAuthenticationFilter,grails 继承它实现了一个 filter GrailsUsernamePasswordAuthenticationFilter

过滤器链条:
Security filter chain: [
SecurityRequestHolderFilter
SecurityContextPersistenceFilter
MutableLogoutFilter
GrailsUsernamePasswordAuthenticationFilter
RestAuthenticationFilter
SecurityContextHolderAwareRequestFilter
GrailsRememberMeAuthenticationFilter
GrailsAnonymousAuthenticationFilter
GrailsHttpPutFormContentFilter
UpdateRequestContextHolderExceptionTranslationFilter
FilterSecurityInterceptor
]

我们可以提供 credentialsExtractor(DefaultJsonPayloadCredentialsExtractor, paramsClosure) 来创建一个包含 tenantId 和 用户名 的 principal 对象。

然后再提供一个 authentication provider 来处理这个 authentication。

是在 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication 方法中设置认证成功信息的,SecurityContextHolder.getContext().setAuthentication(authResult)

只要 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 返回非空对象,就意味认证成功。

UsernamePasswordAuthenticationToken 有一个 details 概念,authenticationDetailsSource.buildDetails(request),由 AuthenticationDetailsSource 设置的。

在 security core plugin 中有设置这个 bean:
authenticationDetailsSource(classFor(‘authenticationDetailsSource’, WebAuthenticationDetailsSource))

Filter 的位置由 SecurityFilterPosition 定义。
spring security core 会构建一个 applicationContext.securityFilterChains。

通过 springSecurityFilterChainRegistrationBean 将 springSecurityFilterChain 这个 filter 注册到 servlet 容器。

在这里 SpringSecurityUtils.buildFilterChains 创建的 chains。
对于一个 url pattern 就会有一个 chain,chain 中有多个 filter,所以叫 chains。

FilterChainProxy 是真正工作的最外的 servlet filter,它引用 bean “securityFilterChains” 来完成实际过滤的工作,它的 bean name 叫 “springSecurityFilterChainProxy”,别名叫 “springSecurityFilterChain”。

grails servlet filter 的真实情况是:

request.servletContext.filterRegistrations = {HashMap@17428}  size = 11
 "grailsCorsFilter" -> {ApplicationFilterRegistration@17443} 
 "webMvcMetricsFilter" -> {ApplicationFilterRegistration@17445} 
 "grailsWebRequestFilter" -> {ApplicationFilterRegistration@17447} 
 "securityDebugFilter" -> {ApplicationFilterRegistration@17449} 
 "Tomcat WebSocket (JSR356) Filter" -> {ApplicationFilterRegistration@17451} 
 "filterChainProxy" -> {ApplicationFilterRegistration@17453} 
 "hiddenHttpMethodFilter" -> {ApplicationFilterRegistration@17455} 
 "restLogoutFilter" -> {ApplicationFilterRegistration@17457} 
 "assetPipelineFilter" -> {ApplicationFilterRegistration@17459} 
 "characterEncodingFilter" -> {ApplicationFilterRegistration@17461} 
 "httpTraceFilter" -> {ApplicationFilterRegistration@17463} 

可以看到,grailsWebRequestFilter 比 securityDebugFilter 还靠前。

角色名一定要有 ROLE_ 前缀,否则不工作。

验证用户名、密码的工作是 UsernamePasswordAuthenticationFilter 做的,他是 spring security jar 中的类。UPA 会从servlet request对象中取 “username” 和 “password” 参数,然后使用 AuthenticationManager 进行 authenticate。
认证成功后,会得到 UsernamePasswordAuthenticationToken 对象,它实现 Authentication 接口,有 details 属性。

spring 的 DaoAuthenticationProvider 会被用来进行 auth。
filter 类是 GrailsUsernamePasswordAuthenticationFilter,父类是 AbstractAuthenticationProcessingFilter,放入 FilterChainProxy 的。

filterProcessesUrl="/login/authenticate"
配置项 apf.filterProcessesUrl = “/login/authenticate” 表示 spring security filter 将对用户名、密码验证的请求地址,是这个地址 filter 才会检查用户名、密码是否正确,然后把成功的结果放入 “安全上下文”。
所以,登录请求的 url 必须是 /login/authenticate,因此 controller 的方法名也最好叫 authenticate。

这时程序会执行 DaoAuthenticationProvider.authenticate()。

我们要添加一个额外的 filter 到 filterChain 中去,设置 tenantId 到线程本地变量。

如果是插件,可以这样添加 filter 到 UPAF 前面。

SpringSecurityUtils.registerFilter 'restAuthenticationFilter', SecurityFilterPosition.FORM_LOGIN_FILTER.order - 3

如果是 application,我们可以用 clientRegisterFilter、filterNames 等方式添加 filter。

clientRegisterFilter 方式:

import com.mycompany.myapp.MyFilter
import org.springframework.boot.context.embedded.FilterRegistrationBean

beans = {
   myFilter(MyFilter) {
      // properties
   }

   myFilterDeregistrationBean(FilterRegistrationBean) {
      filter = ref('myFilter')
      enabled = false
   }
}

grails-app/init/BootStrap.groovy

import grails.plugin.springsecurity.SecurityFilterPosition
import grails.plugin.springsecurity.SpringSecurityUtils

class BootStrap {

   def init = {
      SpringSecurityUtils.clientRegisterFilter(
          'myFilter', SecurityFilterPosition.OPENID_FILTER.order - 3)
   }
}

spring security 要求数据库存储的是加密后的 password,需要用 PasswordEncoder 来对明文密码加密后与数据库中的加密密码对比。

grails 提供了 UserPasswordEncoderListener 来在 User 对象保存到数据库时进行加密,如下:

@CompileStatic
class UserPasswordEncoderListener extends AbstractPersistenceEventListener {

    SpringSecurityService springSecurityService

    protected UserPasswordEncoderListener(Datastore dataStore) {
        super(dataStore)
    }

    private void encodePasswordForEvent(AbstractPersistenceEvent event) {
        if (event.entityObject instanceof TenantUser) {
            TenantUser u = event.entityObject as TenantUser
            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
    }

    @Override
    protected void onPersistenceEvent(AbstractPersistenceEvent event) {
        encodePasswordForEvent(event)
    }

    @Override
    boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return eventType instanceof PreInsertEvent || eventType instanceof PreUpdateEvent
    }
}

注册到 spring

import com.mycompany.myapp.UserPasswordEncoderListener
// Place your Spring DSL code here
beans = {
    userPasswordEncoderListener(UserPasswordEncoderListener)
}

重新生成一下用户数据,就能正确了。

@GrailsCompileStatic 相比 @CompileStatic 能够识别 Grails 通过 AST Transformation 添加的动态方法,比如 Book.findByName(name) 这种。

还需要指定 密码加密 算法:

grails.plugin.springsecurity.password.algorithm = 'bcrypt'
grails.plugin.springsecurity.password.bcrypt.logrounds = 15

Note that the number of rounds must be between 4 and 31.

做用户认证时,要先用 “用户名”、“租户id” 从数据库中查出用户对象,然后用 PasswordEncoder.matches(明文密码, 暗文密码) 是否一致,因为 BCrypt 算法会在密文中加 salt,相同明文每次运算完会得到不同的密文,只有了解如何读取 salt 才能正确比较。

授权是由 Voters 决定的。

需要指定 请求权限映射 的方法:

  • @Secured 方式,grails.plugin.springsecurity.annotation.Secured
  • Static Map 方式,在 application.groovy 中
  • Requestmap domain class 方式,存在数据库中

由 grails.plugin.springsecurity.securityConfigType = “Annotation” 配置属性决定。

org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation

Collection attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);

AnnotationFilterInvocationDefinition 从注解得到权限映射关系的类。

用 controllerAnnotations.staticRules 属性定义无法在注解上添加的规则:

grails.plugin.springsecurity.controllerAnnotations.staticRules = [
   ...
   [pattern: '/js/admin/**',   access: ['ROLE_ADMIN']],
   [pattern: '/someplugin/**', access: ['ROLE_ADMIN']]
]

注意 URL 都必须小写
如果Controller有UrlMapping定义,那么也必须用未url-mapping的形式
“/” 要写成 “/index”,因为会变为 method name。

判断 URL 是否有配置 权限 是 grails.plugin.springsecurity.web.access.intercept.AbstractFilterInvocationDefinition#getAttributes() 这个函数决定的。

退出是由 MutableLogoutFilter filter 实现的,他配置的地址是 /logoff
因为登出时,需要多一系列对象进行操作,比如清session、清 SecurityContext、SecurityContextHolder 等等,所以需要让很多 LogoutHandler 轮流工作。

最好还是通过重定向到 /logoff URL 来让 logoutFilter 处理登出。

successHandler.defaultTargetUrl 没有触发登录的已保存请求时,跳转到的URL地址,默认是 “/” 。

关于 Spring-Security-Rest Plugin 的使用和改造

改造目标

  • 能自动读取header中的 tenant siteId 进行 authentication(身份认证),header name 为 “tenant.siteId”
  • 把tenant siteId 写入 JWT 的 claims 中,key 为 tenant.siteId
  • 其他的都使用 spring-security-core plugin 的实现

决定对哪些 URL 使用 REST 安全认证

通过配置 chainMap 指定对哪些 URL 应用 REST authentication filter,在application.groovy 的 chainMap 属性中指定。

grails.plugin.springsecurity.filterChain.chainMap = [
        //Stateless chain
        [
                pattern: '/**',
                filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'
        ],

        //Traditional, stateful chain
        [
                pattern: '/stateful/**',
                filters: 'JOINED_FILTERS,-restTokenValidationFilter,-restExceptionTranslationFilter'
        ]
]

登录的 REST URL:/api/login

当过滤器看到 URL 是配置项中的值时,例如 grails.plugin.springsecurity.rest.login.endpointUrl='/api/login',就会进行登录认证。
所以我们的客户端需要访问 /api/login URL来进行登录。

如果 authentication 成功,会用 token generator 生成一个 token, 用 token storage implementation 存储这个 token。
如果认证失败,会返回 401 status code。

抽取用户名和密码

Spring-Security-Rest plugin 简称 SSR 插件,有两种方式抽取用户名和密码,一种是从 Servlet Request 对象中取,一种是从 JSON 对象中取。
配置项
可以自定义 Credential 抽取器,生成定制的 UsernamePasswordAuthenticationToken 对象,只需要继承 AbstractJsonPayloadCredentialsExtractor 类就可以。

但我们这里还不需要,因为 TenantAuthFilter 已经在 UsernamePasswordAuthenticationFilter 前面将 siteId 设置到 TheadLocale 中了。
我们只需要定制一个 JWT CustomClaimProvider,注册到 spring 中。

如何编写 Filter 的测试代码?

可以通过 grails integration test 来测试 filter 。如果是一个 plugin ,那么可以创建一个独立的测试 application,在 test-app 的 integration-test 中添加测试代码。
因为 filter 也是一个普通的 spring bean ,所以我们可以像测试一个普通的 spring bean/service 那样测试 filter。

如果不需要做 grails integration test,即不访问数据库,那我们可以将这个测试类设置为 grails web test unit,这样就可以获得一些 web 相关的属性,如 request,response,servletContext 等对象了。

例如我们可以对 spring bean “tokenGenerator” 进行集成测试,检查 JWT 是否正确生成了。

REST plugin 是在哪里生成的 JWT Token?

查看 RestAuthenticationFilter 类,它有一个 TokenGenerator tokenGenerator 属性,tokenGenerate.generateAccessToken() 会生成 access token 和 refresh token。默认JWT加密算法是 HS256,所以用的是类 SignedJwtTokenGenerator,是它生成了JWT token。

JWT Token 的失效时间

默认REST插件配置的失效时间是 1 小时。
配置项是 grails.plugin.springsecurity.rest.token.storage.jwt.expiration=3600 单位是秒。

刷新 Token

Refresh tokens never expire, and can be used to obtain a new access token by sending a POST request to the /oauth/access_token endpoint.

REST 的登出接口

对于 JWT storage type,是不需登出操作的,因为 server 不会记录 token 的状态,token 超时后自然就失效了,客户端将 token 删除掉就是登出了。

对于其他类型的 token storage,可以通过 TokenStorageService 的 removeToken() 方法实现登出。

发布了63 篇原创文章 · 获赞 25 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yangbo_hr/article/details/105203702