SpringSecurity学习总结1-入门
我的学习视频连接:https://www.bilibili.com/video/BV14Q4y1o7nB?spm_id_from=333.999.0.0
Spring Security 官网:https://spring.io/projects/spring-security#learn
1. SpringSecurity入门
Spring Security是一个高度自定义的安全框架。利用Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。Spring Security的2个重要核心功能。“认证”是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序执行动作的其他系统),通俗点说就是系统认为用户是是否能登录。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲究是判断用户是否有权限去做某些事情。
1.1 创建入门项目
我们先新建一个maven项目,我们使用SpringBoott方式,引入以下必要的依赖。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
编写一个SpringBoot项目的启动类
@SpringBootApplication public class SpringSecurityApplication { public static void main(String[] args) { try { SpringApplication.run(SpringSecurityApplication.class, args); } catch (Exception e) { e.printStackTrace(); } } }
在resources目录下,新建一个文件夹static,在static下创建一个 index.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> 登录成功!! </body> </html>
1.2 测试入门项目
我们运行启动类 SpringSecurityApplication,当项目启动之后,启动日志里会有这样一句日志(说明SpringSecurity已经生效,默认登录用户名是user, 默认密码每次启动都不一样)
Using generated security password: afa0c4a7-9108-42b7-9d40-b663920cb72d
当项目启动后,输入请求地址:http://localhost:8080/ 浏览器自动跳转到地址 http://localhost:8080/login 会看到一个登录页面。
这个登录页面是SpringSecurity自带的,输入日志里打印的username和password之后,登录成功,浏览器会挑战到我们自己写的index.html页面。
2. Spring Security的基础方法
2.1 UserDetailsService接口
package org.springframework.security.core.userdetails; public interface UserDetailsService { /** * Locates the user based on the username. In the actual implementation, the search * may possibly be case sensitive, or case insensitive depending on how the * implementation instance is configured. In this case, the <code>UserDetails</code> * object that comes back may have a username that is of a different case than what * was actually requested.. * * @param username the username identifying the user whose data is required. * * @return a fully populated user record (never <code>null</code>) * * @throws UsernameNotFoundException if the user could not be found or the user has no * GrantedAuthority */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
UserDetailsService接口是Spring Security的主要类,后面我们需要继承这个接口,来实现自己的业务逻辑。
其中loadUserByUsername(String username)是用来实现登录逻辑的,参数username就是登录用户名,也就是刚才我们填写的user,如果用户名不存在会报UsernameNotFoundException异常,返回值是UserDetails对象。
2.1 UserDetails接口
package org.springframework.security.core.userdetails; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import java.io.Serializable; import java.util.Collection; public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
UserDetails也是一个接口类,以后我们也可以让自己的用户类实现UserDetails的方法,来完成自己的业务逻辑。
UserDetails继承了Serializable,说明可以被序列化。
Collection<? extends GrantedAuthority> getAuthorities(); 此方法用于获取用户权限的,并且结果不能为null
String getPassword(); 获取用户的密码。
String getUsername(); 获取用户名。
boolean isAccountNonExpired(); 判断账号是否未过期,过期的账号无法进行认证。
boolean isAccountNonLocked(); 判断账号是否未被锁定。被锁定的用户无法进行认证。
boolean isCredentialsNonExpired(); 判断用户的凭证(密码)是否未过期,过期的凭据无法进行身份验证。
boolean isEnabled(); 账号是否被启动,未启动的用户无法进行认证。
2.3 User类
UserDetails是一个接口,所以不能直接使用,SpringSecurity提供了一个实现类User
package org.springframework.security.core.userdetails; public class User implements UserDetails, CredentialsContainer { private String password; private final String username; private final Set<GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities); } public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (((username == null) || "".equals(username)) || (password == null)) { throw new IllegalArgumentException( "Cannot pass null or empty values to constructor"); } this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); } }
User继承了UserDetails,提供了UserDetails的7条属性和2个构造方法。User(String username, String password, Collection<? extends GrantedAuthority> authorities) 这三个参数的构造方法实际上是调用了下面7个参数的构造方法,传的参数就是username、password和authorities。
当UserDetailsService的loadUserByUsername方法显示登录认证之后,会在数据库或内存中获取到username、password、authorities赋值给UserDetails的实现类User,并返回,实现了完整的登录逻辑。
2.4 PsswordEncoder 密码加密
接下来我们认识一下密码验证的核心接口 PasswordEncoder,
密码解析器,接口,
package org.springframework.security.crypto.password; public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); default boolean upgradeEncoding(String encodedPassword) { return false; } }
String encode(CharSequence rawPassword); 密码加密方法,参数rawPassword可以理解为客户端的明文密码,SpringSecurity推荐使用SHA-1或者Hash算法加密,Hash算法推荐使用8位字符或随机salt。
boolean matches(CharSequence rawPassword, String encodedPassword); 密码匹配方法,rawPassword是明文密码,encodedPassword是加密后的密码,匹配这两个密码是否一致。
default boolean upgradeEncoding(String encodedPassword) 二次加密方法,对已加密的密码,再次加密。默认返回false是不需要二次加密。
PasswordEncoder是接口,它也有很多实现类,官方推荐使用的实现类是 BCryptPasswordEncoder
public class BCryptPasswordEncoder implements PasswordEncoder { private Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}"); private final Log logger = LogFactory.getLog(getClass()); private final int strength; private final SecureRandom random; public BCryptPasswordEncoder() { this(-1); } /** * @param strength the log rounds to use, between 4 and 31 */ public BCryptPasswordEncoder(int strength) { this(strength, null); } /** * @param strength the log rounds to use, between 4 and 31 * @param random the secure random instance to use * */ public BCryptPasswordEncoder(int strength, SecureRandom random) { if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) { throw new IllegalArgumentException("Bad strength"); } this.strength = strength; this.random = random; } public String encode(CharSequence rawPassword) { String salt; if (strength > 0) { if (random != null) { salt = BCrypt.gensalt(strength, random); } else { salt = BCrypt.gensalt(strength); } } else { salt = BCrypt.gensalt(); } return BCrypt.hashpw(rawPassword.toString(), salt); } public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword == null || encodedPassword.length() == 0) { logger.warn("Empty encoded password"); return false; } if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) { logger.warn("Encoded password does not look like BCrypt"); return false; } return BCrypt.checkpw(rawPassword.toString(), encodedPassword); } }
BCryptPasswordEncoder是一个强哈希加密方法。
private final int strength; 此参数决定了密码强度,默认是10。如果是10以上的数字,密码会更加安全,但是会在加密过程消耗更多的性能。
public String encode(CharSequence rawPassword) 对明文密码加密,加密时使用了随机salt(随机字符串),保证每次加密结果不一样。(注:如果没有使用随机salt,相同字符串加密后的结果是一样,就很容易猜到密码,很不安全)
我们可以写个测试用例试一下,多运行几次会发现每次加密的结果是不一样的,这就是随机salt起作用了。
@RunWith(SpringRunner.class) @SpringBootTest(classes = SpringSecurityApplication.class) public class SpringSecurityTest { @Test public void checkPassword(){ PasswordEncoder pw = new BCryptPasswordEncoder(); // 加密 String encode = pw.encode("123"); System.out.println("==== 加密后的密码:" + encode); // 比较密码 boolean matches = pw.matches("123", encode); System.out.println("==== 比较密码:" + matches); } }
3. 登录功能实现
3.1 配置密码解析器
当我们要实现自定义登录时,Spring容器内必须已经存在密码解析器,所以我们要提前把PasswordEncoder写到配置类里面去,让Spring来管理。
@Configuration public class SecurityConfig { /** * 密码解析器 */ @Bean public PasswordEncoder getPasswordEncoder(){ return new BCryptPasswordEncoder(); } }
3.2 实现登录验证
创建一个类,实现UserDetailsService接口。正常情况下用户密码都要从数据库查询的,我这里为了测试方便,直接写死的。
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 根据用户名去数据库查询,如果不存在就抛UsernameNotFoundException异常 if(!"admin".equals(username)){ throw new UsernameNotFoundException("用户名不存在"); } // 2. 比较密码(注册时已经加密过,如果匹配成功返回UserDetails) String password = passwordEncoder.encode("123"); // 正常逻辑是需要使用 passwordEncoder.matches() 方法来验证密码的是否正确的。 return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal")); } }
配置完毕后,我们再次启动应用程序,发现日志里不打印默认用户名密码的,在登录页面只能输入 username=admin, password=123 才可以登录。
3.3 默认参数
在SpringSecurity中,默认的用户名和密码参数就是username和password,只支持POST请求,原因在于SpringSecurity实现了一个叫UsernamePasswordAuthenticationFilter 的拦截器,只会获取username和password的值,取不到只就会默认为空字符串,所以验证失败。有兴趣的同学可以自己看看这个类。
package org.springframework.security.web.authentication; public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private boolean postOnly = true;
...... }
如果我们非要改换成其他参数的话,可以在SecurityCofnig配置类里实现一个 configure 方法,用于一些SpringSecurity的配置。下面的配置就把接收用户名和密码的参数换成了username123和password123。
其他的一些配置是在前后端未分离的情况下,一些授权和跳转的设置。
@Override protected void configure(HttpSecurity http) throws Exception { // 表单提交 http.formLogin() // 自定义入参 .usernameParameter("username123") .passwordParameter("password123") .loginPage("/login.html") // 自定义登录页面 // 必须和登录页面的方法一样,会执行自定义登录逻辑 .loginProcessingUrl("/login") // 登录成功跳转的页面,POST请求,toMain方法会跳转到main.html页面 .successForwardUrl("/toMain") // 登录失败跳转的页面,POST请求,toError方法会跳转到error.html页面 .failureForwardUrl("/toError"); // 授权设置 http.authorizeRequests() // 放行/error.html 不需要认证 .antMatchers( "/error.html").permitAll() // 放行/login.html 不需要认证 .antMatchers("/login.html").permitAll() // 所有请求都必须认证才能访问,必须登录 .anyRequest().authenticated(); // 关闭crsf防护 http.csrf().disable(); }
3.4 自定义跳转逻辑
在上面的配置里,登录成功后,跳转设置是 successForwardUrl("/toMain"),表示登录成功后调用/toMain方法,实现内部页面的跳转。假设我们想跳转到 http://www.baidu.com ,这样的设置就不生效了。
点击lsuccessForwordUrl这个方法,我们看到里面使用的是一个登录成功的拦截器 ForwardAuthenticationSuccessHandler
public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) { this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl)); return this; }
所以我们需要自定义一个登录成功的拦截器
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { // 跳转的url private String url; // 构造方法 public MyAuthenticationSuccessHandler(String url) { this.url = url; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 重定向 response.sendRedirect(url); } }
然后调整一下配置内容。把原来的successForwardUrl注释掉,使用successHandler
// .successForwardUrl("/toMain") .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
我们再来看这个自定义登录成功的拦截器参数,有一个Authentication参数,用户登录成功之后可以拿到登录用户的信息,方法如下:
public interface Authentication extends Principal, Serializable { /** * 用户的权限列表 */ Collection<? extends GrantedAuthority> getAuthorities(); /** * 或者用户凭证(密码),但因为安全设置这个值一般的null */ Object getCredentials(); /** * 获取详情 */ Object getDetails(); /** * 获取UserDetails的实现类,用户详情 */ Object getPrincipal(); /** * 是否被认证 */ boolean isAuthenticated(); /** * 设置认证状态 */ void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
而登录失败的跳转配置 failureForwardUrl 里面也是实现了一个登录失败的拦截器 ForwardAuthenticationFailureHandler,业务逻辑基本一样的。
public FormLoginConfigurer<H> failureForwardUrl(String forwardUrl) { this.failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl)); return this; }
4. 授权配置
4.1 授权
上面在SecurityConfig类的configure 方法中,已经添加了一些授权设置。
@Override protected void configure(HttpSecurity http) throws Exception { ..... // 授权设置 http.authorizeRequests() // 放行/error.html 不需要认证 .antMatchers( "/error.html").permitAll() // 放行/login.html 不需要认证 .antMatchers("/login.html").permitAll() // 所有请求都必须认证才能访问,必须登录(这行代码必须放到最后面) .anyRequest().authenticated(); // 关闭crsf防护 http.csrf().disable(); }
anyRequest() 表示匹配所有的请求,一般情况下此方法都会使用,设置全部内容都需要进行认证。
anyMatcher() 表示要设置不需要认证的地址,方法定义如下:
public C antMatchers(String... antPatterns) {
参数是不定向参数,每个参数是一个ant表达式,用于匹配URL规则。
规则如下:
? 匹配一个字符 * 匹配0个或多个字符 ** 匹配0个或多个目录
在实际项目中经常需要放行的所有静态资源,西面演示放行js文件夹所有脚本文件。
.antMatchers("/js/**", "/css/**", "/images/**").permitAll()
还有一种匹配方式是只要是.js文件都放行
.antMatchers("/**/*.js").permitAll()
regexMatchers() 正则表达式,指定要放行的资源或目录
.regexMatchers(".+[.]png").permitAll()
在regexMatchers 和 antMetchers 里还可以指定请求Http请求类型
.regexMatchers(HttpMethod.POST, ".+[.]png").permitAll()
mvcMetchers() mvc匹配
.mvcMatchers("/demo").servletPath("/zh").permitAll()
permitAll() 在上面增加匹配时都有permitAll这个方法,点击进去我们看到有几种选项
static final String permitAll = "permitAll"; // 允许所有 private static final String denyAll = "denyAll"; // 禁止所有 private static final String anonymous = "anonymous"; // 匿名,类似于permitAll,指不需要登录可以的页面,比如首页 private static final String authenticated = "authenticated"; // 授权 private static final String fullyAuthenticated = "fullyAuthenticated"; // 必须账号密码登录授权 private static final String rememberMe = "rememberMe"; // 记住我
4.2 权限控制
在前面的UserDetailsServiceImpl中,我们设置了用户登录后拥有的权限
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
我们可以指定拥有权限的用户,才能访问指定的页面
// 权限控制,严格区分大小写 .antMatchers("/main1.html").hasAuthority("admin") // 匹配2个权限的任意一个 .antMatchers("/main1.html").hasAnyAuthority("admin", "admin2")
4.3 角色控制
在UserDetailsServiceImpl中,用户登录成功后是可以自定角色的。注意:角色区别于权限,必须大写的ROLE_ 开头。
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc"));
上面的例子中,我们指定了一个abc的角色。
// 角色控制,严格区分大小写 .antMatchers("/main1.html").hasRole("abc") // 角色控制,严格区分大小写 .antMatchers("/main1.html").hasAnyRole("abc", "abc2")
4.4 IP地址控制
用于指定IP地址允许访问页面
// 基于IP地址控制 .antMatchers("/main1.html").hasIpAddress("127.0.0.1")
4.5 403异常拦截
按照前面的例子中,如果用户没有权限,就会展示SpringSecurity默认的403页面,比较难看。我们可以配置一个自定义的403异常拦截器。
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { // 响应状态 response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 返回json格式 response.setHeader("Content-Type", "application/json;charset=utf-8"); // 返回消息 PrintWriter writer = response.getWriter(); writer.write("{\"status\":\"error\",\"mgs\":\"权限不足,请联系管理员\"}"); writer.flush(); writer.close(); } }
把这个403异常拦截器配置到SecurityConfig中
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { ...... // 异常拦截 http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 关闭crsf防护 http.csrf().disable(); } }
4.6 access表达式
在以上4.1、4.2、4.3讲到的所有控制方法,本质上都是access,比如我们点击permitAll方法查看
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry permitAll() { return this.access("permitAll"); }
在Spring Security官网上提供了access表达式:
我们之前的权限控制可以改成这样的写法
.antMatchers("/login.html").access("permitAll")
.antMatchers("/main1.html").access("hasRole('abc')")
4.7 自定义访问控制
我们先创建一个权限验证的Service,通过用户已授权的的Grantedauthority来判断用户的相关权限。
@Service public class MyServiceImpl implements MyService { @Override public boolean hashPermission(HttpServletRequest request, Authentication authentication) { // 获取主体 Object obj = authentication.getPrincipal(); // 判断主体是否属于UserDetails if(obj instanceof UserDetails) { // 获取权限,集合是GrantAuthority泛型 UserDetails userDetails = (UserDetails) obj; Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); // 判断请求的url是否在权限里 return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI())); } return false; } }
自定义访问权限的使用方式
// 自定义access方法 .anyRequest().access("@myServiceImpl.hashPermission(request, authentication)")
5 基于注解的访问控制
在Spring Security中提供了一些访问控制的注解。这些注解都是默认是不可用的,需要通过@EnableGlobalMethodSecurity记性开启后使用。
如果设置的条件允许,程序正常执行,如果不允许会报500
注意:注解和SecurityConfigure类中的access配置会有冲突,所以只建议使用其中一个。
org.springframework.security.access.AccessDeniedException: 不允许访问
这些注解可以写到Service接口或方法上,也可以写到Controller或Conroller的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。
5.1 @Secured
@Secured是专门用于判断是否具有角色的。能写在方法或类上。参数要以ROLE_开头。
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Secured { /** * Returns the list of security configuration attributes (e.g. ROLE_USER, ROLE_ADMIN). * * @return String[] The secure method attributes */ public String[] value(); }
在启动类中开启@Secured注解
@EnableGlobalMethodSecurity(securedEnabled = true) @SpringBootApplication public class SpringSecurityApplication { }
使用@Secured注解。我们在Controller中的main方法中使用了@Secured注解,并指定角色abc可以访问。
@Secured("ROLE_abc") @PostMapping("/toMain") public String main(){ return "redirect:main.html"; }
5.2 @PreAuthorize / @ PostAuthorize
@PreAuthorize 和 @PostAuthorize 都是方法或类级别的注解。
@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface PreAuthorize { /** * @return the Spring-EL expression to be evaluated before invoking the protected * method */ String value(); }
@PreAuthorize 表示访问方法或类在执行之前判断权限,大多情况下毒是用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
@PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用。
在启动类上开启 @PreAuthorize注解
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
在方法使用@PreAuthorize注解
@PreAuthorize("hasRole('abc')") @PostMapping("/toMain") public String main(){ return "redirect:main.html"; }
6 退出登录
用户只需要向Spring Security项目中发送/logout 退出请求即可。
6.1 退出登录
实现退出非常简单,只需要在页面总添加 /logout 的超链接
<a href="/logout">退出登录</a>
为了实现更好的效果,通常添加退出的配置。默认的退出url为 /logout, 退出成功后跳转到 /login?logout
还需要在SecurityConfig中添加退出配置
http.logout().logoutSuccessUrl("/login.html");
自定义退出后地址
http.logout().logoutUrl("/user/logout");
7. SpringSecurity中的CSRF
在刚开始介绍Spring Security时,在配置中一直存在这样一行代码:http.csrf().disable(); 如果没有这行代码导致用于无法被认证。这行代码的含义是:关闭csrf防护。
7.1 什么是CSRF ?
CSRF(Cross-site request forgery) 跨站请求伪造,也被称为“OneClick Attack” 或者 "Session Riding"。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,IP地址,端口中任何一个相同就是跨域请求。
客户端和服务进行交互时,由于http协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id 用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
7.2 Spring Security中的CSRF
从Spring Security4开始CSRF防护默认开启。默认会拦截请求,进行CSRF处理。CSRF为了保护不是其他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token在服务端产生)的内容,如果token和服务端是token匹配成功,则正常访问。
我感觉这个没啥用处......