在 Angular 5 应用程序中响应 Spring Boot 时无法读取授权标头

Posted

技术标签:

【中文标题】在 Angular 5 应用程序中响应 Spring Boot 时无法读取授权标头【英文标题】:Not able to read authorization header in response of spring boot in angular 5 application 【发布时间】:2018-08-30 08:56:37 【问题描述】:

我正在使用 spring 框架和 Angular 5 构建应用程序。

我使用 spring boot、spring rest 和 spring security 准备好应用程序的后端。就本应用而言,我使用 Spring JWT 令牌进行授权。

我已经使用 postman 测试了后端服务,它们运行良好,没有任何故障。

现在,当我尝试将应用程序集成到前端(Angular 5)时,我一直面临 CORS 问题,并且我无法访问前端 Angular 应用程序发生的服务调用中的响应标头。

我已经在 WebSecurity 文件中将 CORS 设置为 Expose headers 作为响应。

我能够在响应中看到带有 Chrome 中的授权令牌的令牌,但仍然无法访问。

任何帮助将不胜感激。

谢谢!

任何人都可以建议在 Spring Boot 应用程序中设置 CORS 的正确方法。我当前的应用程序如下所示。

User.java

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;


@Entity
public class User 

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String username;

    private String password;

    public long getId() 
        return id;
    

    public String getUsername() 
        return username;
    

    public void setUsername(String username) 
        this.username = username;
    

    public String getPassword() 
        return password;
    

    public void setPassword(String password) 
        this.password = password;
    


UserController.java

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.app.mycompany.TasksApp.model.User;
import com.app.mycompany.TasksApp.repository.UserRepository;

@RestController
@RequestMapping("/users")
public class UserController 

    private UserRepository userRepository;

    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public UserController(UserRepository userRepository,
                          BCryptPasswordEncoder bCryptPasswordEncoder) 

        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    

    @PostMapping("/sign-up")
    public void signUp(@RequestBody User user) 
        user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
        userRepository.save(user);
    


UserRepository.java

import org.springframework.data.jpa.repository.JpaRepository;

import com.app.mycompany.TasksApp.model.User;

public interface UserRepository extends JpaRepository<User, Long> 
    User findByUsername(String username);

UserDetailsS​​erviceImpl.java

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import com.app.mycompany.TasksApp.model.User;
import com.app.mycompany.TasksApp.repository.UserRepository;
import org.springframework.stereotype.Service;

import static java.util.Collections.emptyList;


@Service
public class UserDetailsServiceImpl implements UserDetailsService 

    private UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) 
        this.userRepository = userRepository;
    

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        User user = userRepository.findByUsername(username);
        if (user == null) 
            throw new UsernameNotFoundException(username);
        
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), emptyList());
    


WebSecurity.java

import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;

import static com.app.mycompany.TasksApp.security.SecurityConstants.SIGN_UP_URL;

import java.util.Arrays;

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter 

    private UserDetailsService userDetailsService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    public WebSecurity(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) 
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
                .anyRequest().authenticated()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .and().cors().and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()));
    

    @Bean
    CorsConfigurationSource corsConfigurationSource() 
        CorsConfiguration configuration = new CorsConfiguration();
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "OPTIONS", "PUT", "DELETE"));
        configuration.setAllowCredentials(true);
        configuration.setExposedHeaders(Arrays.asList("Content-type","Authorization"));
        source.registerCorsConfiguration("/**", configuration);
        return source;
    

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
    


JWTAuthenticationFilter.java

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

import com.app.mycompany.TasksApp.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;

