欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Grails3 RESTful开发及安全认证

程序员文章站 2022-03-24 13:46:53
...
1、创建项目
grails create-app myapp --profile=rest-api
cd myapp
grails
 --profile可以指定项目框架的类型,rest-api增加rest相关jar,去掉了gsp相关的jar。
 
2、创建domain
create-domain-resource com.rest.book
 
3、import项目到Eclipse
import方法参见上一篇博文。
编辑domain class
package org.demo

import grails.rest.*

@Resource()
class Book {

    String title

}
domain的写法可以参考GORM
 
4、创建controller
create-restful-controller com.rest.book
(generate-all com.rest.book)
 create-restful-controller命令创建一个最简单的controller,但是功能是全的。
generate-all命令创建一个包含所有代码的controller,功能与上一个命令创建的相同,但是可以修改代码,便于自行修改。
编辑controller
package org.demo
import grails.rest.*
import grails.converters.*

class BookController extends RestfulController {
    static responseFormats = ['json', 'xml', 'hal']
    BookController() {
        super(Book)
    }
}
主要是添加了format,'hal'
 
5、启动前的设置
UrlMappings
//        "/$controller/$action?/$id?(.$format)?"{
//            constraints {
//                // apply constraints here
//            }
//        }
        "/books"(resources:"book")
 注掉/$controller/$action?/$id?(.$format)?,原因是这个设置会暴露所有的controller,在实际项目中不太安全,但是测试时还是很好用。
添加"/books"(resources:"book")
 
添加一些数据
grails-app/init/BootStrap.groovy
import org.demo.Book

class BootStrap {
    def init = { servletContext ->
        new Book(title :"The Stand" ).save()
        new Book(title :"The Shining" ).save()
    }
    def destroy = {
    }
}
 
6、启动、测试
执行gradle Task
build
bootRun
如果不明白可以参考上一篇博文
 
使用postman请求,当然也可以用Linux的curl命令
 
GET http://localhost:8080/books
得到返回
[
  {
    "id": 1,
    "title": "The Stand"
  },
  {
    "id": 2,
    "title": "The Shining"
  }
]
 
请求 GET http://localhost:8080/books.xml
得到返回
<?xml version="1.0" encoding="UTF-8"?>
<list>
    <book id="1">
        <title>The Stand</title>
    </book>
    <book id="2">
        <title>The Shining</title>
    </book>
</list> 
grails3可以根据后缀返回对应的格式,默认是json。
 
7、添加HAL
添加hal渲染
grails-app/conf/resources.groovy
import grails.rest.render.hal.*
// Place your Spring DSL code here
beans = {
    halBookRenderer(HalJsonRenderer, org.demo.Book)
    halBookCollectionRenderer(HalJsonCollectionRenderer, org.demo.Book)
}
 
重新启动项目
请求 GET http://localhost:8080/books.hal
得到返回
{
  "_links": {
    "self": {
      "href": "http://localhost:8080/books.hal",
      "hreflang": "zh",
      "type": "application/hal+json"
    }
  },
  "_embedded": {
    "book": [
      {
        "_links": {
          "self": {
            "href": "http://localhost:8080/books/1",
            "hreflang": "zh",
            "type": "application/hal+json"
          }
        },
        "title": "The Stand",
        "version": 0
      },
      {
        "_links": {
          "self": {
            "href": "http://localhost:8080/books/2",
            "hreflang": "zh",
            "type": "application/hal+json"
          }
        },
        "title": "The Shining",
        "version": 0
      }
    ]
  }
}
 
8、安全认证
使用插件 grails spring security rest
8.1、安装
build.gradle
dependencies {
     //Other dependencies
     ....
     compile "org.grails.plugins:spring-security-rest:2.0.0.M2"
}
 
修改dependencies后,需要刷一下Eclipse的项目,否则在eclipse里会提示编译错误。
刷新方法,右键点击项目->Gradle->Refresh Gradle Project
 
8.2、添加权限相关Domain类
在grails-app/domain目录下增加Role、User、UserRole。例子如下,也可以根据具体情况自行修改。
org.demo.Role.groovy
package org.demo

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

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

  private static final long serialVersionUID = 1

  String authority

  Role(String authority) {
    this()
    this.authority = authority
  }

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

  static mapping = {
    cache true
  }
}
 
