在Spring Boot应用程序中配置安全性

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在Spring Boot应用程序中配置安全性相关的知识,希望对你有一定的参考价值。

我正在将应用程序升级到Spring Boot 2.0.3

但我的登录请求是未经授权的:

curl -H "Accept:application/json" -H "Content-Type: application/json" "http://localhost:8080/api/users/login" -X POST -d "{ "email" : "myemail@somedomain.com", "password" : "xxxxx" }" -i

回应是401 Unauthorized access. You failed to authenticate

它由我的自定义入口点给出:

@Component
public final class RESTAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {

    private static Logger logger = LoggerFactory.getLogger(RESTAuthenticationEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
        logger.debug("Security - RESTAuthenticationEntryPoint - Entry point 401");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access. You failed to authenticate.");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        setRealmName("User REST");
        super.afterPropertiesSet();
    }

}

调试器显示我的authenticateCustomAuthenticationProvider方法未被调用,因为我预期它是:

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    CredentialsService credentialsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String email = authentication.getName();
        String password = authentication.getCredentials().toString();
        List<SimpleGrantedAuthority> grantedAuthorities = new ArrayList<SimpleGrantedAuthority>();
        User user = null;
        try {
            user = credentialsService.findByEmail(new EmailAddress(email));
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException("The login " + email + " and password could not match.");             
        }
        if (user != null) {
            if (credentialsService.checkPassword(user, password)) {
                grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
                return new UsernamePasswordAuthenticationToken(email, password, grantedAuthorities);
            } else {
                throw new BadCredentialsException("The login " + user.getEmail() + " and password could not match.");               
            }
        }
        throw new BadCredentialsException("The login " + authentication.getPrincipal() + " and password could not match.");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

但是运行了过滤器并找到了一个空令牌:

@Component
public class AuthenticationFromTokenFilter extends OncePerRequestFilter {

    @Autowired
    private TokenAuthenticationService tokenAuthenticationService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        tokenAuthenticationService.authenticateFromToken(request);
        chain.doFilter(request, response);
    }

}

@Service
public class TokenAuthenticationServiceImpl implements TokenAuthenticationService {

    private static Logger logger = LoggerFactory.getLogger(TokenAuthenticationServiceImpl.class);

    private static final long ONE_WEEK = 1000 * 60 * 60 * 24 * 7;
    private static final String TOKEN_URL_PARAM_NAME = "token";

    @Autowired
    private ApplicationProperties applicationProperties;

    @Autowired
    private UserDetailsService userDetailsService;

    public void addTokenToResponseHeader(HttpHeaders headers, String username) {
        String token = buildToken(username);
        headers.add(CommonConstants.AUTH_HEADER_NAME, token);
    }

    public void addTokenToResponseHeader(HttpServletResponse response, Authentication authentication) {
        String username = authentication.getName();
        if (username != null) {
            String token = buildToken(username);
            response.addHeader(CommonConstants.AUTH_HEADER_NAME, token);
        }
    }

    private String buildToken(String username) {
        String token = null;
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails != null) {
            Date expirationDate = new Date(System.currentTimeMillis() + ONE_WEEK);
            token = CommonConstants.AUTH_BEARER + " " + Jwts.builder().signWith(HS256, getEncodedPrivateKey()).setExpiration(expirationDate).setSubject(userDetails.getUsername()).compact();       
        }
        return token;
    }

    public Authentication authenticateFromToken(HttpServletRequest request) {
        String token = extractAuthTokenFromRequest(request);
        logger.debug("The request contained the JWT token: " + token);
        if (token != null && !token.isEmpty()) {
            try {
                String username = Jwts.parser().setSigningKey(getEncodedPrivateKey()).parseClaimsJws(token).getBody().getSubject();
                if (username != null) {
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    logger.debug("Security - The filter authenticated fine from the JWT token");
                }
            } catch (SignatureException e) {
                logger.info("The JWT token " + token + " could not be parsed.");
            }
        }
        return null;
    }

    private String extractAuthTokenFromRequest(HttpServletRequest request) {
        String token = null;
        String header = request.getHeader(CommonConstants.AUTH_HEADER_NAME);
        if (header != null && header.contains(CommonConstants.AUTH_BEARER)) {
            int start = (CommonConstants.AUTH_BEARER + " ").length();
            if (header.length() > start) {
                token = header.substring(start - 1);
            }
        } else {
            // The token may be set as an HTTP parameter in case the client could not set it as an HTTP header
            token = request.getParameter(TOKEN_URL_PARAM_NAME);
        }
        return token;
    }

    private String getEncodedPrivateKey() {
        String privateKey = applicationProperties.getAuthenticationTokenPrivateKey();
        return Base64.getEncoder().encodeToString(privateKey.getBytes());
    }

}

