如何使用Spring Security提供多种身份validation方式
我从各种资源中了解了Spring Security,我知道filter和身份validation管理器是如何分开工作的,但我不确定请求与它们一起工作的确切顺序。 如果我没有错,简而言之,请求首先通过filter,filter调用各自的身份validation管理器。
我想允许两种身份validation – 一种使用JWT令牌,另一种使用用户名和密码。 以下是security.xml的摘录
security.xml文件
MyAuthenticationProvider.java
public class MyAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // Code } @Override public boolean supports(Class authentication) { return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); } }
TokenAuthenticationFilter.java
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter{ protected TokenAuthenticationFilter(String defaultFilterProcessesUrl) { super(defaultFilterProcessesUrl); //defaultFilterProcessesUrl - specified in applicationContext.xml. super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl)); //Authentication will only be initiated for the request url matching this pattern setAuthenticationManager(new NoOpAuthenticationManager()); setAuthenticationSuccessHandler(new TokenSimpleUrlAuthenticationSuccessHandler()); setAuthenticationFailureHandler(new MyAuthenticationFailureHandler()); } /** * Attempt to authenticate request */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String tid = request.getHeader("authorization"); logger.info("token found:"+tid); AbstractAuthenticationToken userAuthenticationToken = authUserByToken(tid,request); if(userAuthenticationToken == null) throw new AuthenticationServiceException("Invalid Token"); return userAuthenticationToken; } /** * authenticate the user based on token * @return */ private AbstractAuthenticationToken authUserByToken(String token,HttpServletRequest request) throws JsonProcessingException { if(token==null) return null; AbstractAuthenticationToken authToken =null; boolean isValidToken = validate(token); if(isValidToken){ List authorities = new ArrayList(); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); authToken = new UsernamePasswordAuthenticationToken("", token, authorities); } else{ BaseError error = new BaseError(401, "UNAUNTHORIZED"); throw new AuthenticationServiceException(error.getStatusMessage()); } return authToken; } private boolean validate(String token) { if(token.startsWith("TOKEN ")) return true; return false; } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { super.doFilter(req, res, chain); } }
通过myAuthenticationProvider
我想要基于用户名 – 密码的身份validation,并通过自定义filter,我想检查JWT令牌。 如果我朝着正确的方向前进,有人可以告诉我吗?
解决方案概述
从广义上讲,要求多个AuthenticationProvider
分为两类:
- 使用不同的身份validation模式validation对不同类型URL的请求,例如:
- 使用基于表单的用户名 – 密码validation来validation
/web/**
所有请求; - 使用基于令牌的身份validation对
/api/**
所有请求进行身份validation。
- 使用基于表单的用户名 – 密码validation来validation
- 使用多种支持的身份validation模式之一validation所有请求。
每个解决方案略有不同,但它们基于共同的基础。
Spring Security对基于表单的用户名 – 密码身份validation提供了开箱即用的支持,因此无论上述两个类别如何,都可以非常轻松地实现。
但是,基于令牌的身份validation不支持开箱即用,因此需要自定义代码来添加必要的支持。 添加此支持需要以下组件:
- 扩展
AbstractAuthenticationToken
的POJO,它将保存用于身份validation的令牌。 - 扩展
AbstractAuthenticationProcessingFilter
的filter,它将从请求中提取标记值并填充上面步骤1中创建的POJO。 -
AuthenticationProvider
实现,它将使用令牌对请求进行身份validation。 - 上述选项1或2的Spring Security配置,具体取决于要求。
AbstractAuthenticationToken
POJO需要保存应该用于validation请求的JWT令牌,因此,最简单的AbstractAuthenticationToken
实现可能如下所示:
public JWTAuthenticationToken extends AbstractAuthenticationToken { private final String token; JWTAuthenticationToken(final String token, final Object details) { super(new ArrayList<>()); this.token = token; setAuthenticated(false); setDetails(details); } @Override public Object getCredentials() { return null; } @Override public String getPrincipal() { return token; } }
AbstractAuthenticationProcessingFilter
需要filter从请求中提取令牌。
public class JWTTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public JWTTokenAuthenticationFilter (String defaultFilterProcessesUrl) { super(defaultFilterProcessesUrl); } @Override public Authentication attemptAuthentication(final HttpServletRequest request , final HttpServletResponse response) throws AuthenticationException { final JWTAuthenticationToken token = new JWTAuthenticationToken(/* Get token from request */ , authenticationDetailsSource.buildDetails(request)); return getAuthenticationManager().authenticate(token); } }
请注意,filter不会尝试执行身份validation; 相反,它将实际身份validation委派给AuthenticationManager
,以确保正确执行任何前后身份validation步骤。
AuthenticationProvider
AuthenticationProvider
是负责执行AuthenticationProvider
的实际组件。 如果配置正确,它将由AuthenticationManager
自动调用。 一个简单的实现看起来像:
public class JWTAuthenticationProvider implements AuthenticationProvider { @Override public boolean supports(final Class> authentication) { return (JWTAuthenticationToken.class.isAssignableFrom(authentication)); } @Override public Authentication authenticate(final Authentication authentication) throws AuthenticationException { final JWTAuthenticationToken token = (JWTAuthenticationToken) authentication; ... } }
针对不同URL的不同身份validation模式的Spring Security配置
为每个URL系列使用不同的http
元素,例如:
... ...
由于不同的URL系列需要不同的身份validation模式,我们需要两个不同的AuthenticationManager
和两个不同的http
配置,每个URL系列一个。 对于每个,我们选择支持哪种身份validation模式。
针对相同URL的多种身份validation模式的Spring Security配置
使用单个http
元素,如下所示:
...
请注意以下事项:
- 不需要为
http
元素显式指定AuthenticationManager
,因为配置中只有一个,其标识符是authenticationManager
,这是默认值。 - 在表单登录filter之后插入令牌filter,而不是替换它。 这可确保表单登录和令牌登录都能正常工作。
-
AuthenticationManager
配置为使用多个AuthenticationProvider
。 这可确保尝试多种身份validation机制,直到找到支持请求的机制。
我这样做的方式是使用2个安全配置器。 我有一个Java配置示例,但如果您了解它,可以将其移植到xml。 请注意,这只是其中一种方式,而不是唯一的方法。
@Configuration @Order(1) public static class LoginSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("user").password("user").roles("USER"); auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN"); } protected void configure(HttpSecurity http) throws Exception { http .antMatcher("/api/login/**") .authorizeRequests() .antMatchers("/api/login/**").authenticated() .and() .httpBasic(); } } @Configuration @Order(2) public static class JWTSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("user1").password("user").roles("USER"); auth.inMemoryAuthentication().withUser("admin1").password("admin").roles("ADMIN"); } @Override protected void configure(HttpSecurity http) throws Exception { http .antMatcher("/api/**") .authorizeRequests() .antMatchers("/api/**").authenticated() .and() .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } }
说明:
在LoginSecurityConfigurerAdapter
,我只拦截api/login
urls。 因此,第一次登录请求将在此处捕获,并且在成功validation后您可以发出JWT。 现在在JWTSecurityConfigurerAdapter
,我正在捕获所有其他请求。 使用tokenauthenticationfilter它将validationJWT,并且只有在有效JWT的情况下,它才会允许访问API。