org.demo.User.groovy
package org.demo

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

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

  private static final long serialVersionUID = 1

  transient springSecurityService

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

  User(String username, String password) {
    this()
    this.username = username
    this.password = password
  }

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

  def beforeInsert() {
    encodePassword()
  }

  def beforeUpdate() {
    if (isDirty('password')) {
      encodePassword()
    }
  }

  protected void encodePassword() {
    password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
  }

  static transients = ['springSecurityService']

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

  static mapping = {
    password column: '`password`'
  }
}
 
org.demo.UserRole.groovy
package org.demo

import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.apache.commons.lang.builder.HashCodeBuilder

@ToString(cache=true, includeNames=true, includePackage=false)
class UserRole implements Serializable {

  private static final long serialVersionUID = 1

  User user
  Role role

  UserRole(User u, Role r) {
    this()
    user = u
    role = r
  }

  @Override
  boolean equals(other) {
    if (!(other instanceof UserRole)) {
      return false
    }
    other.user?.id == user?.id && other.role?.id == role?.id
  }

  @Override
  int hashCode() {
    def builder = new HashCodeBuilder()
    if (user) builder.append(user.id)
    if (role) builder.append(role.id)
    builder.toHashCode()
  }

  static UserRole get(long userId, long roleId) {
    criteriaFor(userId, roleId).get()
  }

  static boolean exists(long userId, long roleId) {
    criteriaFor(userId, roleId).count()
  }

  private static DetachedCriteria criteriaFor(long userId, long roleId) {
    UserRole.where {
      user == User.load(userId) &&
      role == Role.load(roleId)
    }
  }

  static UserRole create(User user, Role role, boolean flush = false) {
    def instance = new UserRole(user: user, role: role)
    instance.save(flush: flush, insert: true)
    instance
  }

  static boolean remove(User u, Role r, boolean flush = false) {
    if (u == null || r == null) return false

    int rowCount = UserRole.where { user == u && role == r }.deleteAll()

    if (flush) { UserRole.withSession { it.flush() } }

    rowCount
  }

  static void removeAll(User u, boolean flush = false) {
    if (u == null) return

    UserRole.where { user == u }.deleteAll()

    if (flush) { UserRole.withSession { it.flush() } }
  }

  static void removeAll(Role r, boolean flush = false) {
    if (r == null) return

    UserRole.where { role == r }.deleteAll()

    if (flush) { UserRole.withSession { it.flush() } }
  }

  static constraints = {
    role validator: { Role r, UserRole ur ->
      if (ur.user == null || ur.user.id == null) return
      boolean existing = false
      UserRole.withNewSession {
        existing = UserRole.exists(ur.user.id, r.id)
      }
      if (existing) {
        return 'userRole.exists'
      }
    }
  }

  static mapping = {
    id composite: ['user', 'role']
    version false
  }
}
 
在grails-app/conf目录里添加application.groovy
// Added by the Spring Security Core plugin:
grails.plugin.springsecurity.userLookup.userDomainClassName='org.demo.User'
grails.plugin.springsecurity.authority.className='org.demo.Role'
grails.plugin.springsecurity.userLookup.authorityJoinClassName='org.demo.UserRole'
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
    [pattern: '/',               access: ['permitAll']],
    [pattern: '/error',          access: ['permitAll']],
    [pattern: '/index',          access: ['permitAll']],
    [pattern: '/index.gsp',      access: ['permitAll']],
    [pattern: '/shutdown',       access: ['permitAll']],
    [pattern: '/assets/**',      access: ['permitAll']],
    [pattern: '/**/js/**',       access: ['permitAll']],
    [pattern: '/**/css/**',      access: ['permitAll']],
    [pattern: '/**/images/**',   access: ['permitAll']],
    [pattern: '/**/favicon.ico', access: ['permitAll']]
]

