PAYATHON 2026

Grails 注册并自动登录后如何重定向到之前页面

支付小周

结论

可以使用 Spring Security 的 RequestCache 获取登录前保存的 SavedRequest。用户注册成功并自动完成认证后,再重定向到 SavedRequest.redirectUrl。

不建议优先读取 Referer 请求头。这个请求头可能不存在,也可能指向登录页或注册页,无法可靠地还原用户最初想访问的支付页面。

原始 URL 从哪里来

未登录用户访问受保护资源时,Spring Security 通常会按以下流程处理:

  1. 将当前请求保存到 HTTP Session。
  2. 跳转到登录页面。
  3. 用户登录后,由认证成功处理器读取保存的请求。
  4. 重定向到原始地址。

注册流程只是在第 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 请求,无法恢复原始请求体。支付流程应将进入支付页和真正提交支付分开,避免通过重定向重复执行支付操作。
  • 如果没有保存的请求,应跳转到固定的默认页面,例如个人中心、订单确认页或首页。

备注:内容仅供参考。