我的安全配置是:

@Configuration
@EnableWebSecurity
@ComponentScan(nameGenerator = PackageBeanNameGenerator.class, basePackages = { "com.thalasoft.user.rest.security", "com.thalasoft.user.rest.filter" })
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationFromTokenFilter authenticationFromTokenFilter;

    @Autowired
    private SimpleCORSFilter simpleCORSFilter;

    @Autowired
    private RESTAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.authenticationProvider(new CustomAuthenticationProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .exceptionHandling()
            .authenticationEntryPoint(restAuthenticationEntryPoint)
        .and()
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .addFilterBefore(simpleCORSFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class)
            .headers().cacheControl().disable().frameOptions().disable()
        .and()
            .userDetailsService(userDetailsService)
            .authorizeRequests()
            .antMatchers(RESTConstants.SLASH + UserDomainConstants.USERS + RESTConstants.SLASH + UserDomainConstants.LOGIN).permitAll()
            .antMatchers(RESTConstants.SLASH + RESTConstants.ERROR).permitAll()
            .antMatchers("/**").hasRole(UserDomainConstants.ROLE_ADMIN).anyRequest().authenticated();
    }

}

用户详细信息服务是:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private CredentialsService credentialsService;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (username != null && !username.isEmpty()) {
            User user = credentialsService.findByEmail(new EmailAddress(username));
            if (user != null) {
                return new UserDetailsWrapper(user);
            }
        }
        throw new UsernameNotFoundException("The user " + username + " was not found.");
    }

}

为什么自定义身份验证提供程序不验证用户名和密码?

更新:我在这个guide读了一些有趣和令人费解的东西

Note that the AuthenticationManagerBuilder is @Autowired into a method in a @Bean - that is what makes it build the global (parent) AuthenticationManager. In contrast if we had done it this way (using an @Override of a method in the configurer) then the AuthenticationManagerBuilder is only used to build a "local" AuthenticationManager, which is a child of the global one. In a Spring Boot application you can @Autowired the global one into another bean, but you can’t do that with the local one unless you explicitly expose it yourself.

那么,我使用configure方法设置authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider);有什么问题吗?我尝试了以下配置,而不是上面的配置:

@Autowired
public void initialize(AuthenticationManagerBuilder authenticationManagerBuilder) {
    authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider);
}

但它仍然没有根据请求运行自定义身份验证提供程序。

我也试过过滤器,如下所示:

http.addFilterAfter(authenticationFromTokenFilter, UsernamePasswordAuthenticationFilter.class);

而不是addFilterBefore但它没有改变任何问题。

答案

根据我的说法,当我们通过实现GenericFilterBean实现我们自己的ApplicationFilter时,我们需要检查从请求接收的令牌是否有效。如果它无效,那么我们需要将令牌转储到安全上下文中(以便身份验证提供程序获取)。我没有通过你的过滤器类。但这对我有用:

@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httprequset=(HttpServletRequest)request;
        String uname=request.getParameter("username");
        String pwd=request.getParameter("password");
        String role=request.getParameter("role");
        List<GrantedAuthority> l = new ArrayList<>();
        l.add( new SimpleGrantedAuthority(role.toUpperCase()) );

        UsernamePasswordAuthenticationToken token=new UsernamePasswordAuthenticationToken(uname,pwd,l);
        token.setAuthenticated(false);

        SecurityContextHolder.getContext().setAuthentication(token);

        chain.doFilter(httprequset, response);
    }
另一答案

在WebSecurityConfiguration里面配置(HttpSecurity http)方法:

http.authorizeRequests().antMatchers("/api/users/login").permitAll(); 
http.authorizeRequests().anyRequest().authenticated();

添加相同的顺序。

说明:应允许登录和注销请求,无需任何身份验证

有效的示例配置方法是:

http.formLogin().disable().logout().disable().httpBasic().disable();

        http.authorizeRequests().antMatchers("/logout", "/login", "/").permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        http.addFilterBefore(new SomeFilter(), SecurityContextHolderAwareRequestFilter.class);
        http.addFilterBefore(new CORSFilter(env), ChannelProcessingFilter.class);
         http.addFilterBefore(new XSSFilter(),CORSFilter.class);

以上是关于在Spring Boot应用程序中配置安全性的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot 应用程序中跨平台的 Spring 安全性

Spring Boot:禁用状态异常代码的安全性

Spring Boot安全设计的配置

10 种保护 Spring Boot 应用的绝佳方法

这些保护Spring Boot 应用的方法,你都用了吗?

Spring Boot 配置问题中的 JWT 令牌