大家好我有一个新手grails / spring安全问题。我们使用spring-security-oauth2-provider在grails 3项目中设置oauth2,似乎可以保护我们的REST API。
但随后我们开始使用GSP在同一个项目中添加Web前端,并走到了一个十字路口。通常oauth2通过对端点进行身份验证并接收令牌来工作,使用该令牌可以在后续请求中的HTTP头中使用该令牌以继续访问受保护资源。但我的Web前端有一个登录页面。所以最初我们认为将Web前端视为客户端之一(我们的iOS应用程序有1个客户端,Android应用程序有1个客户端,所以为什么我们的Web应用程序也没有1个)。但是从我们的控制器代码(用于登录)向我们的oauth2提供者端点发出HTTP请求似乎很奇怪,因为它在同一个项目中;我的Web前端需要进行的大多数后续请求,我们希望直接访问底层服务和域对象,因此添加额外的跃点似乎适得其反。
所以我们选择的是,当我使用login.gsp登录时,在控制器代码中,我绕过oauth2登录,只使用authenticationManager.authenticate()和我构造的UsernamePasswordAuthenticationToken进行直接弹簧安全认证从传递到我的表单的用户名和密码字段,然后在响应上调用SecurityContextHolder.getContext()。setAuthentication()。这种方法解决了我的问题,因为this帖子告诉我这样做只会在当前线程上设置SecurityContextHolder,并且由于SecurityContextHolder在过滤器链的末尾被清除,后续请求将不会认证。所以我所做的就是如果这个身份验证通过(即没有抛出异常),那么我将我的用户对象放在HTTP会话中以及所有后续请求中,尝试将其检索为“告知”我已经过身份验证的方式。但这看起来既黑又脏;它导致我的用户对象没有附加到数据库会话(导致延迟初始化异常)。
我确实从this这样的帖子中找到了其他类似的建议,这些帖子将整个SecurityContext放在HTTP会话中,但似乎没有说明如何在后续请求中使用该SecurityContext。
我想我的最终问题是,我们是否采取了错误的方式?是否有更好,更清洁的方式来完成我想做的事情?我想我们不能成为第一个尝试这样做的人。
答案 0 :(得分:0)
我有点难过,我回答了自己的问题。但我想知道我的解决方案是否是解决此问题的正确方法。
所以我们最终做的是使用过滤器。我们知道拦截器应该是Grails 3的方法,但在到达那里之前我们只想要一个带过滤器的工作版本。
在我们的AuthController.signIn方法中,我们通过UsernamePasswordAuthenticationToken身份验证过程并实际将SecurityContext推送到HTTP会话中。顺便说一句,有趣的是,我后来发现这实际上已经为我做了(使用" SPRING_SECURITY_CONTEXT"键),不需要我显式地执行session.setAttribute。我的理由是,由于SecurityContext仅使用UsernamePasswordAuthenticationToken对此请求进行了身份验证,因此我需要将其保留在HTTP会话中,以便我可以为后续请求重新加载它。所以在我作为第一个顺序放置的过滤器中,我检查HTTP会话中的SecurityContext并复制其"身份验证"进入当前的SecurityContext。请注意,如果在HTTP会话中找不到任何内容,我们还有一个顺序放置在最后的过滤器,用于将用户重定向到登录页面。
通过此更改,我们现在可以访问Controller中的springSecurityService来访问当前登录的用户(在我们无法访问之前)。我们得到的其他细节包括能够在我们的GSP页面中使用sec:ifAllGranted。但事情仍然不可行 - 我们仍然无法使用@Secured("#oauth2.isUser()")注释来保护我们的Controller方法,总是点击401.也许我们使用UsernamePasswordAuthenticationToken进行身份验证仍然不是"完成"我们需要身份验证吗?
我通过将OAuth2访问令牌而不是SecurityContext推送到HTTP会话中进一步尝试,并在我的过滤器中添加"授权" HTTP标头,其值为" Bearer" + accessToken。这不起作用,初步记录表明,在一个HTTP请求(页面视图)中,我多次进入我的过滤器,并且只有第一次成功获取OAuth2访问令牌才能进入请求#39 ; s HTTP标头。
所以在一天结束时,我想知道的是,我的方法是否正确,使用过滤器来保持用户在会话中连接?或者我应该采用不同的方式做到这一点?另外,如何使用@Secured注释保护我的控制器?
答案 1 :(得分:0)
我想知道如果我已经给出答案,SO会如何对待这个答案。唉,谁真的很在乎 - 我是那个一开始就问这个问题的人。
通过我查看的所有文档,例如Grails Security - Reference Documentation和Spring Security Core Plugin - Reference Documentation以及Grails Spring Security Core Plugin Custom Authentication和How to customize spring security plugin Login page in grails我能够发现即使我们使用的是Spring Security OAuth2插件,我们也获得了所有的设施提供Spring Security Core插件。这意味着开箱即用支持基于表单的登录,并且还支持自定义登录表单。
使用自定义登录表单,我们仍然可以使用" authenticate"而不是让login.gsp调用我们自己的AuthController的signIn方法。使用$ {request.contextPath} / login / authenticate的LoginController方法(随Spring Security插件一起提供)。
所以我实际上尝试了这一点,当然首先禁用我们的拦截器然后最终有一个导致浏览器保释的重定向循环。我认为我们需要做的一些配置(使用chainMap或过滤器)我们没有,这就是为什么它仍然无法正常工作。
有一件事是在grails-app / conf / logback.groovy中添加以下两行
logger 'org.springframework.security', DEBUG, ['STDOUT'], false
logger 'grails.plugin.springsecurity', DEBUG, ['STDOUT'], false
这给了我很多日志信息以帮助调试。出于某种原因,我意识到它在我的请求中以某种方式推送了一个匿名令牌,因为有些东西没有经过身份验证。
经过一段时间的调试后,我终于发现答案在于application.groovy。我遇到的问题是,因为默认情况下所有应用程序都有一个Spring Security filterChain,其最后一行/ **匹配JOINED_FILTERS。我们在安装Spring Security OAuth2 Provider时添加的行与/ **匹配JOINED_FILTERS减去一堆其他过滤器。该行实际上是正确的,但因为我们从未删除原始/ **行,优先顺序是在此之前。所以我需要做的就是删除与JOINED_FILTERS匹配的第一行/ **,现在登录工作正常。
最后,我们在filterChain中的行是:
[pattern: '/rest/**', filters: 'JOINED_FILTERS,-securityContextPersistenceFilter,-logoutFilter,-authenticationProcessingFilter,-rememberMeAuthenticationFilter,-oauth2BasicAuthenticationFilter,-exceptionTranslationFilter'],
// We want all the other resources to be Web-based
[pattern: '/web/**', filters: 'JOINED_FILTERS,-statelessSecurityContextPersistenceFilter,-oauth2ProviderFilter,-clientCredentialsTokenEndpointFilter,-oauth2BasicAuthenticationFilter,-oauth2ExceptionTranslationFilter'],
// This is just a catch-all that defaults to the same logic as before, it also would match things like /oauth/** or /auth/**
[pattern: '/**', filters: 'JOINED_FILTERS']
我们还有一些指向我们自己的AuthController的覆盖的其他配置:
grails.plugin.springsecurity.auth.loginFormUrl = '/auth/login' // This is our own Login Form
grails.plugin.springsecurity.failureHandler.defaultFailureUrl = '/auth/login' // Exception message shown in controller
grails.plugin.springsecurity.adh.errorPage = '/auth/denied' // Copied from LoginController