Grails 注册并自动登录后如何重定向到之前页面
结论
可以使用 Spring Security 的 RequestCache 获取登录前保存的 SavedRequest。用户注册成功并自动完成认证后,再重定向到 SavedRequest.redirectUrl。
不建议优先读取 Referer 请求头。这个请求头可能不存在,也可能指向登录页或注册页,无法可靠地还原用户最初想访问的支付页面。
原始 URL 从哪里来
未登录用户访问受保护资源时,Spring Security 通常会按以下流程处理:
- 将当前请求保存到 HTTP Session。
- 跳转到登录页面。
- 用户登录后,由认证成功处理器读取保存的请求。
- 重定向到原始地址。
注册流程只是在第 2 步和第 3 步之间多了一个页面。只要注册和自动登录期间一直使用同一个 Session,通常就能通过 RequestCache 取回原始请求。
注册成功后如何处理
在注册控制器中,先读取保存的请求,再自动登录,最后执行重定向:
import org.springframework.security.web.savedrequest.HttpSessionRequestCache
import org.springframework.security.web.savedrequest.RequestCache
import org.springframework.security.web.savedrequest.SavedRequest
class RegisterController {
def springSecurityService
private final RequestCache requestCache = new HttpSessionRequestCache()
def register() {
// 显示或处理注册页面
}
def save() {
// 1. 校验参数并创建用户
def user = createUser(params)
if (!user) {
flash.error = '注册失败'
render view: 'register'
return
}
// 2. 在自动登录或成功处理器消费缓存之前读取原始请求
SavedRequest savedRequest =
requestCache.getRequest(request, response)
String targetUrl = savedRequest?.redirectUrl
// 3. 自动登录
springSecurityService.reauthenticate(user.username)
// 4. 清除已使用的请求,避免以后再次跳转
requestCache.removeRequest(request, response)
// 5. 返回登录前页面
if (targetUrl) {
redirect url: targetUrl
} else {
redirect controller: 'home', action: 'index'
}
}
private def createUser(Map registrationParams) {
// 根据项目中的 User、Role 模型创建用户并分配权限
// 成功时返回用户对象,失败时返回 null
}
}
springSecurityService.reauthenticate(...) 需要哪些参数,取决于项目使用的 Spring Security Core 插件版本和相关配置。某些版本或自定义认证逻辑需要传入密码,也可能需要直接操作 AuthenticationManager。无论采用哪种自动登录方式,都应在认证完成后使用同一 Session 中的 SavedRequest。
注册步骤较多时显式保存目标地址
如果注册过程需要在多个控制器之间跳转、会重新创建 Session,或者自定义认证成功处理器可能提前删除 SavedRequest,可以在进入注册页时取出目标地址并保存到 Session:
import org.springframework.security.web.savedrequest.HttpSessionRequestCache
class RegisterController {
private final requestCache = new HttpSessionRequestCache()
def register() {
def savedRequest = requestCache.getRequest(request, response)
if (savedRequest) {
session.registrationTargetUrl = savedRequest.redirectUrl
}
render view: 'register'
}
}
注册成功后读取该地址,并立即将它从 Session 中删除:
def targetUrl = session.registrationTargetUrl
session.removeAttribute('registrationTargetUrl')
springSecurityService.reauthenticate(user.username)
if (targetUrl) {
redirect url: targetUrl
} else {
redirect controller: 'home', action: 'index'
}
这种做法适合步骤较多的注册流程,但需要防范开放重定向漏洞。目标地址应来自 Spring Security 在服务器端保存的请求,不能直接接受未经校验的任意 returnUrl。
如果确实需要从请求参数中接收地址,应将其限制为站内相对路径,例如:
private String safeTargetUrl(String value) {
if (value?.startsWith('/') && !value.startsWith('//')) {
return value
}
return null
}
不要直接依赖 Session 属性名
Spring Security 通常会把保存的请求放在 Session 中,常见的底层属性名是:
SPRING_SECURITY_SAVED_REQUEST
虽然可以直接读取这个属性:
def savedRequest = session['SPRING_SECURITY_SAVED_REQUEST']
def targetUrl = savedRequest?.redirectUrl
但这种写法依赖内部实现。通过 RequestCache API 读取和清理请求更稳妥,也能避免插件升级、自定义请求缓存或安全配置变化后代码失效。
注意事项
- 注册、自动登录和重定向需要使用同一个 HTTP Session。如果注册过程中调用了
session.invalidate(),保存的请求就会丢失。 - 启用 Session 固定攻击防护后,认证过程可能更换 Session ID。正常的 Session migration 通常会保留属性,但自定义配置的行为可能不同,因此最好在自动登录前读取
SavedRequest。 SavedRequest.redirectUrl可能是完整 URL。只应信任 Spring Security 捕获的原始站内请求,不要直接使用用户传入的外部地址。- 如果原始请求是
POST,普通redirect会发起新的GET请求,无法恢复原始请求体。支付流程应将进入支付页和真正提交支付分开,避免通过重定向重复执行支付操作。 - 如果没有保存的请求,应跳转到固定的默认页面,例如个人中心、订单确认页或首页。
备注:内容仅供参考。
