使用弹簧安全性和角度进行身份验证

Posted

技术标签:

【中文标题】使用弹簧安全性和角度进行身份验证【英文标题】:authentification with spring security and angular 【发布时间】:2020-06-21 13:59:45 【问题描述】:

我正在使用 spring boot-angular 应用程序实现 spring security。在我的用户控制器中,我在以下 url 上映射了一个方法:/api/user/login 来连接用户,获取用户信息,识别用户并检查他是否在数据库中注册。当我启动 Angular 应用程序时,会触发 url: http://localhost:8080/api/user/login 并出现 405 状态错误。我有几个问题

1) url:http://localhost:8080/api/user/login, 在开始时被触发是否正常?我不明白为什么。我希望用户的连接专门用于弹出表单(触发http://localhost:8080/api/user/login url)

2)如果正常的话,这样一个url的目的是什么,我该如何用这个url实现端点控制器?

3) 在我的 Angular 应用程序中,我将 proxy.config.json 设置如下


  "/api/*": 

    "target":  
       "host": "localhost",
       "protocol": "http:",
       "port": 8080
     ,
    "secure": false,
     "changeOrigin": true,
     "logLevel": "info"
  

将每个 localhost:42OO/api 重定向到 localhost:8080/api。为此,在前端部分,我使用“ng serve --proxy-config proxy.config.json”命令启动应用程序。

当“http://localhost:8080/api/user/login”被触发时,我有以下错误信息:

    Blocage d’une requête multiorigines (Cross-Origin Request) : la politique « Same Origin » ne permet pas de consulter la ressource distante située sur http://localhost:8080/api/user/login. Raison : l’en-tête CORS « Access-Control-Allow-Origin » est manquant.
Blocking of a multi-origin request (Cross-Origin Request): the "Same Origin" policy does not allow consulting the remote resource located on http: // localhost: 8080 / api / user / login. Reason: The CORS header "Access-Control-Allow-Origin" is missing.

那么,如何处理呢?

这里是spring安全问题的配置

文件 WebSecurityConfig.java

package com.example.demoImmobilierBack;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.example.demoImmobilierBack.service.MyUserDetailsService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Bean
    public UserDetailsService userDetailsService() 
        return new MyUserDetailsService();
    

    @Autowired
    private DataSource dataSource;

    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

    @Bean
    public DaoAuthenticationProvider authenticationProvider() 
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    

    @Bean("authenticationManager")
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();


    @Override
    protected void configure(AuthenticationManagerBuilder auth) 
        auth.authenticationProvider(authenticationProvider());
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http         
        .headers()
         .frameOptions().sameOrigin()
         .and()
           .authorizeRequests()
            .antMatchers("/**/*.scss", "/**/*.js","/**/*.html").permitAll()
               .antMatchers("/").permitAll()
               .antMatchers("/admin/**").hasRole("ADMIN")
               .anyRequest().authenticated()
               .and()
           .formLogin()
               .loginPage("/api/user/login")
               .defaultSuccessUrl("/")
//               .failureUrl("/login?error")
               .failureUrl("/")
               .permitAll()
               .and()
           .logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .logoutSuccessUrl("/")
//            .logoutSuccessUrl("/login?logout")
            .deleteCookies("my-remember-me-cookie")
               .permitAll()
               .and()
            .rememberMe()
             //.key("my-secure-key")
             .rememberMeCookieName("my-remember-me-cookie")
             .tokenRepository(persistentTokenRepository())
             .tokenValiditySeconds(24 * 60 * 60)
             .and()
           .exceptionHandling()
           .and()
           .csrf().disable();
    

    PersistentTokenRepository persistentTokenRepository()
        JdbcTokenRepositoryImpl tokenRepositoryImpl = new JdbcTokenRepositoryImpl();
        tokenRepositoryImpl.setDataSource(dataSource);
        return tokenRepositoryImpl;
    


用户控制器UserController.java

package com.example.demoImmobilierBack.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.demoImmobilierBack.dto.UserDTO;
import com.example.demoImmobilierBack.service.UserService;