import static com.app.mycompany.TasksApp.security.SecurityConstants.EXPIRATION_TIME;
import static com.app.mycompany.TasksApp.security.SecurityConstants.HEADER_STRING;
import static com.app.mycompany.TasksApp.security.SecurityConstants.SECRET;
import static com.app.mycompany.TasksApp.security.SecurityConstants.TOKEN_PREFIX;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter 

    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) 
        this.authenticationManager = authenticationManager;
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException 
        try 
            User creds = new ObjectMapper()
                    .readValue(req.getInputStream(), User.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getUsername(),
                            creds.getPassword(),
                            new ArrayList<>())
            );
         catch (IOException e) 
            throw new RuntimeException(e);
        
    

    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException 
        String token = Jwts.builder()
                .setSubject(((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
    


JWTAuthorizationFilter.java

import java.io.IOException;
import java.util.ArrayList;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import io.jsonwebtoken.Jwts;

import static com.app.mycompany.TasksApp.security.SecurityConstants.EXPIRATION_TIME;
import static com.app.mycompany.TasksApp.security.SecurityConstants.HEADER_STRING;
import static com.app.mycompany.TasksApp.security.SecurityConstants.SECRET;
import static com.app.mycompany.TasksApp.security.SecurityConstants.TOKEN_PREFIX;

public class JWTAuthorizationFilter extends BasicAuthenticationFilter 

    public JWTAuthorizationFilter(AuthenticationManager authManager) 
        super(authManager);
    

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException 
        String header = req.getHeader(HEADER_STRING);
        if (header == null || !header.startsWith(TOKEN_PREFIX)) 
            chain.doFilter(req, res);
            return;
        
        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) 
        String token = request.getHeader(HEADER_STRING);
        if (token != null) 
            // parse the token.
            String user = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();
            if (user != null) 
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            
            return null;
        
        return null;
    


Angular UI 服务调用

我已将 api 更改为也使用 HttpClient 方法并观察响应。我仍然无法访问标题作为响应。

authenticate(credentials, callback) 

    const AuthorizationHeader = credentials ? 'Basic ' + btoa(credentials.username + ':' + credentials.password) : '';

    const serviceheaders = new HttpHeaders('Content-Type': 'application/json', 'Authorization' : AuthorizationHeader);

    // const headers = new HttpHeaders(
    //   'Content-Type': 'application/json', 'Authorization' : AuthorizationHeader
    // );

    const username = credentials ? credentials.username : '';
    const password = credentials ? credentials.password : '';

    this.http.post<HttpResponse<any>>('http://localhost:8080/login', JSON.stringify(username: username, password: password)
          , headers: serviceheaders, observe: 'response', responseType: 'json')
                  .subscribe( (response: HttpResponse<any>) => 
                    console.log('Headers only: ' + response.headers);
                    console.log('Response Headers : ' + response.headers.get('Authorization'));
                    const token = response.headers.get('Authorization');
                    if (token !== null) 
                      this.authenticated = true;
                    
                    return callback && callback();
    );

  

【问题讨论】:

***.com/questions/45505619/… 嗨,亚历克斯,感谢您的回复。但是在这种情况下,我认为问题更多是因为 cors 安全性不允许我访问标头。我在后端服务中设置了“Access-Control-Expose-Headers”。但是,我仍然无法访问响应标头中的授权密钥。你能告诉我我在后端设置 cors 配置的方式是否正确吗? 不太熟悉后端的东西 ;) 但至少如果你不observe 响应,就没有办法访问标题。 嗨,Alex,很抱歉没有通知您。我在前端的服务调用中也做了这些修改,以观察响应。请找到更新的方法 - authenticate(),我在其中明确添加了变量以观察 http 调用中的响应。你能确认这是否是正确的使用方法吗?通过此修改,当我尝试获取 response.headers.get('Authorization) 时,我看到控制台上打印了一个空值 【参考方案1】:

我知道我迟到了,但我在使用 React + Spring Boot 时遇到了类似的问题。通过在我的 CORS 配置中添加“config.setExposedHeaders(Arrays.asList("Authorization"))”解决了这个问题:

@Bean
CorsConfigurationSource corsConfigurationSource() 

    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    source.registerCorsConfiguration("/**", config.applyPermitDefaultValues());
    config.setExposedHeaders(Arrays.asList("Authorization"));

    return source;

在我的前端,我做了以下操作:

export default function ( history ) 
   const [login, setLogin] = useState('');
   const [password, setpassword] = useState('');

   async function handleLogin(e) 
       e.preventDefault();

       const response = await Api.post('/login', 
           login, senha: password
       );

       console.log(response.headers);
       if(response.headers['authorization'])
           history.push("/menu");
       

如您所见,“Authorization”标头是小写的。

【讨论】:

以上是关于在 Angular 5 应用程序中响应 Spring Boot 时无法读取授权标头的主要内容,如果未能解决你的问题,请参考以下文章

在 Angular 5 应用程序中响应 Spring Boot 时无法读取授权标头

Angular 5 - 尽管 (Set-Cookie) 在响应标头中,但设置了 Cookie

Angular 1.5,错误响应状态始终为-1

Angular 5+ 无法保存结果形式的可观察响应

Angular 5 管理带有 blob 响应和 json 错误的 http get

JSON响应的Angular 5显示结果