使用弹簧安全性和角度进行身份验证
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;
UserDetailsService 实现:
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 之间的区别
【讨论】:
以上是关于使用弹簧安全性和角度进行身份验证的主要内容,如果未能解决你的问题,请参考以下文章