@RestController
@RequestMapping("/api/user")
public class UserController 

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/login",
    method = RequestMethod.POST)
    public @ResponseBody UserDTO login(@RequestBody UserDTO userDTO)
        String message = userService.checkIfUserExistsAndGoodCredential(userDTO);
        if (message.isEmpty()) 
            userDTO = userService.findByEmailAndPassword(userDTO.getEmail(), userDTO.getPassword());
            userDTO.setPassword("");
         else 
            userDTO.setMessage(message);
        
        return userDTO;
    

InitialDataLoader.java 文件

package com.example.demoImmobilierBack.service;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import com.example.demoImmobilierBack.model.Privilege;
import com.example.demoImmobilierBack.model.Role;
import com.example.demoImmobilierBack.model.User;
import com.example.demoImmobilierBack.repository.PrivilegeRepository;
import com.example.demoImmobilierBack.repository.RoleRepository;
import com.example.demoImmobilierBack.repository.UserRepository;

@Component
public class InitialDataLoader implements
  ApplicationListener<ContextRefreshedEvent> 

    boolean alreadySetup = false;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PrivilegeRepository privilegeRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    @Transactional
    public void onApplicationEvent(ContextRefreshedEvent event) 

        if (alreadySetup)
            return;
        Privilege readPrivilege
          = createPrivilegeIfNotFound("READ_PRIVILEGE");
        Privilege writePrivilege
          = createPrivilegeIfNotFound("WRITE_PRIVILEGE");

        List<Privilege> adminPrivileges = Arrays.asList(
          readPrivilege, writePrivilege);        
        createRoleIfNotFound("ADMIN", adminPrivileges);
        createRoleIfNotFound("LOUEUR", adminPrivileges);
        createRoleIfNotFound("ACHETER", adminPrivileges);
        createRoleIfNotFound("DEPOSE_LOUER", adminPrivileges);
        createRoleIfNotFound("DEPOSE_ACHETER", adminPrivileges);
        createRoleIfNotFound("AGENCE", adminPrivileges);
        createRoleIfNotFound("PROMOTEUR", adminPrivileges);



        Role adminRole = roleRepository.findByName("ADMIN");

        User user = userRepository.findByEmail("flamant@club-internet.fr");
        if (user == null) 
            user = new User();
            user.setGender("M");
            user.setFirstName("adminFirstName");
            user.setLastName("adminLastName");
            user.setRaisonSociale("adminLastName");
            user.setName("PARTICULIER");
            user.setLastName("adminLastName");
            user.setPassword(passwordEncoder.encode("adminPassword"));
            user.setEmail("flamant@club-internet.fr");
            user.setRoles(Arrays.asList(adminRole));
            user.setEnabled(true);
            user.setAccountNonExpired(true);
            user.setAccountNonLocked(true);
            user.setCredentialsNonExpired(true);
            userRepository.save(user);
        

        alreadySetup = true;
    

    @Transactional
    private Privilege createPrivilegeIfNotFound(String name) 

        Privilege privilege = privilegeRepository.findByName(name);
        if (privilege == null) 
            privilege = new Privilege(name);
            privilegeRepository.save(privilege);
        
        return privilege;
    

    @Transactional
    private Role createRoleIfNotFound(
      String name, Collection<Privilege> privileges) 

        Role role = roleRepository.findByName(name);
        if (role == null) 
            role = new Role(name);
            role.setPrivileges(privileges);
            roleRepository.save(role);
        
        return role;
    

UserDetailsS​​ervice 实现:

