使用 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

使用 RESTful 登录 API 验证我的 Spring Boot 应用程序

jhipster spring boot 自定义身份验证提供程序