Grails 3 和 Spring Security REST 中的 Twitter OAuth
了解如何在 Grails 3 和 Spring Security REST 插件中使用 Twitter OAuth
作者:Ben Rhine、Sergio del Amo
Grails 版本 3.3.5
1 开始
在本指南中,我们将展示如何使用 Spring Security Rest 插件将 Twitter OAuth2 身份验证添加到您的应用。
1.1 所需条件
要完成本指南,您需要以下条件
-
抽出一些时间
-
一个像样的文本编辑器或 IDE
-
已安装 JDK 1.8 或更高版本,并已正确配置
JAVA_HOME
1.2 如何完成本指南
要开始,请执行以下操作
-
下载并解压该源代码
或
-
复制 Git 仓库
git clone https://github.com/grails-guides/grails-oauth-twitter.git
Grails 指南仓库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用,其中包含一些额外的代码,可让您先下手为强。 -
complete
完工的示例。这是完成指南中演示的步骤并向initial
文件夹应用这些变更的结果。
要完成本指南,请转到 initial
文件夹
-
cd
进入grails-guides/grails-oauth-twitter/initial
并按照下一部分中的说明进行操作。
在 cd 到 grails-guides/grails-oauth-twitter/complete 后,您可以直接进入完成的示例 |
如果您希望从头开始,使用 Grails Application Forge 来创建新的 Grails 3 应用程序。
2 OAuth 身份验证
OAuth2 是一种行业标准身份验证协议,许多财富 500 强企业用来保护网站和应用程序。其运作机制允许第三方授权服务器通过帐户所有者批准访问来发布访问令牌。在我们的示例中,我们将使用 Twitter,因此用更通俗的语言来说,即 Twitter 用户允许他们的帐户向请求应用程序发布访问令牌。
2.1 设置和配置 Twitter OAuth
要在您的应用程序上启动和运行 Twitter OAuth,需要在 Twitter 开发者控制台 中花费一点时间进行工作和配置。进入并单击应用
选择开始使用标准访问
这会为您提供有关如何将您的应用程序连接到 Twitter 的快速说明。点击链接 apps.twitter.com
。
这会将您带到 Twitter 的应用程序管理
在 Twitter 应用程序管理页面点击创建新应用程序
Twitter 不接受 localhost ,所以改用本地 ip 地址 127.0.0.1 。 |
使用应用名称、基本说明、您的网址以及回调(您希望 Twitter 在您登录后返回到哪里)填写表单。然后点击创建您的 Twitter 应用程序
。
现在您应该看到您的应用程序已在 Twitter 上成功创建。
接下来选择密钥和访问令牌
选项卡
所有这些都是在 Twitter 中需要完成的设置。保存您的消费者密钥和密文,便于快速访问。现在,我们将看看使用我们的 Twitter 凭证来设置我们的应用程序。
3 设置您的应用程序
随着我们所有的 Twitter 配置就绪,现在是时候针对使用安全功能并通过 Twitter 通过 REST 借助 OAuth2 连接进行配置我们的应用程序了。
下面的图表说明了我们将实施的安全解决方案。
3.1 添加安全依赖项
我们要做的第一件事是向我们的 build.gradle
文件添加spring-security-core
和 spring-security-rest
插件。
compile 'org.grails.plugins:spring-security-core:3.2.1'
compile 'org.grails.plugins:spring-security-rest:2.0.0.RC1'
3.2 自定义令牌阅读器
我们覆盖默认令牌阅读器以从 cookie 阅读 JWT 令牌。
实现TokenReader
package demo
import grails.plugin.springsecurity.rest.token.AccessToken
import grails.plugin.springsecurity.rest.token.reader.TokenReader
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletRequest
@Slf4j
@CompileStatic
class JwtCookieTokenReader implements TokenReader {
final static String DEFAULT_COOKIE_NAME = 'JWT'
String cookieName = DEFAULT_COOKIE_NAME
@Override
AccessToken findToken(HttpServletRequest request) {
log.debug "Looking for jwt token in a cookie named {}", cookieName
String tokenValue = null
Cookie cookie = request.getCookies()?.find { Cookie cookie -> cookie.name.equalsIgnoreCase(cookieName) }
if ( cookie ) {
tokenValue = cookie.value
}
log.debug "Token: ${tokenValue}"
return tokenValue ? new AccessToken(tokenValue) : null
}
}
在 grails-app/conf/spring/resources.groovy
中将它注册为 tokenReader
。
import demo.JwtCookieTokenReader
beans {
tokenReader(JwtCookieTokenReader) {
cookieName = 'jwt'
}
...
..
.
}
3.3 配置安全
在我们添加了依赖项后,我们需要配置安全。
使用以下内容创建一个 application.groovy
文件staticRules
配置。
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 Spring Security Rest Plugin
grails {
plugin {
springsecurity {
rest {
token {
validation {
useBearerToken = false (1)
enableAnonymousAccess = true (2)
}
storage {
jwt {
secret = 'foobar123'*4 (3)
}
}
}
oauth {
frontendCallbackUrl = { String tokenValue -> "http://127.0.0.1:8080/auth/success?token=${tokenValue}" } (4)
twitter {
client = org.pac4j.oauth.client.TwitterClient (5)
key = '${TWITTER_KEY}' (6)
secret = '${TWITTER_SECRET}' (7)
defaultRoles = [] (8)
}
}
}
providerNames = ['anonymousAuthenticationProvider'] (9)
}
}
}
1 | 您必须禁用携带令牌支持才能注册您自己的 tokenReader 实施。 |
2 | 为应用 Grails Spring Security Rest 插件的过滤器后,启用对 URL 的匿名访问 |
3 | 用来签名 JWT 令牌的必需的机密 |
4 | 回调 URL - 使用 Twitter 进行身份验证后,这个回调 URL 会被调用,并带有用于对用户进行身份验证的 JWT 令牌 |
5 | 使用哪个 pac4j 客户端;以我们的案例来说,就是 Twitter 客户端 |
6 | 在启动应用程序时,将 Twitter Consumer Key(API Key)作为系统属性 TWITTER_KEY 提供 |
7 | 在启动应用程序时,将 Twitter Consumer Secret(API Secret)作为系统属性 TWITTER_SECRET 提供 |
8 | 对通过 Twitter 身份验证的用户进行的指定角色 |
9 | 我们将只针对 Twitter 对用户进行身份验证。因此,我们只使用匿名身份验证提供程序。在 Spring Security Core 插件文档 中了解有关认证提供程序的更多信息 |
要启动应用程序,你需要将 client_id
和 client_secret
作为系统属性提供。例如:
`./gradlew -DTWITTER_KEY=XXXXXX -DTWITTER_SECRET=XXXX bootRun
我们希望我们的应用程序默认情况下是无状态的,一些端点允许匿名访问
String ANONYMOUS_FILTERS = 'anonymousAuthenticationFilter,restTokenValidationFilter,restExceptionTranslationFilter,filterInvocationInterceptor' (1)
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern: '/dbconsole/**', filters: 'none'],
[pattern: '/assets/**', filters: 'none'],
[pattern: '/**/js/**', filters: 'none'],
[pattern: '/**/css/**', filters: 'none'],
[pattern: '/**/images/**', filters: 'none'],
[pattern: '/**/favicon.ico', filters: 'none'],
[pattern: '/', filters: ANONYMOUS_FILTERS], (1)
[pattern: '/book/show/*', filters: ANONYMOUS_FILTERS], (1)
[pattern: '/bookFavourite/index', filters: ANONYMOUS_FILTERS], (1)
[pattern: '/auth/success', filters: ANONYMOUS_FILTERS], (1)
[pattern: '/oauth/authenticate/twitter', filters: ANONYMOUS_FILTERS], (1)
[pattern: '/oauth/callback/twitter', filters: ANONYMOUS_FILTERS], (1)
[pattern: '/**', filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'], (1)
]
1 | 当未发送令牌时,允许匿名访问的无状态链。但是,如果请求中有令牌,它将被验证 |
2 | /** 是一个不允许匿名访问的无状态链。因此,令牌将总是必需的,并且如果丢失,系统会向客户端发回 Bad Request 响应 |
我们不会将用户信息保存在数据库中。你可能已经注意到项目中没有 User 、 Role 、 UserRole 领域类。我们也没有设置诸如 userLookUp.userDomainClass 等的配置值 |
现在我们覆盖 login/auth
视图。因此,我们不再在该页面中显示用户名/密码表单
<html>
<head>
<meta name="layout" content="${gspLayout ?: 'main'}"/>
<title><g:message code='springSecurity.login.title'/></title>
</head>
<body>
<div id="login">
<div class="inner centered" >
<div class="fheader"><g:message code='springSecurity.login.header'/></div>
<g:if test='${flash.message}'>
<div class="login_message">${flash.message}</div>
</g:if>
</div>
</div>
</body>
</html>
我们没有在该页面中包含使用 Twitter 登录的按钮,因为我们在根布局 main.gsp 文件中已经包含了该按钮
3.4 注销处理程序
Spring Security 允许注册自定义 注销处理程序。注册一个新的注销处理程序来清除 JWT cookie。我们重新使用了 Spring Security 附带的 CookieClearingLogoutHandler
。
修改 grails-app/conf/spring/resources.groovy
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
beans {
...
..
.
cookieClearingLogoutHandler(CookieClearingLogoutHandler, ['jwt'])
}
将自定义注销处理程序添加到 Spring Security Core 插件 logout.handlerNames
。
grails.plugin.springsecurity.logout.handlerNames = ['rememberMeServices', 'securityContextLogoutHandler', 'cookieClearingLogoutHandler']
3.5 JWT Cookie
创建 AuthController.groovy
。当用户使用 Twitter 成功登录时,将调用 AuthController.success
。
AuthController.success 的路径在 application.groovy 中的 frontendCallbackUrl 中使用。 |
package demo
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import grails.plugin.springsecurity.annotation.Secured
import grails.plugin.springsecurity.rest.token.reader.TokenReader
import groovy.util.logging.Slf4j
import javax.servlet.http.Cookie
@Slf4j
class AuthController implements GrailsConfigurationAware {
TokenReader tokenReader
int jwtExpiration
String grailsServerUrl
static allowedMethods = [
success: 'GET',
logout: 'POST'
]
@Secured('permitAll')
def success(String token) {
log.debug('token value {}', token)
if (token) {
Cookie cookie = jwtCookie(token)
response.addCookie(cookie) (1)
}
[grailsServerUrl: grailsServerUrl]
}
protected Cookie jwtCookie(String tokenValue) {
Cookie jwtCookie = new Cookie( cookieName(), tokenValue )
jwtCookie.maxAge = jwtExpiration (5)
jwtCookie.path = '/'
jwtCookie.setHttpOnly(httpOnly()) (3)
if ( httpOnly() ) {
jwtCookie.setSecure(true) (4)
}
jwtCookie
}
@Override
void setConfiguration(Config co) {
jwtExpiration = co.getProperty('grails.plugin.springsecurity.rest.token.storage.memcached.expiration', Integer, 3600) (5)
grailsServerUrl = co.getProperty('grails.serverURL', String)
}
protected boolean httpOnly() {
grailsServerUrl?.startsWith('https')
}
protected String cookieName() {
if ( tokenReader instanceof JwtCookieTokenReader ) {
return ((JwtCookieTokenReader) tokenReader).cookieName (6)
}
return 'jwt'
}
}
1 | 响应一个 Cookie,它的值为 JWT 令牌 |
2 | 使用与名称相同并且 maxAge 等于 0 的 Cookie 进行响应会删除该 Cookie。这样,用户就注销了。 |
3 | 阻止网站中执行的任何 JavaScript(甚至您自己的 JavaScript)执行 document.cookies 并访问 cookie |
4 | 如果您执行 http://,而不是 https://,cookie 不会留下。您应该在生产环境中使用 https。 |
5 | 将 cookie 过期时间设置为与 JWT 过期时间相匹配 |
6 | 使用相同的 cookie 名称,自定义 tokenReader 将使用此前定义的内容进行预期。 |
由于此应用程序的安全解决方案的无状态特性,注销用户需要删除包含其 JWT 令牌的 cookie。 |
success
操作的 GSP 执行到主页的简单重定向。我们在客户端执行重定向以确保正确设置 cookie。
<html>
<head>
<meta http-equiv="refresh" content="0; url=${grailsServerUrl ?: 'http://localhost:8080'}/" />
</head>
<body><g:message code="redirecting" default="Redirecting..."/></body>
</html>
3.6 增强日志记录
如果您希望启用增强日志记录以便在调用 Twitter OAuth API 时查看返回内容,请将以下内容添加到 logback.groovy
文件末尾
logger("org.springframework.security", DEBUG, ['STDOUT'], false)
logger("grails.plugin.springsecurity", DEBUG, ['STDOUT'], false)
logger("org.pac4j", DEBUG, ['STDOUT'], false)
4 构建您的应用程序
我们即将构建一个应用程序,该应用程序会列出书籍并允许我们收藏书籍。在使用 Twitter 登录后,收藏按钮才会可用。
4.1 您的域名
对于您的域名,我们需要域对象 Book
和 BookFavourite
。按如下所示创建
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Book {
String image
String title
String author
String about
String href
static mapping = {
image nullable: false
title nullable: false
author nullable: false
about nullable: false
href nullable: false
about type: 'text'
}
}
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class BookFavourite {
Long bookId
String username
static constraints = {
bookId nullable: false
username nullable: false
}
}
4.2 您的数据
现阶段,我们将创建我们的图书数据,这样我们就有东西可以继续处理了。继续,让您的 BootStrap.groovy
与以下内容相匹配。
所需的图像资源已添加到初始项目中以供您使用。 |
package demo
import groovy.transform.CompileStatic
@CompileStatic
class BootStrap {
public final static List< Map<String, String> > GRAILS_BOOKS = [
[
title : 'Grails 3 - Step by Step',
author: 'Cristian Olaru',
href: 'https://grailsthreebook.com/',
about : 'Learn how a complete greenfield application can be implemented quickly and efficiently with Grails 3 using profiles and plugins. Use the sample application that accompanies the book as an example.',
image: 'grails_3_step_by_step.png',
],
[
title : 'Practical Grails 3',
author: ' Eric Helgeson',
href : 'https://www.grails3book.com/',
about : 'Learn the fundamental concepts behind building Grails applications with the first book dedicated to Grails 3. Real, up-to-date code examples are provided, so you can easily follow along.',
image: 'pratical-grails-3-book-cover.png',
],
[
title : 'Falando de Grails',
author: 'Henrique Lobo Weissmann',
href : 'http://www.casadocodigo.com.br/products/livro-grails',
about : 'This is the best reference on Grails 2.5 and 3.0 written in Portuguese. It's a great guide to the framework, dealing with details that many users tend to ignore.',
image: 'grails_weissmann.png',
],
[
title : 'Grails Goodness Notebook',
author: 'Hubert A. Klein Ikkink',
href : 'https://leanpub.com/grails-goodness-notebook',
about : 'Experience the Grails framework through code snippets. Discover (hidden) Grails features through code examples and short articles. The articles and code will get you started quickly and provide deeper insight into Grails.',
image: 'grailsgood.png',
],
[
title : 'The Definitive Guide to Grails 2',
author: 'Jeff Scott Brown and Graeme Rocher',
href : 'http://www.apress.com/9781430243779',
about : 'As the title states, this is the definitive reference on the Grails framework, authored by core members of the development team.',
image: 'grocher_jbrown_cover.jpg',
],
[
title : 'Grails in Action',
author: 'Glen Smith and Peter Ledbrook',
href : 'http://www.manning.com/gsmith2/',
about : 'The second edition of Grails in Action is a comprehensive introduction to Grails 2 focused on helping you become super-productive fast.',
image: 'gsmith2_cover150.jpg',
],
[
title : 'Grails 2: A Quick-Start Guide',
author: 'Dave Klein and Ben Klein',
href : 'http://www.amazon.com/gp/product/1937785777?tag=misa09-20',
about : 'This revised and updated edition shows you how to use Grails by iteratively building a unique, working application.',
image : 'bklein_cover.jpg',
],
[
title : 'Programming Grails',
author: 'Burt Beckwith',
href : 'http://shop.oreilly.com/product/0636920024750.do',
about : 'Dig deeper into Grails architecture and discover how this application framework works its magic.',
image: 'bbeckwith_cover.gif'
]
] as List< Map<String, String> >
public final static List< Map<String, String> > GROOVY_BOOKS = [
[
title: 'Making Java Groovy',
author: 'Ken Kousen',
href: 'http://www.manning.com/kousen/',
about: 'Make Java development easier by adding Groovy. Each chapter focuses on a task Java developers do, like building, testing, or working with databases or restful web services, and shows ways Groovy can make those tasks easier.',
image: 'Kousen-MJG.png',
],
[
title: 'Groovy in Action, 2nd Edition',
author: 'Dierk König, Guillaume Laforge, Paul King, Cédric Champeau, Hamlet D\'Arcy, Erik Pragt, and Jon Skeet',
href: 'http://www.manning.com/koenig2/',
about: 'This is the undisputed, definitive reference on the Groovy language, authored by core members of the development team.',
image: 'regina.png',
],
[
title: 'Groovy for Domain-Specific Languages',
author: 'Fergal Dearle',
href: 'http://www.packtpub.com/groovy-for-domain-specific-languages-dsl/book',
about: 'Learn how Groovy can help Java developers easily build domain-specific languages into their applications.',
image: 'gdsl.jpg',
],
[
title: 'Groovy 2 Cookbook',
author: 'Andrey Adamovitch, Luciano Fiandeso',
href: 'http://www.packtpub.com/groovy-2-cookbook/book',
about: 'This book contains more than 90 recipes that use the powerful features of Groovy 2 to develop solutions to everyday programming challenges.',
image: 'g2cook.jpg',
],
[
title: 'Programming Groovy 2',
author: 'Venkat Subramaniam',
href: 'http://pragprog.com/book/vslg2/programming-groovy-2',
about: 'This book helps experienced Java developers learn to use Groovy 2, from the basics of the language to its latest advances.',
image: 'vslg2.jpg'
],
] as List< Map<String, String> >
BookDataService bookDataService
def init = { servletContext ->
for (Map<String, String> bookInfo : (GRAILS_BOOKS + GROOVY_BOOKS)) {
bookDataService.save(bookInfo.title, bookInfo.author, bookInfo.about, bookInfo.href, bookInfo.image)
}
}
def destroy = {
}
}
4.3 您的服务
接下来,我们需要创建服务来返回我们的所有书籍和我们的收藏夹图书。首先,我们利用数据服务来实现基本功能。
package demo
import grails.gorm.services.Service
import grails.gorm.transactions.ReadOnly
import groovy.transform.CompileDynamic
import org.grails.datastore.mapping.query.api.BuildableCriteria
import org.hibernate.transform.Transformers
interface IBookDataService {
Book save(String title, String author, String about, String href, String image)
Number count()
Book findById(Long id)
}
@Service(Book)
abstract class BookDataService implements IBookDataService {
@CompileDynamic
@ReadOnly
List<BookImage> findAll() {
BuildableCriteria c = Book.createCriteria()
c.list {
resultTransformer(Transformers.aliasToBean(BookImage))
projections {
property('id', 'id')
property('image', 'image')
}
}
}
@CompileDynamic
@ReadOnly
List<BookImage> findAllByIds(List<Long> ids) {
BuildableCriteria c = Book.createCriteria()
c.list {
inList('id', ids)
resultTransformer(Transformers.aliasToBean(BookImage))
projections {
property('id', 'id')
property('image', 'image')
}
}
}
}
为 BookFavourite
域类 CRUD 操作创建一个 GORM 数据服务。
package demo
import grails.gorm.services.Query
import grails.gorm.services.Service
@Service(BookFavourite)
interface BookFavouriteDataService {
BookFavourite save(Long bookId, String username)
void delete(Long bookId, String username)
@Query("select $b.bookId from ${BookFavourite b} where $b.username = $username") (1)
List<Long> findBookIdByUsername(String username)
BookFavourite findByBookIdAndUsername(Long bookId, String username)
}
4.4 您的控制器
创建服务层后,我们需要创建控制器。首先,我们创建基本图书控制器,其可以返回所有书籍的列表或显示选定的图书。
package demo
import grails.plugin.springsecurity.SpringSecurityService
import grails.plugin.springsecurity.annotation.Secured
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
@Secured('permitAll')
@CompileStatic
class BookController {
static allowedMethods = [index: 'GET', show: 'GET']
BookDataService bookDataService
BookFavouriteDataService bookFavouriteDataService
SpringSecurityService springSecurityService
def index() {
[bookList: bookDataService.findAll()]
}
def show(Long id) {
[
bookInstance: bookDataService.findById(id),
bookFavourite: bookFavouriteDataService.findByBookIdAndUsername(id, loggedUsername())
]
}
@CompileDynamic
protected String loggedUsername() {
final String username = springSecurityService.principal?.username
username
}
}
接下来是收藏控制器,其可以返回我们收藏的一系列图书,以及收藏所需图书的操作。
package demo
import grails.plugin.springsecurity.SpringSecurityService
import grails.plugin.springsecurity.annotation.Secured
import groovy.transform.CompileDynamic
class BookFavouriteController {
static allowedMethods = [
index: 'GET',
favourite: 'POST',
unfavourite: 'POST',
]
SpringSecurityService springSecurityService
BookFavouriteDataService bookFavouriteDataService
BookDataService bookDataService
@Secured('permitAll')
def index() {
String username = loggedUsername()
List<Long> bookIds = bookFavouriteDataService.findBookIdByUsername(username) (1)
List<BookImage> bookList = bookDataService.findAllByIds(bookIds) (2)
render view: '/book/index', model: [bookList: bookList]
}
@Secured('isAuthenticated()')
def favourite(Long bookId) {
String username = loggedUsername()
bookFavouriteDataService.save(bookId, username) (3)
redirect(action: 'index')
}
@Secured('isAuthenticated()')
def unfavourite(Long bookId) {
String username = loggedUsername()
bookFavouriteDataService.delete(bookId, username)
redirect(action: 'index')
}
@CompileDynamic
protected String loggedUsername() {
springSecurityService.principal.username
}
}
1 | 调用我们的自定义收藏图书查找器 |
2 | 调用我们自定义的 findAll |
3 | 调用来存储我们的收藏 |
4.5 您的视图
我们最终准备好用工作界面来将所有内容关联起来。首先,让我们为书籍创建一个索引页面,以便在位于 views/book
处查看它们的列表。
<html>
<head>
<title>Groovy & Grails Books</title>
<meta name="layout" content="main" />
</head>
<body>
<div id="content" role="main">
<section class="row colset-2-its">
<g:each in="${bookList}" var="${book}">
<g:link controller="book" id="${book.id}" action="show">
<asset:image src="${book.image}" width="200" />
</g:link>
</g:each>
</section>
</div>
</body>
</html>
接下来,我们将在 views/books
中创建一个 show.gsp
,这样当我们选择一本书时,便可以查看其详细信息。
<html>
<head>
<title>${bookInstance?.title}</title>
<meta name="layout" content="main" />
</head>
<body>
<div id="content" role="main">
<section class="row colset-2-its">
<g:if test="${bookInstance}">
<h1><a href="${bookInstance.href}">${bookInstance.title}</a></h1>
<sec:ifLoggedIn>
<g:form controller="bookFavourite" action="${bookFavourite != null ? 'unfavourite' : 'favourite'}">
<g:hiddenField name="bookId" value="${bookInstance.id}"/>
<g:if test="${bookFavourite}">
<input type="submit" class="btn btn-default" value="${g.message(code: 'book.unfavourite', default: 'Unfavourite')}"/>
</g:if>
<g:else>
<input type="submit" class="btn btn-default" value="${g.message(code: 'book.favourite', default: 'Favourite')}"/>
</g:else>
</g:form>
</sec:ifLoggedIn>
<p>${bookInstance.about}</p>
<h2><g:message code="book.author" args="[bookInstance.author]" default="By {0}"/></h2>
<asset:image src="${bookInstance.image}" width="200" />
</g:if>
</section>
</div>
</body>
</html>
在上述代码中,我们将最喜爱的按钮包装在已登录的检查中,因为只有在我们登录的情况下,我们才能收藏一本书
最后,我们在布局文件中添加了一个允许我们选择所有书籍、最喜欢的书籍或登录/注销的简单菜单将其全部捆绑在一起。我们将这个菜单添加到导航 div 和 <g:layoutBody/>
之间。
<div class="centered" style="margin: 10px auto;">
<g:link controller="book" action="index">
<g:message code="book.all" default="All"/>
</g:link>
<span>|</span>
<g:link controller="bookFavourite" action="index">
<g:message code="book.favourite" default="Favourites"/>
</g:link>
<span>|</span>
<sec:ifNotLoggedIn>
<g:render template="/auth/loginWithTwitter"/>
</sec:ifNotLoggedIn>
<sec:ifLoggedIn>
<g:form controller="logout" style="display: inline;">
<input type="submit" value="${g.message(code: "logout", default:"Logout")}"/>
</g:form>
</sec:ifLoggedIn>
</div>
<g:if test="${flash.message}">
<div class="message" style="display: block">${flash.message}</div>
</g:if>
在上述代码中,我们
-
将我们的菜单居中;链接到列出所有书籍;链接到列出最喜欢的书籍,“使用 Twitter 登录”/注销
-
使用登录链接包含模板
-
使用注销链接包含模板
-
添加一个消息块,以便我们可以查看我们的登录/注销消息
我们创建一个模板,其中包含我们的链接以通过 Twitter 触发我们的 OAuth 登录;链接 /oauth/authenticate
由 spring-security-rest
插件提供,并以 /twitter
结尾,作为链接 /oauth/authenticate/twitter
的提供程序。这会将你重定向到早已熟悉的常规 Twitter 登录页面。
<a href="/oauth/authenticate/twitter">
<asset:image src="sign-in-with-twitter-link.png"
alt="${g.message(code: "login.twitter", default:"Login with Twitter")}" height="10"/>
</a>
sign-in-with-twitter-link.png 已经为你提供了资产 |
5 运行已完成的应用程序
使用 Gradle bootRun
任务运行该应用程序。
$ ./gradlew bootRun
如前所述。现在我们的应用程序正在运行,导航至 http://localhost:8080 以查看以下内容。
选择一本书,并查看在未登录时没有任何 收藏
按钮可用。
从菜单中单击 使用 Twitter 登录
并输入你的 Twitter 凭据
在你成功地重定向之后,你可以检查并找出存储为 Cookie 的 JWT 令牌
选择一本书,并查看 收藏
按钮现在可用。
单击注销,你将看到显示我们的注销消息。
6 后续步骤
要进一步了解,请阅读 Grails Spring Security Rest 和 Spring Security Core 文档。