package com.example.demoImmobilierBack.service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.example.demoImmobilierBack.model.Privilege;
import com.example.demoImmobilierBack.model.Role;
import com.example.demoImmobilierBack.model.User;
import com.example.demoImmobilierBack.repository.RoleRepository;
import com.example.demoImmobilierBack.repository.UserRepository;

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService 

    @Autowired
    private UserRepository userRepository;

    //@Autowired
    //private IUserService service;

    @Autowired
    private MessageSource messages;

    @Autowired
    private RoleRepository roleRepository;

    @Override
    public UserDetails loadUserByUsername(String email)
      throws UsernameNotFoundException 

        User user = userRepository.findByEmail(email);
        if (user == null) 
            return new org.springframework.security.core.userdetails.User(
              " ", " ", true, true, true, true, 
              (Collection<? extends GrantedAuthority>) getAuthorities(Arrays.asList(roleRepository.findByName("ROLE_USER"))));
        

        return new org.springframework.security.core.userdetails.User(
          user.getEmail(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), 
          user.isAccountNonLocked(), getRolesAuthorities(user.getRoles()));
    

    private Collection<? extends GrantedAuthority> getRolesAuthorities(
              Collection<Role> roles) 
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        for (Role role :roles) 
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        

        return authorities;
    

    private Collection<? extends GrantedAuthority> getAuthorities(
      Collection<Role> roles) 

        return getGrantedAuthorities(getPrivileges(roles));
    

    private List<String> getPrivileges(Collection<Role> roles) 

        List<String> privileges = new ArrayList<>();
        List<Privilege> collection = new ArrayList<>();
        for (Role role : roles) 
            collection.addAll(role.getPrivileges());
        
        for (Privilege item : collection) 
            privileges.add(item.getName());
        
        return privileges;
    

    private List<GrantedAuthority> getGrantedAuthorities(List<String> privileges) 
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String privilege : privileges) 
            authorities.add(new SimpleGrantedAuthority(privilege));
        
        return authorities;
    

【问题讨论】:

为 CORS 添加了答案。对于其他问题,请分享您的 Angular 代码(为什么在开始时获取 URL)。谢谢。 【参考方案1】:

至少,要继续前进,您应该在后端启用CORS。 Angular 应用程序在 http://localhost:4200 提供服务,浏览器拒绝向另一个域(在本例中为 http://localhost:8080)发出请求。More information on CORS.

因此,您应该在您的后端应用中将您的前端网址列入白名单。

您可以通过在 Application 类中添加一些行来使用 Spring Boot 轻松做到这一点:

@SpringBootApplication
public class Application implements WebMvcConfigurer 
    ...

    /**
     * CORS configuration
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) 
        registry.addMapping("/**")
                .allowedOrigins(
                        "http://localhost:4200"
                )
                .allowedMethods(
                        "GET",
                        "PUT",
                        "POST",
                        "DELETE",
                        "PATCH",
                        "OPTIONS"
                );
    

    ...

【讨论】:

如果您已经在主类中添加了@SpringBootApplication,为什么我们还需要@Configuration。? 更多关于这个注释在这里..docs.spring.io/spring-boot/docs/2.1.12.RELEASE/reference/html/…【参考方案2】:

gnana 和 Arpit,感谢您的回答。

考虑到 CORS 上的问题,我添加了您提供给我的代码并且它可以工作。但是你能解释一下你的解决方案(SOLUTION1)和(SOLUTION2 :)我添加文件的事实之间的区别吗 proxy.config.json


  "/api/*": 

    "target":  
       "host": "localhost",
       "protocol": "http:",
       "port": 8080
     ,
    "secure": false,
     "changeOrigin": true,
     "logLevel": "info"
  

并用

启动了前面部分
ng serve --proxy-config proxy.config.json

使用 SOLUTION2,在前端部分启动的每个请求(即http://localhost:4200)都被转换为http://localhost:8080,那么在角度应用程序开始时自动触发的以下请求http://localhost:8080/api/user/login 呢

使用您的解决方案 (SOLUTION1),您允许诸如 http://localhost:4200 之类的请求(但实际上在 SOLUTION2 仍在运行的情况下,这些请求在前端部分被转换为 http://localhost:8080),同时后端负责当我第一次运行angular应用程序时触发的请求http://localhost:8080/api/user/login(应用程序的主页,不包括登录页面(稍后在我单击按钮时显示))

如果你能给我一些解释,谢谢

考虑到我在第一篇文章中提出的其他问题,我进一步调查了

不知道为什么我运行前端应用的主页时会触发http://localhost:8080/api/user/login请求。 Thierry 建议分享我的 Angular 代码,但我不相信它会有所帮助:在 Angular 部分提供登录的唯一请求不会被触发。我检查了它。在前端部分登录的身份验证过程基于单击按钮时出现(弹出)的表单。 Thierry,您能否建议我分享您希望我分享的 Angular 应用程序的哪一部分。谢谢

我可以告诉你的是,当我有以下控制器时

@RestController
@RequestMapping("/api/user")
public class UserController 

@Autowired
private UserService userService;

@RequestMapping(value = "/login",
method = RequestMethod.POST)
public @ResponseBody UserDTO login(@RequestBody UserDTO userDTO)
    String message = userService.checkIfUserExistsAndGoodCredential(userDTO);
    if (message.isEmpty()) 
        userDTO = userService.findByEmailAndPassword(userDTO.getEmail(), userDTO.getPassword());
        userDTO.setPassword("");
     else 
        userDTO.setMessage(message);
    
    return userDTO;

下面的url:http://localhost:8080/api/user/login被触发了(为什么?看不出来的原因,只能说是靠后面的下面代码了

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http         
        .headers()
         .frameOptions().sameOrigin()
         .and()
           .authorizeRequests()
            .antMatchers("/**/*.scss", "/**/*.js","/**/*.html").permitAll()
               .antMatchers("/").permitAll()
               .antMatchers("/admin/**").hasRole("ADMIN")
               .anyRequest().authenticated()
               .and()
           .formLogin()
               .loginPage("/api/user/login")
               .defaultSuccessUrl("/")
