Grails SDK
下载SDK,配置环境变量GRAILS_HOME和PATH
grails-4.0 .3 .zip
Create APP
grails create-app cn.duchaoqun.demo
cd demo
run-app
Install Grails Spring Security Core Plugin
compile "org.grails.plugins:spring-security-core:4.0.0.RC2"
stop-app
compile
Create User, Role, and Product Domain Class
在添加完插件之后,就可以创建如下命令创建 Domain 了。
s2- quickstart cn.duchaoqun User Role
修改grails-app/domain/cn/duchaoqun/Role.groovy
String toString ( ) {
authority
}
修改grails-app/domain/cn/duchaoqun/User.groovy
// 在password字段后面添加
String fullname
// 添加约束
static constraints = {
password nullable: false , blank: false , password: true
username nullable: false , blank: false , unique: true
fullname nullable: false , blank: false
}
创建 Product实体 grails-app/domain/cn/duchaoqun/Product.groovy
create-domain-class cn .duchaoqun .Product
package cn.duchaoqun
class Product {
String prodCode
String prodName
String prodModel
String prodDesc
String prodImageUrl
String prodPrice
static constraints = {
prodCode nullable : false , blank : false
prodName nullable : false , blank : false
prodModel nullable : false , blank : false
prodDesc nullable : false , blank : false
prodImageUrl nullable : true
prodPrice nullable : false , blank : false
}
String toString ( ) {
prodName
}
}
Create CustomUserDetailsService
因为我们修改了User实体,这里我们需要自定义一个CustomUserDetails,创建src/main/groovy/cn/duchaoqun/CustomUserDetails.groovy
package cn.duchaoqun
import grails.plugin.springsecurity.userdetails.GrailsUser
import org.springframework.security.core.GrantedAuthority
class CustomUserDetails extends GrailsUser {
final String fullname
CustomUserDetails (String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked,
Collection <GrantedAuthority > authorities,
long id, String fullname) {
super (username, password, enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, authorities, id)
this .fullname = fullname
}
}
create -service cn.duchaoqun.CustomUserDetails
package cn.duchaoqun
import grails.plugin.springsecurity.SpringSecurityUtils
import grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
import grails.plugin.springsecurity.userdetails.NoStackUsernameNotFoundException
import grails.gorm.transactions.Transactional
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException
class CustomUserDetailsService implements GrailsUserDetailsService {
static final List NO_ROLES = [new SimpleGrantedAuthority (SpringSecurityUtils.NO_ROLE)]
UserDetails loadUserByUsername (String username, boolean loadRoles)
throws UsernameNotFoundException {
return loadUserByUsername(username)
}
@Transactional(readOnly=true, noRollbackFor=[IllegalArgumentException, UsernameNotFoundException])
UserDetails loadUserByUsername (String username) throws UsernameNotFoundException {
User user = User.findByUsername(username)
if (!user) throw new NoStackUsernameNotFoundException ()
def roles = user.authorities
def authorities = roles.collect {
new SimpleGrantedAuthority (it.authority)
}
return new CustomUserDetails (user.username, user.password, user.enabled,
!user.accountExpired, !user.passwordExpired,
!user.accountLocked, authorities ?: NO_ROLES, user.id,
user.fullname)
}
}
然后再 grails-app/conf/spring/resources.groovy
中注册新的 Service
import cn.duchaoqun.UserPasswordEncoderListener
import cn.duchaoqun.CustomUserDetailsService
beans = {
userPasswordEncoderListener (UserPasswordEncoderListener)
userDetailsService (CustomUserDetailsService)
}
Override Login Auth View
自定义登录页面。
在 views 文件夹创建一个login 文件夹,然后再里面创建 auth.gsp
grails-app/views/login/auth.gsp
<html >
<head >
<meta name ="layout" content ="${gspLayout ?: 'main'}" />
<title > <g:message code ='springSecurity.login.title' /> </title >
</head >
<body >
<div class ="row" >
<div class ="col-sm-9 col-md-7 col-lg-5 mx-auto" >
<div class ="card card-signin my-5" >
<div class ="card-body" >
<h5 class ="card-title text-center" > Please Login</h5 >
<g:if test ='${flash.message}' >
<div class ="alert alert-danger" role ="alert" > ${flash.message}</div >
</g:if >
<form class ="form-signin" action ="${postUrl ?: '/login/authenticate'}" method ="POST" id ="loginForm" autocomplete ="off" >
<div class ="form-group" >
<label for ="username" > Username</label >
<input type ="text" class ="form-control" name ="${usernameParameter ?: 'username'}" id ="username" autocapitalize ="none" />
</div >
<div class ="form-group" >
<label for ="password" > Password</label >
<input type ="password" class ="form-control" name ="${passwordParameter ?: 'password'}" id ="password" />
<i id ="passwordToggler" title ="toggle password display" onclick ="passwordDisplayToggle()" > 👁 </i >
</div >
<div class ="form-group form-check" >
<label class ="form-check-label" >
<input type ="checkbox" class ="form-check-input" name ="${rememberMeParameter ?: 'remember-me'}" id ="remember_me" <g:if test ='${hasCookie}' > checked="checked"</g:if > /> Remember me
</label >
</div >
<button id ="submit" class ="btn btn-lg btn-primary btn-block text-uppercase" type ="submit" > Sign in</button >
<hr class ="my-4" >
<p > Don't have an account? <g:link controller ="register" > Register</g:link > </p >
</form >
</div >
</div >
</div >
</div >
<script type ="text/javascript" >
document .addEventListener ("DOMContentLoaded" , function (event ) {
document .forms ['loginForm' ].elements ['username' ].focus ();
});
function passwordDisplayToggle ( ) {
var toggleEl = document .getElementById ("passwordToggler" );
var eyeIcon = '\u{1F441}' ;
var xIcon = '\u{2715}' ;
var passEl = document .getElementById ("password" );
if (passEl.type === "password" ) {
toggleEl.innerHTML = xIcon;
passEl.type = "text" ;
} else {
toggleEl.innerHTML = eyeIcon;
passEl.type = "password" ;
}
}
</script >
</body >
</html >
将改页面设置为默认页面grails-app/controllers/UrlMappings.groovy
"/" (view: "index" )
"/" (controller:'login' , action:'auth' )
Add User Info and Logout to the Navbar
修改grails-app/views/layout/main.gsp
<!doctype html >
<html lang ="en" class ="no-js" >
<head >
<meta http-equiv ="Content-Type" content ="text/html; charset=UTF-8" />
<meta http-equiv ="X-UA-Compatible" content ="IE=edge" />
<title >
<g:layoutTitle default ="Grails" />
</title >
<meta name ="viewport" content ="width=device-width, initial-scale=1" />
<asset:link rel ="icon" href ="favicon.ico" type ="image/x-ico" />
<asset:stylesheet src ="application.css" />
<g:layoutHead />
</head >
<body >
<nav class ="navbar navbar-expand-lg navbar-dark navbar-static-top" role ="navigation" >
<a class ="navbar-brand" href ="/#" > <asset:image src ="grails.svg" alt ="Grails Logo" /> </a >
<button class ="navbar-toggler" type ="button" data-toggle ="collapse" data-target ="#navbarContent" aria-controls ="navbarContent" aria-expanded ="false" aria-label ="Toggle navigation" >
<span class ="navbar-toggler-icon" > </span >
</button >
<div class ="collapse navbar-collapse" aria-expanded ="false" style ="height: 0.8px;" id ="navbarContent" >
<ul class ="nav navbar-nav ml-auto" >
<g:pageProperty name ="page.nav" />
<sec:ifLoggedIn >
<li class ="nav-item dropdown" >
<a class ="nav-link dropdown-toggle" href ="#" id ="navbardrop" data-toggle ="dropdown" >
<sec:loggedInUserInfo field ='fullname' />
</a >
<div class ="dropdown-menu navbar-dark" >
<g:form controller ="logout" >
<g:submitButton class ="dropdown-item navbar-dark color-light" name ="Submit" value ="Logout" style ="color:gray" />
</g:form >
</div >
</li >
</sec:ifLoggedIn >
</ul >
</div >
</nav >
<div class ="container" >
<g:layoutBody />
</div >
<div class ="footer row" role ="contentinfo" >
<div class ="col" >
<a href ="http://guides.grails.org" target ="_blank" >
<asset:image src ="advancedgrails.svg" alt ="Grails Guides" class ="float-left" />
</a >
<strong class ="centered" > <a href ="http://guides.grails.org" target ="_blank" > Grails Guides</a > </strong >
<p > Building your first Grails app? Looking to add security, or create a Single-Page-App? Check out the <a href ="http://guides.grails.org" target ="_blank" > Grails Guides</a > for step-by-step tutorials.</p >
</div >
<div class ="col" >
<a href ="http://docs.grails.org" target ="_blank" >
<asset:image src ="documentation.svg" alt ="Grails Documentation" class ="float-left" />
</a >
<strong class ="centered" > <a href ="http://docs.grails.org" target ="_blank" > Documentation</a > </strong >
<p > Ready to dig in? You can find in-depth documentation for all the features of Grails in the <a href ="http://docs.grails.org" target ="_blank" > User Guide</a > .</p >
</div >
<div class ="col" >
<a href ="https://grails-slack.cfapps.io" target ="_blank" >
<asset:image src ="slack.svg" alt ="Grails Slack" class ="float-left" />
</a >
<strong class ="centered" > <a href ="https://grails-slack.cfapps.io" target ="_blank" > Join the Community</a > </strong >
<p > Get feedback and share your experience with other Grails developers in the community <a href ="https://grails-slack.cfapps.io" target ="_blank" > Slack channel</a > .</p >
</div >
</div >
<div id ="spinner" class ="spinner" style ="display:none;" >
<g:message code ="spinner.alt" default ="Loading… " />
</div >
<asset:javascript src ="application.js" />
</body >
</html >
Create Register Controller and View
create -controller cn.duchaoqun.Register
package cn.duchaoqun.
import grails.validation.ValidationException
import grails.gorm.transactions.Transactional
import grails.plugin.springsecurity.annotation.Secured
import cn.duchaoqun.User
import cn.duchaoqun.Role
import cn.duchaoqun.UserRole
@Transactional
@Secured('permitAll' )
class RegisterController {
static allowedMethods = [register: "POST" ]
def index () { }
def register () {
if (!params .password.equals (params .repassword)) {
flash.message = "Password and Re-Password not match"
redirect action: "index"
return
} else {
try {
def user = User.findByUsername(params .username)?: new User(username: params .username, password: params .password, fullname: params .fullname).save()
def role = Role.get (params .role.id)
if (user && role) {
UserRole.create user, role
UserRole.withSession {
it.flush()
it.clear()
}
flash.message = "You have registered successfully. Please login."
redirect controller: "login" , action: "auth"
} else {
flash.message = "Register failed"
render view: "index"
return
}
} catch (ValidationException e) {
flash.message = "Register Failed"
redirect action: "index"
return
}
}
}
}
创建 Register 页面 grails-app/views/register/index.gsp
<html >
<head >
<meta name ="layout" content ="${gspLayout ?: 'main'}" />
<title > Register</title >
</head >
<body >
<div class ="row" >
<div class ="col-sm-9 col-md-7 col-lg-5 mx-auto" >
<div class ="card card-signin my-5" >
<div class ="card-body" >
<h5 class ="card-title text-center" > Register Here</h5 >
<g:if test ='${flash.message}' >
<div class ="alert alert-danger" role ="alert" > ${flash.message}</div >
</g:if >
<form class ="form-signin" action ="register" method ="POST" id ="loginForm" autocomplete ="off" >
<div class ="form-group" >
<label for ="role" > Role</label >
<g:select class ="form-control" name ="role.id"
from ="${cn.duchaoqun.Role.list()}"
optionKey ="id" />
</div >
<div class ="form-group" >
<label for ="username" > Username</label >
<input type ="text" placeholder ="Your username" class ="form-control" name ="username" id ="username" autocapitalize ="none" />
</div >
<div class ="form-group" >
<label for ="password" > Password</label >
<input type ="password" placeholder ="Your password" class ="form-control" name ="password" id ="password" />
</div >
<div class ="form-group" >
<label for ="password" > Re-Enter Password</label >
<input type ="password" placeholder ="Re-enter password" class ="form-control" name ="repassword" id ="repassword" />
</div >
<div class ="form-group" >
<label for ="username" > Full Name</label >
<input type ="text" placeholder ="Your full name" class ="form-control" name ="fullname" id ="fullname" autocapitalize ="none" />
</div >
<button id ="submit" class ="btn btn-lg btn-primary btn-block text-uppercase" type ="submit" > Register</button >
<hr class ="my-4" >
<p > Already have an account? <g:link controller ="login" action ="auth" > Login</g:link > </p >
</form >
</div >
</div >
</div >
</div >
<script type ="text/javascript" >
document .addEventListener ("DOMContentLoaded" , function (event ) {
document .forms ['loginForm' ].elements ['username' ].focus ();
});
</script >
</body >
</html >
Create the Secure Product CRUD Scaffolding
generate -all cn.duchaoqun .Product
修改对应的 Controller,grails-app/controllers/ProductController.groovy
package cn.duchaoqun
import grails.validation .ValidationException
import static org.springframework .http .HttpStatus .*
import grails.plugin .springsecurity .annotation .Secured
class ProductController {
ProductService productService
static allowedMethods = [save : "POST" , update : "PUT" , delete : "DELETE" ]
@Secured (['ROLE_ADMIN' , 'ROLE_USER' ])
def index (Integer max ) {
params.max = Math .min (max ?: 10 , 100 )
respond productService.list (params), model :[productCount : productService.count ()]
}
@Secured (['ROLE_ADMIN' , 'ROLE_USER' ])
def show (Long id ) {
respond productService.get (id)
}
@Secured ('ROLE_ADMIN' )
def create ( ) {
respond new Product (params)
}
@Secured ('ROLE_ADMIN' )
def save (Product product ) {
if (product == null ) {
notFound ()
return
}
try {
productService.save (product)
} catch (ValidationException e) {
respond product.errors , view :'create'
return
}
request.withFormat {
form multipartForm {
flash.message = message (code : 'default.created.message' , args : [message (code : 'product.label' , default : 'Product' ), product.id ])
redirect product
}
'*' { respond product, [status : CREATED ] }
}
}
@Secured ('ROLE_ADMIN' )
def edit (Long id ) {
respond productService.get (id)
}
@Secured ('ROLE_ADMIN' )
def update (Product product ) {
if (product == null ) {
notFound ()
return
}
try {
productService.save (product)
} catch (ValidationException e) {
respond product.errors , view :'edit'
return
}
request.withFormat {
form multipartForm {
flash.message = message (code : 'default.updated.message' , args : [message (code : 'product.label' , default : 'Product' ), product.id ])
redirect product
}
'*' { respond product, [status : OK ] }
}
}
@Secured ('ROLE_ADMIN' )
def delete (Long id ) {
if (id == null ) {
notFound ()
return
}
productService.delete (id)
request.withFormat {
form multipartForm {
flash.message = message (code : 'default.deleted.message' , args : [message (code : 'product.label' , default : 'Product' ), id])
redirect action :"index" , method :"GET"
}
'*' { render status : NO_CONTENT }
}
}
protected void notFound ( ) {
request.withFormat {
form multipartForm {
flash.message = message (code : 'default.not.found.message' , args : [message (code : 'product.label' , default : 'Product' ), params.id ])
redirect action : "index" , method : "GET"
}
'*' { render status : NOT_FOUND }
}
}
}
设置登录成功后的默认页面 grails-app/conf/application.groovy
grails.plugin.springsecurity.successHandler.defaultTargetUrl = '/product'
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?