目录
1.基础概念
1)认证授权会话
2)授权的数据模型
3)RBAC
2.Spring Security简单实现
3.UserDetailsService
4.PassworldEncoder
5.自定义登录页面
6.自定义跳转
7.响应异常信息
8.RememberMe功能
认证概念:
认证就是对用户的身份进行确认,比如我们登录QQ需要输入账号和密码,而这个过程就是认证.
用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时要求验证用户的身份信息,身份合法可以继续访问,不合法则拒绝访问,常见的用户身份验证方式有:用户密码登录,二维码登录,手机短信登录,指纹认证等方式.
会话概念:
用户通过认证后,为了避免用户每次操作都进行认证,可以将用户的信息保存在会话中.
会话就是系统为了保持当前用户登录状态所提供的机制,常见的有基于session方式和token方式.
授权概念:
授权是用户认证通过后,根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有则拒绝访问.
认证是为了保护用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能访问不同的资源.
如何进行授权就是如何对用户访问资源进行控制,首先需要学习授权相关的数据模型.
授权可简单理解为Who 对 What 进行 How 操作
Who:是主体(Subject),主体一般指用户,也可以是程序,需要访问系统中的资源.
What:资源(Resource),如系统菜单,页面,按钮,代码方法,系统商品信息,系统订单信息等.
How:权限/许可(Permission),规定了用户对资源的操作许可,权限离开资源没有意义,如用户查询权限等.
主体(用户id,账号,密码,...)
权限(权限id,权限标识,权限名称,资源id,...)
资源(资源id,资源名称,访问地址,...)
角色(角色id,角色名称,...)
角色和权限关系(角色id,权限id,...)
主体(用户)和角色关系(用户id,角色id,...)
同时资源可以分为功能资源和数据资源,举个例子,我要查询商品订单就是数据资源,而我要进行消息发布则是功能资源.
主体(用户),资源,权限关系图:
在实际开发中,有时也可以将资源和权限合并
权限(权限id,权限标识,权限名称,资源名称,资源访问地址,....)
RBAC基于角色的访问控制(Role-Based Access Control),是按照角色进行授权,比如主体的角色为总经理可以查询员工信息.
伪代码如下:
查询工资信息
判断是否是总经理
如果是则继续访问,如果不是则无权访问处理
但是需要进行修改角色权限时就需要修改授权相关代码,系统可扩展性差.
RBAC基于角色的访问控制(Role-Based Access Control),是按照资源(或权限)进行授权,比如用户必须具有查询工资的权限才可以进行查询.
伪代码如下:
查询工资信息
判断主体是否拥有查询工资权限
有则访问,无则拒绝
用户可以按照角色来判断,或者也可以通过资源(权限).
1)导入依赖
Spring Security依赖
org.springframework.boot
spring-boot-starter-security
org.springframework.security
spring-security-test
test
2)编写一个Controller类
@RestController
@SpringBootApplication
public class SecstudentApplication {
@GetMapping("/")
public String hello(){
return "hello,spring security";
}
public static void main(String[] args) {
SpringApplication.run(SecstudentApplication.class, args);
}
}
3)启动项目
当项目启动的时候,如果我们没配置security账户密码,默认为user和控制台生成的密码.
访问8080端口,会出现验证页面,这是security自带的默认页面.
输入相应的账户和密码才能进行访问.
通常我们不可能将账户密码写在配置文件中,也不显示,我们都是通过去数据库拿到账户和密码然后进行验证.
所以必须通过我们自定义逻辑来校验账户和密码.
我们可以通过实现UserDetailsService来实现.
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
//这个方法是通过用户名加载用户的一个方法,返回了一个UserDetails
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
在实现UserDetailsService的时候,我们还要返回一个UserDetails
UserDetails也是一个接口,它里面定义了各种返回用户信息和权限的方法:
public interface UserDetails extends Serializable {
//返回获取的所有权限,但是不能返回空
//包含用户的所有权限,当某功能需要权限,但是没有时就会出现404等跳转页面
Collection extends GrantedAuthority> getAuthorities();
//返回密码
String getPassword();
//返回用户名
String getUsername();
//判断用户是否过期,过期的账号不能用来登录
boolean isAccountNonExpired();
//判断用户是否被锁定,被锁定的用户也不能用来登录
boolean isAccountNonLocked();
//判断用户凭证是否过期
boolean isCredentialsNonExpired();
//判断账户是否可用
boolean isEnabled();
}
UserDetetails是一个接口,必须要有类来实现该接口才能使用,而security提供了一个默认实现类User,User里面存放着用户的信息和权限.
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 600L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set 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) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "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));
}
//其他方法....
}
如果我们通过重写UserDetailsService的loadUserByUserName的自定义逻辑方法没认证/查找到这个用户,则会抛出UsernameNotFoundException异常,标识没有找到这个用户.
在进行验证的时候,我们需要对密码进行加密,同时我们存储在数据库中的密码也应该是加密的,而security提供了一个加密接口
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
该接口定义了两个主要方法,一个是encode(加密方法),另一个是matches对比方法,而我们也不需要自己去写加密类,只需要使用提供的默认实现类就行了.
推荐使用BCryptPasswordEncoder来进行加密.
public class BCryptPasswordEncoder implements PasswordEncoder {
private Pattern BCRYPT_PATTERN;
private final Log logger;
private final int strength;
private final BCryptPasswordEncoder.BCryptVersion version;
private final SecureRandom random;
public BCryptPasswordEncoder() {
this(-1);
}
public BCryptPasswordEncoder(int strength) {
this(strength, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version) {
this(version, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, SecureRandom random) {
this(version, -1, random);
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
this(BCryptPasswordEncoder.BCryptVersion.$2A, strength, random);
}
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength) {
this(version, strength, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
this.logger = LogFactory.getLog(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.version = version;
this.strength = strength == -1 ? 10 : strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else {
String salt = this.getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
}
private String getSalt() {
return this.random != null ? BCrypt.gensalt(this.version.getVersion(), this.strength, this.random) : BCrypt.gensalt(this.version.getVersion(), this.strength);
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
}
然后我们只需要在创建配置类的时候创建这个实现类的bean对象就行了.
接下来就是在自定义认证逻辑中进行注入,因为我们在进行自定义认证逻辑中需要对账号和密码进行一个校验,通常我们从数据库中拿出来的密码就已经加密了,但是现在是模拟所以手动加密,然后会将拿到的密码放入到User中进行打包,然后security会对User中的密码进行校验,它会将用户输入的密码和加密过的密码用matchs进行解码然后比较.
实现UserDetailsService类的自定义认证类.
@Service
public class UserDetailServiceImp implements UserDetailsService {
//密码加密器
@Autowired
private PasswordEncoder pw;
//自定义认证方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//从数据库中查找,判断该用户是否存在
if(!"admin".equals(username)){
throw new UsernameNotFoundException("该用户不存在,请进行注册");
}
//暂时这样写的,实际上应该从数据库中取数据
String password = pw.encode("123");
//如果存在则将账号和密码还有相应的权限进行打包,放入到security的User类中,进行认证
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}
但是这里注意,这里并不进行密码校验,这里会将数据进行取出,然后会将取到的密码,和用户输入加密过后的密码进行令牌匹配,成功的话才能通过.
有了自定义认证类,我们可以将我们的认证方法写在里面,对用户登录进行校验.
有了自定义方法,我们就可以进行自己的认证方法了,但是页面现在还是使用的是security默认自带的页面,而一般开发我们都是使用自己的页面,所以我们现在要实现使用自己的页面进行认证.
自定义的认证授权器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注册一个加密器
@Bean
public PasswordEncoder getpw() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//首先,使用表单进行验证
http.formLogin()
//当有前端action的值为login的时候视为认证请求,会取调用认证方法
.loginProcessingUrl("/login")
//指定默认的认证页面
.loginPage("/login.html")
//认证成功后跳转到默认的页面
.successForwardUrl("/toMain")
//认证失败后进行跳转到默认的错误页面
.failureForwardUrl("/toError");
//资源访问认证
http.authorizeRequests()
//指定的资源不需认证可进行访问
.antMatchers("/login.html").permitAll()
.antMatchers("/error.html").permitAll()
//表示所有请求都需认证
.anyRequest()
.authenticated();
//关闭csrf
http.csrf().disable();
}
}
在自定义认证授权器中,定义了自定义的表单认证,同时指定了默认的验证页面和对认证的处理,并且将认证后成功和失败进行跳转,但是不能直接写入资源路径,因为post请求无法识别,只能使用get请求所以要在Controller中进行相应的跳转.
登录页面:
用户登录
需要注意的是,在进行传输的时候action必须要和loginProcessingUrl中的值对应,否则找不到自定义认证方法就会报错,同时用户名和密码传的值name默认必须是username和password否则会出错,但是也可以自定义传入值的名称.
自定义设置账户和密码名称,同时前端传入名也要一样
.usernameParameter("username123")
.passwordParameter("password123");
控制器跳转:
@Controller
public class MyController {
@RequestMapping("toMain")
public String toMain(){
return "redirect:main.html";
}
@RequestMapping("toError")
public String toError(){
return "redirect:error.html";
}
}
这样在我们进行验证后就会跳转到相应的页面了.
1)自定义成功登录处理器
在现在前后端分离的情况下,我们一般会跳转到外面的网页,而不是我们的内部资源.
所以我们要对外进行跳转的时候需要使用其他的方法进行跳转.
实现AuthenticationSuccessHandler方法,同时将url放入
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
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);
}
}
实现了上面的方法后我们就可以通过secessHanler来实现对指定url的跳转
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
而错误跳转也差不多是这样不过实现类不同.
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
同时http后的方法是
.failureHandler(new MyAuthenticationFailureHandler("error.html"));
而现在我们的认证授权器如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注册一个加密器
@Bean
public PasswordEncoder getpw() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//首先,使用表单进行验证
http.formLogin()
//当有前端action的值为login的时候视为认证请求,会取调用认证方法
.loginProcessingUrl("/login")
//指定默认的认证页面
.loginPage("/login.html")
//认证成功后跳转到默认的页面
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
//认证失败后进行跳转到默认的错误页面
.failureHandler(new MyAuthenticationFailureHandler("error.html"));
//资源访问认证
http.authorizeRequests()
//指定的资源不需认证可进行访问
.antMatchers("/login.html").permitAll()
.antMatchers("/error.html").permitAll()
//表示所有请求都需认证
.anyRequest()
.authenticated();
//关闭csrf
http.csrf().disable();
}
}
在有时候我们访问会出现一系列的问题,从而会返回不同的错误和状态码,但是这对用户来说难以看懂,所以我们需要设置相应的相应异常信息的功能,返回友好的提示或者页面跳转提示用户.
1.实现AccessDeniedHandler接口
/**
* 设置自定义的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);
//设置响应头
response.setHeader("Content-Type", "application/json;charset=utf-8");
//拿到响应的输出流
PrintWriter writer = response.getWriter();
//设置返回消息内容(json格式)
writer.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}");
//将输出内容刷新到内存
writer.flush();
writer.close();
}
}
我们设定专门相应哪个状态码,比如(404)
这个HttpServletResponse是一个枚举类,里面定义了各种状态码
public interface HttpServletResponse extends ServletResponse {
int SC_CONTINUE = 100;
int SC_SWITCHING_PROTOCOLS = 101;
int SC_OK = 200;
int SC_CREATED = 201;
int SC_ACCEPTED = 202;
int SC_NON_AUTHORITATIVE_INFORMATION = 203;
int SC_NO_CONTENT = 204;
int SC_RESET_CONTENT = 205;
int SC_PARTIAL_CONTENT = 206;
int SC_MULTIPLE_CHOICES = 300;
int SC_MOVED_PERMANENTLY = 301;
int SC_MOVED_TEMPORARILY = 302;
int SC_FOUND = 302;
int SC_SEE_OTHER = 303;
int SC_NOT_MODIFIED = 304;
int SC_USE_PROXY = 305;
int SC_TEMPORARY_REDIRECT = 307;
int SC_BAD_REQUEST = 400;
int SC_UNAUTHORIZED = 401;
int SC_PAYMENT_REQUIRED = 402;
int SC_FORBIDDEN = 403;
int SC_NOT_FOUND = 404;
int SC_METHOD_NOT_ALLOWED = 405;
int SC_NOT_ACCEPTABLE = 406;
int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
int SC_REQUEST_TIMEOUT = 408;
int SC_CONFLICT = 409;
int SC_GONE = 410;
int SC_LENGTH_REQUIRED = 411;
int SC_PRECONDITION_FAILED = 412;
int SC_REQUEST_ENTITY_TOO_LARGE = 413;
int SC_REQUEST_URI_TOO_LONG = 414;
int SC_UNSUPPORTED_MEDIA_TYPE = 415;
int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
int SC_EXPECTATION_FAILED = 417;
int SC_INTERNAL_SERVER_ERROR = 500;
int SC_NOT_IMPLEMENTED = 501;
int SC_BAD_GATEWAY = 502;
int SC_SERVICE_UNAVAILABLE = 503;
int SC_GATEWAY_TIMEOUT = 504;
int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
}
当我们拿到响应的输出流后可以将我们的提示信息写成json格式添加到里面,返回给前端读取展示,然后将输出刷新到内存中就大功告成了.
接下来我们需要在SecurityConfig中配置
@Override
protected void configure(HttpSecurity http) throws Exception {
@Resource
private MyAccessDeniedHandler myAccessDeniedHandler;
//首先,使用表单进行验证
http.formLogin()
//当有前端action的值为login的时候视为认证请求,会取调用认证方法
.loginProcessingUrl("/login")
//指定默认的认证页面
.loginPage("/login.html")
//认证成功后跳转到默认的页面
// .successHandler(new MyAuthenticationSuccessHandler("index.html"))
.successForwardUrl("/toIndex")
//认证失败后进行跳转到默认的错误页面
// .failureHandler(new MyAuthenticationFailureHandler("error.html"));
.failureForwardUrl("/toError");
//资源访问授权认证
http.authorizeRequests()
//指定的资源不需认证可进行访问
.antMatchers("/login.html").access("permitAll")
//按照权限来判断访问权限
// .antMatchers("/index.html").hasAnyAuthority("admin", "normal")
//
// //按照角色来判断访问权限
// .antMatchers("/index.html").hasAnyRole("admin")
//
// //按照IP地址来判断访问权限,通常用在后台管理页面,而且localhost和127.0.0.1不一样!!
// .antMatchers("/index.html").hasIpAddress("127.0.0.1")
.antMatchers("/error.html").permitAll()
/**
* 使用antMatchers同时也可以对多个静态资源进行放行,通常对js和css文件还有图片资源进行放行
* 可以使用*表示匹配任意字符
* **表示任意目录或文件
*/
.antMatchers("/js/**", "/css/**").permitAll()
//表示所有请求都需认证
.anyRequest()
.authenticated();
//关闭防火墙
http.csrf().disable();
//自定义异常响应信息
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
将我们自定义响应异常的类注入进来,然后在配置中使用我们自定义的异常类进行对应异常的处理.
在登录的时候通常都会有一个记住密码等等的功能,其实就是用来维持用户登录会话的.当启用后我们会将用户登录后的User信息存入到数据库,当用户在维持世界内进行访问时不再需要进行认证.
1.User持久化
//该bean创建负责持久化
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
//放入需要的datasource
jdbcTokenRepository.setDataSource(dataSource);
//第一次启动自动创建表
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
这个bean负责对需要使用记住我功能的用户进行持久化,将认证过后的User持久化到数据库中维持会话.
然后将这个类注入到config中,进行使用
同时还需要将我们自定义的认证方法注入进来,用来拿到我们的User,然后将User持久化.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAccessDeniedHandler myAccessDeniedHandler;
@Resource
private UserDetailServiceImp userDetailServiceImp;
@Autowired
private DataSource dataSource;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
//注册一个加密器
@Bean
public PasswordEncoder getpw() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//首先,使用表单进行验证
http.formLogin()
//当有前端action的值为login的时候视为认证请求,会取调用认证方法
.loginProcessingUrl("/login")
//指定默认的认证页面
.loginPage("/login.html")
//认证成功后跳转到默认的页面
// .successHandler(new MyAuthenticationSuccessHandler("index.html"))
.successForwardUrl("/toIndex")
//认证失败后进行跳转到默认的错误页面
// .failureHandler(new MyAuthenticationFailureHandler("error.html"));
.failureForwardUrl("/toError");
//资源访问授权认证
http.authorizeRequests()
//指定的资源不需认证可进行访问
.antMatchers("/login.html").access("permitAll")
//按照权限来判断访问权限
// .antMatchers("/index.html").hasAnyAuthority("admin", "normal")
//
// //按照角色来判断访问权限
// .antMatchers("/index.html").hasAnyRole("admin")
//
// //按照IP地址来判断访问权限,通常用在后台管理页面,而且localhost和127.0.0.1不一样!!
// .antMatchers("/index.html").hasIpAddress("127.0.0.1")
.antMatchers("/error.html").permitAll()
/**
* 使用antMatchers同时也可以对多个静态资源进行放行,通常对js和css文件还有图片资源进行放行
* 可以使用*表示匹配任意字符
* **表示任意目录或文件
*/
.antMatchers("/js/**", "/css/**").permitAll()
//表示所有请求都需认证
.anyRequest()
.authenticated();
//关闭防火墙
http.csrf().disable();
//自定义异常响应信息
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
//记住我功能
http.rememberMe()
//设置持久化时间,秒为单位,默认2星期
.tokenValiditySeconds(60)
.userDetailsService(userDetailServiceImp)
.tokenRepository(persistentTokenRepository);
//退出登录
http.logout()
.logoutSuccessUrl("/login.html");
这样用户在登录后指定的时间内不需要再进行认证就可以登录,同时可以指定维持时间,默认两星期,最后就是我们的退出登录,退出后将返回指定的页面,我这里设定的是退出后返回登录页面.
另外就是csrf问题,正常情况可能会打开,但是我们测试是关闭的.
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
分析的话可以看看这个贴
利用spring-security解决CSRF问题_I,Frankenstein的博客-CSDN博客_csrf spring从配置到原理,一步步讲解如何利用Spring的security框架来处理scrf问题。https://blog.csdn.net/u013185616/article/details/70446392?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165007837416780271560258%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165007837416780271560258&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-70446392.142%5Ev9%5Econtrol,157%5Ev4%5Enew_style&utm_term=scurity%E7%9A%84csrf&spm=1018.2226.3001.4187
而通常都是拦截post请求,并且我们提交表单都是使用的post,所以post请求必须带上csrf的token才能进行访问,但是我前端很菜,所以暂时不去探索了,记录一下以后再去填坑.