通过 Spring 进行 RESTful 身份验证
Posted
技术标签:
【中文标题】通过 Spring 进行 RESTful 身份验证【英文标题】:RESTful Authentication via Spring 【发布时间】:2012-06-05 06:41:42 【问题描述】:问题: 我们有一个基于 Spring MVC 的 RESTful API,其中包含敏感信息。 API 应该是安全的,但是在每个请求中都发送用户的凭据(用户/密码组合)是不可取的。根据 REST 准则(和内部业务要求),服务器必须保持无状态。该 API 将由另一台服务器以 mashup 样式的方式使用。
要求:
客户端使用凭据向.../authenticate
(不受保护的 URL)发出请求;服务器返回一个安全令牌,其中包含足够的信息供服务器验证未来的请求并保持无状态。这可能包含与 Spring Security 的Remember-Me Token 相同的信息。
客户端向各种(受保护的)URL 发出后续请求,将先前获得的令牌附加为查询参数(或者,不太理想的是,附加一个 HTTP 请求标头)。
不能期望客户端存储 cookie。
既然我们已经使用了 Spring,那么解决方案应该使用 Spring Security。
我们一直在努力解决这个问题,希望有人已经解决了这个问题。
鉴于上述情况,您将如何解决这一特殊需求?
【问题讨论】:
嗨,克里斯,我不确定在查询参数中传递该标记是否是最好的主意。这将显示在日志中,无论是 HTTPS 还是 HTTP。标头可能更安全。仅供参考。很好的问题。 +1 你对无状态的理解是什么?您的令牌要求与我对无状态的理解相冲突。 Http 身份验证答案在我看来是唯一的无状态实现。 @MarkusMalkusch stateless 是指服务器了解与给定客户端的先前通信。 HTTP 根据定义是无状态的,会话 cookie 使其有状态。令牌的生命周期(和来源,就此而言)是无关紧要的;服务器只关心它是否有效并且可以绑定回用户(不是会话)。因此,传递识别令牌不会干扰状态性。 @ChrisCashwell 你如何确保令牌没有被客户端欺骗/生成?您是否在服务器端使用私钥加密令牌,将其提供给客户端,然后在以后的请求中使用相同的密钥对其进行解密?显然 Base64 或其他一些混淆是不够的。您能否详细说明这些令牌的“验证”技术? 虽然这已经过时了,而且我已经超过 2 年没有接触或更新代码,但我已经创建了一个 Gist 来进一步扩展这些概念。 gist.github.com/ccashwell/dfc05dd8bd1a75d189d1 【参考方案1】:我们设法使这项工作完全按照 OP 中的描述进行,希望其他人可以使用该解决方案。这是我们所做的:
像这样设置安全上下文:
<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
<security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
<security:intercept-url pattern="/authenticate" access="permitAll"/>
<security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>
<bean id="CustomAuthenticationEntryPoint"
class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />
<bean id="authenticationTokenProcessingFilter"
class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
<constructor-arg ref="authenticationManager" />
</bean>
如您所见,我们创建了一个自定义AuthenticationEntryPoint
,如果请求未在过滤器链中通过我们的AuthenticationTokenProcessingFilter
进行身份验证,它基本上只会返回一个401 Unauthorized
。
CustomAuthenticationEntryPoint:
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException
response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
AuthenticationTokenProcessingFilter:
public class AuthenticationTokenProcessingFilter extends GenericFilterBean
@Autowired UserService userService;
@Autowired TokenUtils tokenUtils;
AuthenticationManager authManager;
public AuthenticationTokenProcessingFilter(AuthenticationManager authManager)
this.authManager = authManager;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException
@SuppressWarnings("unchecked")
Map<String, String[]> parms = request.getParameterMap();
if(parms.containsKey("token"))
String token = parms.get("token")[0]; // grab the first "token" parameter
// validate the token
if (tokenUtils.validate(token))
// determine the user based on the (already validated) token
UserDetails userDetails = tokenUtils.getUserFromToken(token);
// build an Authentication object with the user's info
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
// set the authentication into the SecurityContext
SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));
// continue thru the filter chain
chain.doFilter(request, response);
显然,TokenUtils
包含一些私密(并且非常特定于案例)的代码,不能轻易共享。这是它的界面:
public interface TokenUtils
String getToken(UserDetails userDetails);
String getToken(UserDetails userDetails, Long expiration);
boolean validate(String token);
UserDetails getUserFromToken(String token);
这应该会让你有个好的开始。
【讨论】:
令牌随请求一起发送时是否需要对令牌进行身份验证。直接获取用户名信息并在当前上下文/请求中设置如何? @Spring 我不会将它们存储在任何地方......令牌的整个想法是它需要与每个请求一起传递,并且可以(部分)解构以确定其有效性(因此validate(...)
方法)。这很重要,因为我希望服务器保持无状态。我想你可以使用这种方法而不需要使用 Spring。
如果客户端是浏览器,token怎么存储?还是您必须为每个请求重做身份验证?
很棒的提示。 @ChrisCashwell - 我找不到的部分是您在哪里验证用户凭据并发回令牌?我猜它应该在 /authenticate 端点的 impl 中的某个地方。我对吗 ?如果不是 /authenticate 的目标是什么?
AuthenticationManager 里面有什么?【参考方案2】:
为什么不开始使用带有 JSON WebTokens 的 OAuth
http://projects.spring.io/spring-security-oauth/
OAuth2 是一种标准化的授权协议/框架。根据官方 OAuth2 Specification:
您可以找到更多信息here
【讨论】:
Spring Security OAuth 项目已弃用。 Spring Security 提供了最新的 OAuth 2.0 支持。有关详细信息,请参阅 OAuth 2.0 迁移指南。【参考方案3】:关于携带信息的令牌,JSON Web 令牌 (http://jwt.io) 是一项出色的技术。主要概念是将信息元素(声明)嵌入到令牌中,然后对整个令牌进行签名,以便验证端可以验证声明确实可信。
我使用这个 Java 实现:https://bitbucket.org/b_c/jose4j/wiki/Home
还有一个 Spring 模块(spring-security-jwt),但我没有研究它支持什么。
【讨论】:
【参考方案4】:您可以考虑Digest Access Authentication。基本上协议如下:
-
请求来自客户端
服务器以唯一的 nonce 字符串响应
客户端提供了一个用户名和密码(以及一些其他值)md5 与随机数散列;这个哈希被称为 HA1
服务器随后能够验证客户的身份并提供所请求的材料
与 nonce 的通信可以继续,直到服务器提供新的 nonce(计数器用于消除重放攻击)
所有这些通信都是通过标头进行的,正如 jmort253 指出的那样,这通常比在 url 参数中传达敏感材料更安全。
Spring Security 支持摘要式访问身份验证。请注意,尽管文档说您必须有权访问客户的纯文本密码,但您可以为您的客户successfully authenticate if you have the HA1 hash。
【讨论】:
虽然这是一种可能的方法,但为了检索令牌必须进行多次往返,这使得它有点不可取。 如果您的客户端遵循 HTTP 身份验证规范,则这些往返仅在第一次调用和 5. 发生时发生。以上是关于通过 Spring 进行 RESTful 身份验证的主要内容,如果未能解决你的问题,请参考以下文章
使用 Spring Security 和 Redis 对带有 Java 配置的 RESTFul api 进行基于 Cookie 的身份验证
通过 restful api 进行 Azure AD 身份验证