//               .failureUrl("/login?error")
               .failureUrl("/")
               .permitAll()

当我检查 Firebug 时,我有以下标题

URL de la requête : http://localhost:8080/api/user/login
Méthode de la requête : GET
Adresse distante : 127.0.0.1:8080
Code d’état :405

以及下面的回复

timestamp   2020-03-11T11:09:05.482+0000
status  405
error   Method Not Allowed
message Request method 'GET' not supported
path    /api/user/login

此响应是预期的,因为它被映射到 POST 方法,但是当我将控制器映射更改为以下时

@RestController
@RequestMapping("/api/user")
public class UserController 

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/dummylogin",
    method = RequestMethod.POST)
    public @ResponseBody UserDTO login(@RequestBody UserDTO userDTO)
        String message = userService.checkIfUserExistsAndGoodCredential(userDTO);
        if (message.isEmpty()) 
            userDTO = userService.findByEmailAndPassword(userDTO.getEmail(), userDTO.getPassword());
            userDTO.setPassword("");
         else 
            userDTO.setMessage(message);
        
        return userDTO;
    

    @RequestMapping(value = "/login",
    method = RequestMethod.GET)
    public @ResponseBody String login()
        return "Hello world";
     

我得到以下标题

URL de la requête :http://localhost:8080/api/user/login
Méthode de la requête : GET
Adresse distante : 127.0.0.1:8080
Code d’état :200

以及下面的回复

Hello world

你能解释一下为什么这个请求会被自动触发吗?如果我不能放弃这个行为,我必须如何将它映射到控制器部分?谢谢

【讨论】:

【参考方案3】:

我找到了纠正错误行为的方法。添加行就足够了

.antMatchers("/**/*").permitAll()

@Override
protected void configure(HttpSecurity http) throws Exception 
    http         
    .headers()
     .frameOptions().sameOrigin()
     .and()
       .authorizeRequests()
        .antMatchers("/**/*.scss", "/**/*.js","/**/*.html").permitAll()
           .antMatchers("/**/*").permitAll()
           .antMatchers("/admin/**").hasRole("ADMIN")
           .anyRequest().authenticated()
           .and()
       .formLogin()
           .loginPage("/api/user/login")
           .defaultSuccessUrl("/")
           .failureUrl("/")
           .permitAll()
           .and()
       .logout()

添加此行允许未经身份验证的用户访问在主页上触发的请求,因此不要要求未经身份验证的用户显示登录表单

考虑到允许 CORS 请求,我仍然希望得到解释以了解 SOLUTION1 和 SOLUTION2 之间的区别

【讨论】:

以上是关于使用弹簧安全性和角度进行身份验证的主要内容,如果未能解决你的问题,请参考以下文章

如何从控制器调用身份验证 - 弹簧安全核心插件

Spring安全和角度

没有弹簧安全性的 ldap 和 JWT 身份验证

弹簧安全过滤器没有启动

具有弹簧安全性的 JWT 身份验证 graphql-spqr

Spring安全缓存基本身份验证?不验证后续请求