使用 Spring Boot 设置无状态身份验证
Posted
技术标签:
【中文标题】使用 Spring Boot 设置无状态身份验证【英文标题】:Setup Stateless Authentication using Spring Boot 【发布时间】:2016-02-20 11:12:45 【问题描述】:我正在使用最新版本的 Spring Boot,并且正在尝试设置 StatelessAuthenticaion。到目前为止,我一直在阅读的教程非常模糊,我不确定我做错了什么。我正在使用的教程是...
http://technicalrex.com/2015/02/20/stateless-authentication-with-spring-security-and-jwt/
我的设置的问题是,除了 TokenAuthenticationService::addAuthentication
从未被调用因此我的令牌从未设置,因此它在调用 TokenAuthenticationService::getAuthentication
时返回 null 并因此返回 401即使我成功登录(因为永远不会调用 addAuthentication 来设置标头中的令牌)。我试图想办法添加TokenAuthenticationService::addAuthentication
,但我觉得这很困难。
在教程中,他将类似于WebSecurityConfig::UserDetailsService.userService
的内容添加到auth.userDetailsService()
中。我遇到的唯一问题是,当我这样做时,它会引发 CastingErrorException。它仅在我使用 UserDetailsService customUserDetailsService
时才有效...
WebSecurityConfig
package app.config;
import app.repo.User.CustomUserDetailsService;
import app.security.RESTAuthenticationEntryPoint;
import app.security.RESTAuthenticationFailureHandler;
import app.security.RESTAuthenticationSuccessHandler;
import app.security.TokenAuthenticationService;
import app.security.filters.StatelessAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.sql.DataSource;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
private static PasswordEncoder encoder;
private final TokenAuthenticationService tokenAuthenticationService;
private final CustomUserDetailsService userService;
@Autowired
private UserDetailsService customUserDetailsService;
@Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
public WebSecurityConfig()
this.userService = new CustomUserDetailsService();
tokenAuthenticationService = new TokenAuthenticationService("tooManySecrets", userService);
@Autowired
public void configureAuth(AuthenticationManagerBuilder auth,DataSource dataSource) throws Exception
auth.jdbcAuthentication().dataSource(dataSource);
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
@Override
protected void configure(HttpSecurity http) throws Exception
http.authorizeRequests().antMatchers("/**").authenticated();
http.csrf().disable();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.formLogin().defaultSuccessUrl("/").successHandler(authenticationSuccessHandler);
http.formLogin().failureHandler(authenticationFailureHandler);
//This is ho
http.addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService),
UsernamePasswordAuthenticationFilter.class);
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(customUserDetailsService);
@Bean
@Override
public CustomUserDetailsService userDetailsService()
return userService;
@Bean
public TokenAuthenticationService tokenAuthenticationService()
return tokenAuthenticationService;
TokenAuthenticationService
成功调用了getAuthentication
方法,但在我阅读的教程中,没有正确解释如何调用addAuthentication
令牌认证服务
package app.security;
import app.repo.User.CustomUserDetailsService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TokenAuthenticationService
private static final String AUTH_HEADER_NAME = "X-AUTH-TOKEN";
private final TokenHandler tokenHandler;
//This is called in my WebSecurityConfig() constructor
public TokenAuthenticationService(String secret, CustomUserDetailsService userService)
tokenHandler = new TokenHandler(secret, userService);
public void addAuthentication(HttpServletResponse response, UserAuthentication authentication)
final UserDetails user = authentication.getDetails();
response.addHeader(AUTH_HEADER_NAME, tokenHandler.createTokenForUser(user));
public Authentication getAuthentication(HttpServletRequest request)
final String token = request.getHeader(AUTH_HEADER_NAME);
if (token != null)
final UserDetails user = tokenHandler.parseUserFromToken(token);
if (user != null)
return new UserAuthentication(user);
return null;
令牌处理程序
package app.security;
import app.repo.User.CustomUserDetailsService;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
public final class TokenHandler
private final String secret;
private final CustomUserDetailsService userService;
public TokenHandler(String secret, CustomUserDetailsService userService)
this.secret = secret;
this.userService = userService;
public UserDetails parseUserFromToken(String token)
String username = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
return userService.loadUserByUsername(username);
public String createTokenForUser(UserDetails user)
return Jwts.builder()
.setSubject(user.getUsername())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
在我的 WebServiceConfig 中。我添加以下内容
http.addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService),
UsernamePasswordAuthenticationFilter.class);
它调用以下类作为过滤器。它获得了身份验证,但没有实际添加它的位置。
StatelessAuthenticationFilter
package app.security.filters;
import app.security.TokenAuthenticationService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Created by anthonygordon on 11/17/15.
*/
public class StatelessAuthenticationFilter extends GenericFilterBean
private final TokenAuthenticationService authenticationService;
public StatelessAuthenticationFilter(TokenAuthenticationService authenticationService)
this.authenticationService = authenticationService;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException
HttpServletRequest httpRequest = (HttpServletRequest) request;
Authentication authentication = authenticationService.getAuthentication(httpRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
SecurityContextHolder.getContext().setAuthentication(null);
以下类是在TokenAuthenticationService::addAuthentication
中传递的内容
用户认证
package app.security;
import app.repo.User.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class UserAuthentication implements Authentication
private final UserDetails user;
private boolean authenticated = true;
public UserAuthentication(UserDetails user)
this.user = user;
@Override
public String getName()
return user.getUsername();
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
return user.getAuthorities();
@Override
public Object getCredentials()
return user.getPassword();
@Override
public UserDetails getDetails()
return user;
@Override
public Object getPrincipal()
return user.getUsername();
@Override
public boolean isAuthenticated()
return authenticated;
@Override
public void setAuthenticated(boolean authenticated)
this.authenticated = authenticated;
就是这样……
我的解决方案(但需要帮助)...
我的解决方案是在我的成功处理程序中设置TokenAuthenticationService::addAuthentication
方法...唯一的问题是教程将TokenAuthenticationService
类添加到WebServiceConfig 类中。那是它唯一可以访问的地方。如果有办法在我的 successHandler 中获取它,我也许可以设置令牌。
package app.security;
import app.controllers.Requests.TriviaResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by anthonygordon on 11/12/15.
*/
@Component
public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
TriviaResponse tresponse = new TriviaResponse();
tresponse.setMessage("You have successfully logged in");
String json = ow.writeValueAsString(tresponse);
response.getWriter().write(json);
clearAuthenticationAttributes(request);
【问题讨论】:
【参考方案1】:当用户首次提供登录凭据时,您必须自己致电 TokenAuthenticationService.addAuthentication()
。
在用户使用其 Google 帐户成功登录后,本教程会在 GoogleAuthorizationResponseServlet
中调用 addAuthentication()
。以下是相关代码:
private String establishUserAndLogin(HttpServletResponse response, String email)
// Find user, create if necessary
User user;
try
user = userService.loadUserByUsername(email);
catch (UsernameNotFoundException e)
user = new User(email, UUID.randomUUID().toString(), Sets.<GrantedAuthority>newHashSet());
userService.addUser(user);
// Login that user
UserAuthentication authentication = new UserAuthentication(user);
return tokenAuthenticationService.addAuthentication(response, authentication);
如果您已经有一个身份验证成功处理程序,那么我认为您在正确的轨道上,您需要从那里调用TokenAuthenticationService.addAuthentication()
。将tokenAuthenticationService
bean 注入您的处理程序,然后开始使用它。如果您的成功处理程序最终不是 Spring bean,那么您可以通过调用 WebApplicationContextUtils.getRequiredWebApplicationContext.getBean(TokenAuthenticationService.class)
显式查看 tokenAuthenticationService
。
本教程的 GitHub 存储库中还有一个 issue,它将解决用户提供的初始登录与所有后续请求中发生的无状态身份验证之间的混淆。
【讨论】:
【参考方案2】:您可以像下面这样定义 StatelessLoginFilter
.addFilterBefore(
new StatelessLoginFilter("/api/signin",
tokenAuthenticationService, userDetailsService,
authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
然后像这样写类
class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter
private final TokenAuthenticationService tokenAuthenticationService;
private final UserDetailsService userDetailsService;
protected StatelessLoginFilter(String urlMapping,
TokenAuthenticationService tokenAuthenticationService,
UserDetailsService userDetailsService,
AuthenticationManager authManager)
super(new AntPathRequestMatcher(urlMapping));
this.userDetailsService = userDetailsService;
this.tokenAuthenticationService = tokenAuthenticationService;
setAuthenticationManager(authManager);
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException
final User authenticatedUser = userDetailsService
.loadUserByUsername(authentication.getName());
final UserAuthentication userAuthentication = new UserAuthentication(
authenticatedUser);
tokenAuthenticationService.addAuthentication(response,
userAuthentication);
SecurityContextHolder.getContext()
.setAuthentication(userAuthentication);
【讨论】:
以上是关于使用 Spring Boot 设置无状态身份验证的主要内容,如果未能解决你的问题,请参考以下文章
Spring Boot:Oauth2:访问被拒绝(用户是匿名的);重定向到身份验证入口点
spring-boot 在单个 Web 应用程序路径上设置基本身份验证?
如何在 Spring Boot 中为 Spring LDAP 身份验证设置覆盖 BindAuthenticator handleBindException