grails.plugin.springsecurity.filterChain.chainMap = [
    [pattern: '/assets/**',      filters: 'none'],
    [pattern: '/**/js/**',       filters: 'none'],
    [pattern: '/**/css/**',      filters: 'none'],
    [pattern: '/**/images/**',   filters: 'none'],
    [pattern: '/**/favicon.ico', filters: 'none'],
    [pattern: '/api/**',         filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'],
    [pattern: '/books/**',       filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'] ,
    [pattern: '/**',             filters: 'JOINED_FILTERS,-restTokenValidationFilter,-restExceptionTranslationFilter']
]
 
给controller添加权限
org.demo.BookController.groovy
package org.demo

import grails.rest.*
import grails.converters.*
import grails.plugin.springsecurity.annotation.Secured

@Secured(['ROLE_ADMIN'])
class BookController extends RestfulController {
    static responseFormats = ['json', 'xml', 'hal']
    BookController() {
        super(Book)
    }
}
 
添加一个用户
grails-app/init/BootStrap.groovy
import org.demo.*

class BootStrap {

    def init = { servletContext ->
        Role admin = new Role("ROLE_ADMIN").save()
        User user = new User("user", "pass").save()
        UserRole.create(user, admin, true)

        new Book(title:"The Stand").save()
        new Book(title:"The Shining").save()
    }
    def destroy = {
    }
}
 
重新编译,启动
 
请求 GET http://localhost:8080/books
得到认证失败的结果
{
  "timestamp": 1461773336119,
  "status": 401,
  "error": "Unauthorized",
  "message": "No message available",
  "path": "/books"
}
 
登陆
POST http://localhost:8080/api/login
Body 选 raw JSON(application/json)
内容:{"username":"user", "password":"pass"}
 
得到结果
{
  "username": "user",
  "roles": [
    "ROLE_ADMIN"
  ],
  "token_type": "Bearer",
  "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTUDBcL2JRQlJcL0RrRWdrQ2hVQXFrRExNQldPUklkTTBING8xWW1WSVFzVkdwMXNSXC91d2ZuTzNKMGhXYXBNTURDQTJpSWg4Ulg0SnJEd0FhcDI2TXJjdGU4TXdTa0w2azMydTU5XC9cLzU2djdtRFFhSGdUYThhRjhWT1J4Vno2SnRWY3hnYkRUSFBiOFRPRE9rS2JJOVp5WUpNbWNIKzhFbmdCbEhoazRXV3d5dzVZUlRBWlZ6WmF1eGphYWx2RGd0THhBK09PWmdrZUtyM25QM0tIU3VNXC9BZ1cxZDFhQ29XMllZR0dvTW1uclNxNjBVNjR4Mm9ieFloYW9jTStOSmtPNlFXazVFNllmT29TU3RRUkdBWXl5ekg1V3BNclJXSGh4YnphelhGUWFhS3NCREtmTUdITDNKRW5ET3V2dTN0bVVsR0FmdmtDNW5YcDBxTHQ1QlwvVWRqMTlUUWxCcXJxU1phOHBFUlh5SE8zSGk3MDVcL3ZUMjk3RFpMQU5USjYrZVwvS2VhdmxxQjdcL2ZIUFRGNjBGMXFZNnJOZXdLcnRsTnhNRk14YkdwM3lqNHYzMzg3dmpqOE1rTEpEclA3XC9QdVlXSDVycjFGU1NNczJzNnRzUjBSNlczVE9STHoxUDN0dEN4Mlwvd0pCVklmNVMwR0QxS0ZNUVV0NnlWNlBWdFlXUnpJMWo1dExpOFwvcmJ1WHN2T0o0bU81Wm5kc3Z4QTBhcE9mcFwvZG5NNytKSUozTUhqQVJJWlUrWGdCcW1kSkNcL1hSMWZuMDZQZGZKM21BM3NcLzhGOUpYTGZvUUF3QUEiLCJzdWIiOiJ1c2VyIiwicm9sZXMiOlsiUk9MRV9BRE1JTiJdLCJleHAiOjE0NjE3Nzc2NzcsImlhdCI6MTQ2MTc3NDA3N30.UzEAN6CUbBsdH9QW13cxvBEjiWAkLcvX38st6IsWR3I",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTUDBcL2JRQlJcL0RrRWdrQ2hVQXFrRExNQldPUklkTTBING8xWW1WSVFzVkdwMXNSXC91d2ZuTzNKMGhXYXBNTURDQTJpSWg4Ulg0SnJEd0FhcDI2TXJjdGU4TXdTa0w2azMydTU5XC9cLzU2djdtRFFhSGdUYThhRjhWT1J4Vno2SnRWY3hnYkRUSFBiOFRPRE9rS2JJOVp5WUpNbWNIKzhFbmdCbEhoazRXV3d5dzVZUlRBWlZ6WmF1eGphYWx2RGd0THhBK09PWmdrZUtyM25QM0tIU3VNXC9BZ1cxZDFhQ29XMllZR0dvTW1uclNxNjBVNjR4Mm9ieFloYW9jTStOSmtPNlFXazVFNllmT29TU3RRUkdBWXl5ekg1V3BNclJXSGh4YnphelhGUWFhS3NCREtmTUdITDNKRW5ET3V2dTN0bVVsR0FmdmtDNW5YcDBxTHQ1QlwvVWRqMTlUUWxCcXJxU1phOHBFUlh5SE8zSGk3MDVcL3ZUMjk3RFpMQU5USjYrZVwvS2VhdmxxQjdcL2ZIUFRGNjBGMXFZNnJOZXdLcnRsTnhNRk14YkdwM3lqNHYzMzg3dmpqOE1rTEpEclA3XC9QdVlXSDVycjFGU1NNczJzNnRzUjBSNlczVE9STHoxUDN0dEN4Mlwvd0pCVklmNVMwR0QxS0ZNUVV0NnlWNlBWdFlXUnpJMWo1dExpOFwvcmJ1WHN2T0o0bU81Wm5kc3Z4QTBhcE9mcFwvZG5NNytKSUozTUhqQVJJWlUrWGdCcW1kSkNcL1hSMWZuMDZQZGZKM21BM3NcLzhGOUpYTGZvUUF3QUEiLCJzdWIiOiJ1c2VyIiwicm9sZXMiOlsiUk9MRV9BRE1JTiJdLCJpYXQiOjE0NjE3NzQwODN9.sv4cdahezBEOsy5cleOUUKHiwmKISHJCpx1kywwps_U"
}
 
需要认证的请求带上access_token,有效期3600秒(可以自行设置)。
 
请求 GET http://localhost:8080/books
Headers 
key:Authorization
value:
Bearer eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTUDBcL2JRQlJcL0RrRWdrQ2hVQXFrRExNQldPUklkTTBING8xWW1WSVFzVkdwMXNSXC91d2ZuTzNKMGhXYXBNTURDQTJpSWg4Ulg0SnJEd0FhcDI2TXJjdGU4TXdTa0w2azMydTU5XC9cLzU2djdtRFFhSGdUYThhRjhWT1J4Vno2SnRWY3hnYkRUSFBiOFRPRE9rS2JJOVp5WUpNbWNIKzhFbmdCbEhoazRXV3d5dzVZUlRBWlZ6WmF1eGphYWx2RGd0THhBK09PWmdrZUtyM25QM0tIU3VNXC9BZ1cxZDFhQ29XMllZR0dvTW1uclNxNjBVNjR4Mm9ieFloYW9jTStOSmtPNlFXazVFNllmT29TU3RRUkdBWXl5ekg1V3BNclJXSGh4YnphelhGUWFhS3NCREtmTUdITDNKRW5ET3V2dTN0bVVsR0FmdmtDNW5YcDBxTHQ1QlwvVWRqMTlUUWxCcXJxU1phOHBFUlh5SE8zSGk3MDVcL3ZUMjk3RFpMQU5USjYrZVwvS2VhdmxxQjdcL2ZIUFRGNjBGMXFZNnJOZXdLcnRsTnhNRk14YkdwM3lqNHYzMzg3dmpqOE1rTEpEclA3XC9QdVlXSDVycjFGU1NNczJzNnRzUjBSNlczVE9STHoxUDN0dEN4Mlwvd0pCVklmNVMwR0QxS0ZNUVV0NnlWNlBWdFlXUnpJMWo1dExpOFwvcmJ1WHN2T0o0bU81Wm5kc3Z4QTBhcE9mcFwvZG5NNytKSUozTUhqQVJJWlUrWGdCcW1kSkNcL1hSMWZuMDZQZGZKM21BM3NcLzhGOUpYTGZvUUF3QUEiLCJzdWIiOiJ1c2VyIiwicm9sZXMiOlsiUk9MRV9BRE1JTiJdLCJleHAiOjE0NjE3Nzc2NzcsImlhdCI6MTQ2MTc3NDA3N30.UzEAN6CUbBsdH9QW13cxvBEjiWAkLcvX38st6IsWR3I
 
请求的value格式是token_type+空格+access_token。
得到结果
[
  {
    "id": 1,
    "title": "The Stand"
  },
  {
    "id": 2,
    "title": "The Shining"
  }
]
 
注意事项:
有些数据库user是关键字,所以更换数据库时可以改成person。