Spirng Security知识点整理

x33g5p2x  于2021-11-17 转载在 其他  
字(41.2k)|赞(0)|评价(0)|浏览(376)

案例

新建工程,引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>SpringSecurity</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

<!-- 父工程-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.4</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.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
      </dependency>

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-devtools</artifactId>
          <scope>runtime</scope>
          <optional>true</optional>
      </dependency>
      
  </dependencies>
    
</project>

创建启动项和controller层

@SpringBootApplication
public class main
{
    public static void main(String[] args) {
        SpringApplication.run(main.class,args);
    }
}
@RestController
public class HelloController
{
    @GetMapping("/hello")
    public String hello()
    {
        return "hello spring security";
    }
}

启动项目

启动日志会打印一个通过UUID随机生成的密码

访问controller,首先请求会被安全框架的aop机制拦截,要求使用用户名和密码验证登录

默认的用户名和密码为:

用户名: user

密码: 日志打印生成的uuid

自定义用户名和密码

配置文件中设置用户名和密码

spring:
  security:
    user:
      name: 大忽悠
      password: 123456

对应的绑定配置文件的类,如下:

@ConfigurationProperties(
    prefix = "spring.security"
)
public class SecurityProperties {
    public static final int BASIC_AUTH_ORDER = 2147483642;
    public static final int IGNORED_ORDER = -2147483648;
    public static final int DEFAULT_FILTER_ORDER = -100;
    private final SecurityProperties.Filter filter = new SecurityProperties.Filter();
    private final SecurityProperties.User user = new SecurityProperties.User();

    public SecurityProperties() {
    }

    public SecurityProperties.User getUser() {
        return this.user;
    }

    public SecurityProperties.Filter getFilter() {
        return this.filter;
    }

    public static class User {
        private String name = "user";
        private String password = UUID.randomUUID().toString();
        private List<String> roles = new ArrayList();
        private boolean passwordGenerated = true;

        public User() {
        }

        public String getName() {
            return this.name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getPassword() {
            return this.password;
        }

        public void setPassword(String password) {
            if (StringUtils.hasLength(password)) {
                this.passwordGenerated = false;
                this.password = password;
            }
        }

        public List<String> getRoles() {
            return this.roles;
        }

        public void setRoles(List<String> roles) {
            this.roles = new ArrayList(roles);
        }

        public boolean isPasswordGenerated() {
            return this.passwordGenerated;
        }
    }

    public static class Filter {
        private int order = -100;
        private Set<DispatcherType> dispatcherTypes;

        public Filter() {
            this.dispatcherTypes = new HashSet(Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
        }

        public int getOrder() {
            return this.order;
        }

        public void setOrder(int order) {
            this.order = order;
        }

        public Set<DispatcherType> getDispatcherTypes() {
            return this.dispatcherTypes;
        }

        public void setDispatcherTypes(Set<DispatcherType> dispatcherTypes) {
            this.dispatcherTypes = dispatcherTypes;
        }
    }
}

关闭验证功能

主配置类中排除安全框架的配置

//排除security的配置,不启用
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class main
{
    public static void main(String[] args) {
        SpringApplication.run(main.class,args);
    }
}

默认用户认证模块涉及到的三个类

UserDetailsService

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

返回值: UserDetails

返回值 UserDetails 是一个接口,定义如下

public interface UserDetails extends Serializable {
//获取用户权限
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();
//账号是否未过期
    boolean isAccountNonExpired();
//账号是否未被锁定
    boolean isAccountNonLocked();
//凭证是否未过期,凭证就是密码
    boolean isCredentialsNonExpired();
//用户是否启用状态
    boolean isEnabled();
}

UserDetails的实现类之:User

要想返回 UserDetails的实例就只能返回接口的实现类。SpringSecurity 中提供了如下的实例。对于我们只需要使用里面的 User类即可。注意 User 的全限定路径是:

org.springframework.security.core.userdetails.User此处经常和系统中自己开发的 User 类弄混。

在 User 类中提供了很多方法和属性。

public class User implements UserDetails, CredentialsContainer {
    private static final long serialVersionUID = 550L;
    private static final Log logger = LogFactory.getLog(User.class);
    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;
    .....

}

其中构造方法有两个,调用其中任何一个都可以实例化

UserDetails实现类 User类的实例。而三个参数的构造方法实际上也是调用 7 个参数的构造方法。

  • username:用户名
  • password:密码
  • authorities:用户具有的权限。此处不允许为 null

此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security 会根据 User 中的 password和客户端传递过来的 password进行比较。如果相同则表示认证通过,如果不相同表示认证失败。

authorities里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建authorities 集合对象的。参数是一个字符串,多个权限使用逗号分隔。

方法参数

方法参数表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无法接收。

异常

UsernameNotFoundException用户名没有发现异常。在loadUserByUsername中是需要通过自己的逻辑从数据库中取值的。如果通过用户名没有查询到对应的数据,应该抛出UsernameNotFoundException,系统就知道用户名没有查询到。

PasswordEncoder

Spring Security 要求容器中必须有PasswordEncoder实例。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder的bean对象。

接口介绍
  • encode():把参数按照特定的解析规则进行解析。
  • matches() :验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
  • upgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。
public interface PasswordEncoder {
//加密
    String encode(CharSequence var1);
//匹配
    boolean matches(CharSequence var1, String var2);
//二次加密
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

内置解析器介绍

BCryptPasswordEncoder 简介

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。

BCryptPasswordEncoder 是对 bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认 10.

代码演示

新建测试方法BCryptPasswordEncoder 用法。

@SpringBootTest(classes = main.class)
public class Test
{
  @org.junit.jupiter.api.Test
 public void test()
 {
     //创建解析器
     PasswordEncoder pw = new BCryptPasswordEncoder();
     //对密码加密
     String encode = pw.encode("123");
     System.out.println(encode);

     //判断原字符和加密后内容是否匹配
     boolean matches = pw.matches("123", encode);
     System.out.println("==================="+matches);
 }
}

自定义登录逻辑

当 进 行 自 定 义 登 录 逻 辑 时 需 要 用 到 之 前 讲 解 的UserDetailsServicePasswordEncoder。但是 Spring Security 要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder实例。所以不能直接 new 对象。

编写配置类

@Configuration
public class SecurityConfig {

   @Bean
   public PasswordEncoder getPw(){
      return new BCryptPasswordEncoder();
   }
}

自定义逻辑

在 Spring Security 中实现 UserDetailService 就表示为用户详情服务。在这个类中编写用户认证逻辑。

@Service
public class UserServiceImpl implements UserDetailsService {
   @Autowired
   private PasswordEncoder pw;

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      //1.查询数据库判断用户名是否存在,如果不存在抛出UsernameNotFoundException异常
      if (!"admin".equals(username)){
         throw new UsernameNotFoundException("用户名不存在");
      }
      //2.把查询出来的密码(注册时已经加密过)进行解析,或直接把密码放入构造方法中
      String password = pw.encode("123");
      return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
   }
}

其框架会把提交的密码使用我们定义的passwordEncode加密后调用**org.springframework.security.crypto.password.PasswordEncoder#matches**方法,与 返回的User中的密码进行比对。配对正常就验证通过。

查看效果

重启项目后,在浏览器中输入账号:admin,密码:123。后可以正确进入到 login.html 页面。

自定义登录页面

虽然 Spring Security 给我们提供了登录页面,但是对于实际项目中,大多喜欢使用自己的登录页面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简单,只需要修改配置类即可。

编写登录页面

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
    用户名:<input type="text" name="username" /><br/>
    密码:<input type="password" name="password" /><br/>
    <input type="submit" value="登录" />
</form>
</body>
</html>
修改配置类

修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapter,并重写 configure 方法。

  • successForwardUrl():登录成功后跳转地址
  • loginPage() :登录页面
  • loginProcessingUrl:登录页面表单提交地址,此地址可以不真实存在。
  • antMatchers():匹配内容
  • permitAll():允许
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
      //表单提交
      http.formLogin()
            //自定义登录页面
            .loginPage("/login.html")
            //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
            .loginProcessingUrl("/login")
            //登录成功后跳转页面,POST请求
            .successForwardUrl("/toMain");

      http.authorizeRequests()
            //login.html不需要被认证
            .antMatchers("/login.html").permitAll()
            //所有请求都必须被认证,必须登录后被访问
            .anyRequest().authenticated();

      //关闭csrf防护
      http.csrf().disable();
   }

   @Bean
   public PasswordEncoder getPw(){
      return new BCryptPasswordEncoder();
   }
}

/toMain请求不需要被放行,是因为tomain请求时登录成功后执行的,此时已经有了凭证,即登录成功过后执行的请求会被自动放行,而没登录之前的请求都会被拦截

编写控制器

@Controller
public class LoginController {
   //该方法不执行
   // @RequestMapping("/login")
   // public String login(){
   // System.out.println("登录方法");
   // return "main.html";
   // }

   /** * 成功后跳转页面 * @return */
   @RequestMapping("/toMain")
   public String toMain()
   {
      return "redirect:/main.html";
   }

}
知识点: controller层中return “redirect:/main.html”;可以重定向到在templates包外面的页面,默认return "main"是会进行视图解析器拼串,然后转发到templates包下面寻找对应的页面,没有找到就404

认证过程其他常用配置

失败跳转

表单处理中成功会跳转到一个地址,失败也可以跳转到一个地址。

编写页面error.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
操作失败,请重新登录 <a href= "/login.html">跳转</a>
</body>
</html>
修改表单配置

在配置方法中表单认证部分添加failureForwardUrl()方法,表示登录失败跳转的 url。此处依然是 POST 请求,所以跳转到可以接收 POST请求的控制器/error中。

//表单提交
http.formLogin()
      //自定义登录页面
      .loginPage("/login.html")
      //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
      .loginProcessingUrl("/login")
      //登录成功后跳转页面,POST请求
      .successForwardUrl("/toMain")
      //登录失败后跳转页面,POST请求
      .failureForwardUrl("/toError");
添加控制器的方法

在控制器类中添加控制器方法,方法映射路径/error。此处要注意:由于是 POST 请求访问/error。所以如果返回值直接转发到 error.html 中,即使有效果,控制台也会报警告,提示 error.html 不支持 POST 访问方式。

/** * 失败后跳转页面 * @return */
@RequestMapping("/toError")
public String toError(){
   return "redirect:/error.html";
}
设置error.html不需要认证
http.authorizeRequests()
      //login.html不需要被认证
      .antMatchers("/login.html").permitAll()
      //error.html不需要被认证
      .antMatchers("/error.html").permitAll()
      //所有请求都必须被认证,必须登录后被访问
      .anyRequest().authenticated();

设置请求账户和密码的参数名
源码简介

当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器。

  • usernamePasrameter:账户参数名
  • passwordParameter:密码参数名
  • postOnly=true:默认情况下只允许POST请求。

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
修改配置
//表单提交
http.formLogin()
      //自定义登录页面
      .loginPage("/login.html")
      //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
      .loginProcessingUrl("/login")
      //登录成功后跳转页面,POST请求
      .successForwardUrl("/toMain")
      //登录失败后跳转页面,POST请求
      .failureForwardUrl("/toError")
      .usernameParameter("myusername")
      .passwordParameter("mypassword");

修改login.html

<form action="/login" method="post">
    用户名:<input type="text" name="myusername" /><br/>
    密码:<input type="password" name="mypassword" /><br/>
    <input type="submit" value="登录" />
</form>

自定义登录成功处理器

源码分析

使用successForwardUrl()时表示成功后转发请求到地址。内部是通过 successHandler()方法进行控制成功后交给哪个类进行处理

public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
        this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
        return this;
    }

ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。由于是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。

public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private final String forwardUrl;

    public ForwardAuthenticationSuccessHandler(String forwardUrl) {
        //判断是否是合法的url
        Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
            return "'" + forwardUrl + "' is not a valid forward URL";
        });
        //合法url就保存
        this.forwardUrl = forwardUrl;
    }

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //然后转发
        request.getRequestDispatcher(this.forwardUrl).forward(request, response);
    }
}

当需要控制登录成功后去做一些事情时,可以进行自定义认证成功控制器。

代码实现

自定义类

新建类 MyAuthenticationSuccessHandler 编写如下:

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 {
      //Principal 主体,存放了登录用户的信息
      User user = (User) authentication.getPrincipal();
      System.out.println(user.getUsername());
      //处于安全的考虑,取出来的密码会显示null
      //输出null
      System.out.println(user.getPassword());
      System.out.println(user.getAuthorities());
      response.sendRedirect(url);
   }
}

修改配置项

使用 successHandler()方法设置成功后交给哪个对象进行处理

//表单提交
http.formLogin()
      //自定义登录页面
      .loginPage("/login.html")
      //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
      .loginProcessingUrl("/login")
      //登录成功后跳转页面,POST请求
      // .successForwardUrl("/toMain")
      //和successForwardUrl不能共存
      .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
      //登录失败后跳转页面,POST请求
      .failureForwardUrl("/toError")
      .usernameParameter("myusername")
      .passwordParameter("mypassword");
自定义登录失败处理器
源码分析

failureForwardUrl()内部调用的是failureHandler()方法

public FormLoginConfigurer<H> failureForwardUrl(String forwardUrl) {
        this.failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl));
        return this;
    }

ForwardAuthenticationFailureHandler 中也是一个请求转发,并在request 作用域中设置 SPRING_SECURITY_LAST_EXCEPTION的 key,内容为异常对象。

public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private final String forwardUrl;

    public ForwardAuthenticationFailureHandler(String forwardUrl) {
        Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
            return "'" + forwardUrl + "' is not a valid forward URL";
        });
        this.forwardUrl = forwardUrl;
    }

    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
        //转发到指定的url
        request.getRequestDispatcher(this.forwardUrl).forward(request, response);
    }
}
代码实现

新建控制器

新建 MyForwardAuthenticationFailureHandler 实现AuthenticationFailureHandler。在方法中添加重定向语句

public class MyForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {

   private String url;

   public MyForwardAuthenticationFailureHandler(String url) {
      this.url = url;
   }

   @Override
   public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
      response.sendRedirect(url);
   }
}

修改配置类

修改配置类中表单登录部分。设置失败时交给失败处理器进行操作。failureForwardUrlfailureHandler不可共存

//表单提交
http.formLogin()
      //自定义登录页面
      .loginPage("/login.html")
      //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
      .loginProcessingUrl("/login")
      //登录成功后跳转页面,POST请求
      // .successForwardUrl("/toMain")
      //和successForwardUrl不能共存
      .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
      //登录失败后跳转页面,POST请求
      // .failureForwardUrl("/toError")
      .failureHandler(new MyForwardAuthenticationFailureHandler("/error.html"))
      .usernameParameter("myusername")
      .passwordParameter("mypassword");

访问控制url匹配

在前面讲解了认证中所有常用配置,主要是对http.formLogin()进行操作。而在配置类中 http.authorizeRequests()主要是对url进行控制,也就是我们所说的授权(访问控制)。http.authorizeRequests()也支持连缀写法,总体公式为:

  • url 匹配规则.权限控制方法

通过上面的公式可以有很多 url 匹配规则和很多权限控制方法。这些内容进行各种组合就形成了Spring Security中的授权。

在所有匹配规则中取所有规则的交集。配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该放到后面。

anyRequest()

在之前认证过程中我们就已经使用过 anyRequest(),表示匹配所有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证。

.anyRequest().authenticated();

配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该放到后面。

http.authorizeRequests()
              .anyRequest().authenticated()
              //login.html不需要被认证
              .antMatchers("/login.html").permitAll()
              //error.html不需要被认证
              .antMatchers("/error.html").permitAll();
              //所有请求都必须被认证,必须登录后被访问
              //.anyRequest().authenticated();

anyrequest要放在后面,拦截所有请求,前面放的是需要放行的请求

antMatcher()

方法定义如下

public C antMatchers(String... antPatterns)

数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。

规则如下:

  • ?: 匹配一个字符
  • *:匹配 0 个或多个字符
  • ** :匹配 0 个或多个目录

在实际项目中经常需要放行所有静态资源,下面演示放行 js 文件夹下所有脚本文件。

.antMatchers("/js/**","/css/**").permitAll()

还有一种配置方式是只要是.js 文件都放行

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

regexMatchers()

介绍

使用正则表达式进行匹配。和 antMatchers()主要的区别就是参数,antMatchers()参数是 ant 表达式,regexMatchers()参数是正则表达式。

演示所有以.js 结尾的文件都被放行。

.regexMatchers( ".+[.]js").permitAll()

两个参数时使用方式----针对regexMatchers和antMatcher

无论是antMatchers()还是regexMatchers()都具有两个参数的方法,其中第一个参数都是 HttpMethod,表示请求方式,当设置了HttpMethod后表示只有设定的特定的请求方式才执行对应的权限设置。

枚举类型 HttpMethod内置属性如下:

public enum HttpMethod {
    GET,
    HEAD,
    POST,
    PUT,
    PATCH,
    DELETE,
    OPTIONS,
    TRACE;
    ....
}
.antMatchers(HttpMethod.POST,"/error.html").permitAll()

mvcMatchers()

mvcMatchers()适用于配置了 servletPath 的情况。

servletPath就是所有的 URL 的统一前缀。在 SpringBoot 整合SpringMVC 的项目中可以在 application.properties 中添加下面内容设置 ServletPath

spring.mvc.servlet.path=/yjxxt

在 Spring Security 的配置类中配置.servletPath()是 mvcMatchers()返回值特有的方法,antMatchers()和 regexMatchers()没有这个方法。在servletPath()中配置了servletPath后,mvcMatchers()直接写 SpringMVC 中@RequestMapping()中设置的路径即可。

.mvcMatchers("/demo").servletPath("/yjxxt").permitAll()

如果不习惯使用 mvcMatchers()也可以使用 antMatchers(),下面代码和上面代码是等效

.antMatchers("/yjxxt/demo").permitAll()

msp—servlet-path和context-path

MSP是对整个项目的地址增加前缀,也就是

http://localhost:8080/MSP前缀/你的具体的控制器接口

MVC-Servlet-Path的配置:

application.yml配置

spring:
  # spring.mvc.servlet.path
  mvc:
    servlet:
      path: /xxx

application.properties配置

spring.mvc.servlet.path = /xxx

除了MVC之外,其实SpringBoot还允许设置项目根路径:

server:
  servlet:
    context-path: /ctx-path

那么是不是应该连接起来的情况:

http://localhost:8080/ctx-path/MSP前缀/你的具体的控制器接口

AntMatcher和MvcMatcher之间的区别

  • antMatcher(String antPattern)-允许配置HttpSecurity仅在匹配提供的蚂蚁模式时被调用。
  • mvcMatcher(String mvcPattern)-仅允许在匹配提供的Spring MVC模式时调用HttpSecurity。

通常,mvcMatcher比antMatcher更安全。例如:

antMatchers("/secured") 仅匹配确切的 /secured URL
mvcMatchers("/secured") 匹配/secured以及/secured/,/secured.html,/secured.xyz
,因此更通用,还可以处理一些可能的配置错误。

mvcMatcher使用与Spring MVC用于匹配的规则相同(使用@RequestMapping注释时)。

如果Spring MVC无法处理当前请求,则将使用ant模式的合理默认值

内置访问控制方法

Spring Security 匹配了 URL 后调用了permitAll()表示不需要认证,随意访问。在 Spring Security 中提供了多种内置控制。

permitAll()

permitAll()表示所匹配的 URL 任何人都允许访问。

authenticated()

authenticated()表示所匹配的 URL 都需要被认证才能访问。

anonymous()

anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter 链中

denyAll()

denyAll()表示所匹配的 URL 都不允许被访问。

rememberMe()

被“remember me”的用户允许访问

fullyAuthenticated()

如果用户不是被 remember me 的,才可以访问

角色权限判断

除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求

hasAuthority(String)

判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。下图中 admin和normal 就是用户的权限。admin和normal 严格区分大小写。

在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。

.antMatchers("/main1.html").hasAuthority("admin")

hasAnyAuthority(String …)

如果用户具备给定权限中某一个,就允许访问。

下面代码中由于大小写和用户的权限不相同,所以用户无权访问

.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")

基于角色控制访问

hasRole(String)

如果用户具备给定角色就允许访问。否则出现 403。

参数取值来源于自定义登录逻辑 UserDetailsService实现类中创建 User 对象时给 User 赋予的授权。

在给用户赋予角色时角色需要以:ROLE_开头,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_是固定的字符开头。

使用 hasRole()时参数也只写 abc 即可。否则启动报错。

给用户赋予角色:

在配置类中直接写 abc 即可。

.antMatchers("/main1.html").hasRole("abc")

hasAnyRole(String …)

如果用户具备给定角色的任意一个,就允许被访问

基于ip控制访问

hasIpAddress(String)

如果请求是指定的 IP 就运行访问。

可以通过 request.getRemoteAddr()获取 ip 地址。

需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。

当浏览器中通过 localhost 进行访问时控制台打印的内容:

当浏览器中通过 127.0.0.1 访问时控制台打印的内容:

当浏览器中通过具体 ip 进行访问时控制台打印内容:

.antMatchers("/main1.html").hasIpAddress("127.0.0.1")

自定义403处理方案

使用 Spring Security 时经常会看见 403(无权限),默认情况下显示的效果如下:

而在实际项目中可能都是一个异步请求,显示上述效果对于用户就不是特别友好了。Spring Security 支持自定义权限受限。

新建类

新建类实现 AccessDeniedHandler

/** * @author zhoubin * @since 1.0.0 */
@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 out = response.getWriter();
      out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
      out.flush();
      out.close();
   }
}

修改配置类

配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。

myAccessDeniedHandler 是在配置类中进行自动注入的。

//异常处理
http.exceptionHandling()
      .accessDeniedHandler(myAccessDeniedHandler);

基于表达式的访问控制

access()方法使用

之前学习的登录用户权限判断实际上底层实现都是调用access(表达式)

可以通过access()实现和之前学习的权限控制完成相同的功能。

以 hasRole 和 和 permitAll 举例

使用自定义方法

虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自己自定义逻辑的情况。

判断登录用户是否具有访问当前 URL 权限。

新建接口及实现类

MyService.java

import org.springframework.security.core.Authentication;

import javax.servlet.http.HttpServletRequest;

public interface MyService {
   boolean hasPermission(HttpServletRequest request, Authentication authentication);
}

MyServiceImpl.java

@Component
public class MyServiceImpl implements MyService {

   @Override
   public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
      //取出主体
      Object obj = authentication.getPrincipal();
      //判断是否是UserDetails类或者其子实现类
      if (obj instanceof UserDetails){
         UserDetails userDetails = (UserDetails) obj;
         //取出当前主体所拥有的权限集合
         Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
         //以url作为当前主体的权限进行判断
         return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
      }
      return false;
   }
}

修改配置类

在 access 中通过@bean的id名方法(参数)的形式进行调用配置类中修改如下:

//url拦截
http.authorizeRequests()
      //login.html不需要被认证
      // .antMatchers("/login.html").permitAll()
      .antMatchers("/login.html").access("permitAll")
      // .antMatchers("/main.html").hasRole("abc")
      .antMatchers("/main.html").access("hasRole('abc')")
      //@bean的id名(方法参数1,方法参数2...)
      .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)")

自定义登录逻辑处理:

@Service
public class UserServiceImpl implements UserDetailsService {
   @Autowired
   private PasswordEncoder pw;

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      //1.查询数据库判断用户名是否存在,如果不存在抛出UsernameNotFoundException异常
      if (!"admin".equals(username)){
         throw new UsernameNotFoundException("用户名不存在");
      }
      //2.把查询出来的密码(注册时已经加密过)进行解析,或直接把密码放入构造方法中
      String password = pw.encode("123");
      return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,/main.html"));
   }
}

此时访问/main.html,用户拥有这个url的访问权限,因此可以访问

基于注解的访问控制

在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity进行开启后使用。

如果设置的条件允许,程序正常执行。如果不允许会报 500

这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。

访问所有请求前,都会先判断是否登录过,然后再对权限进行验证,如果没有权限访问,才会报出500错误

@Secured

@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。

开启注解,默认不启用注解配置

在 启 动 类 ( 也 可 以 在 配 置 类 等 能 够 扫 描 的 类 上 ) 上 添 加@EnableGlobalMethodSecurity(securedEnabled = true)

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecurityDemoApplication {

   public static void main(String[] args) {
      SpringApplication.run(SpringsecurityDemoApplication.class, args);
   }

}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({GlobalMethodSecuritySelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
    boolean prePostEnabled() default false;

    boolean securedEnabled() default false;

    boolean jsr250Enabled() default false;

    boolean proxyTargetClass() default false;

    AdviceMode mode() default AdviceMode.PROXY;

    int order() default 2147483647;
}

在控制器方法上添加@Secured 注解

/** * 成功后跳转页面 * @return */
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String toMain(){
   return "redirect:/main.html";
}

配置类

@Override
protected void configure(HttpSecurity http) throws Exception {
   //表单提交
   http.formLogin()
         //自定义登录页面
         .loginPage("/login.html")
         //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
         .loginProcessingUrl("/login")
         //登录成功后跳转页面,POST请求
         .successForwardUrl("/toMain")
         
   //url拦截
   http.authorizeRequests()
         //login.html不需要被认证
         .antMatchers("/login.html").permitAll()
         //所有请求都必须被认证,必须登录后被访问
         .anyRequest().authenticated();
   //关闭csrf防护
   http.csrf().disable();
}

@PreAuthorize/@PostAuthorize

  • @PreAuthorize表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
  • @PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用到。

开启注解

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringsecurityDemoApplication {

   public static void main(String[] args) {
      SpringApplication.run(SpringsecurityDemoApplication.class, args);
   }

}

添加@PreAuthorize

在控制器方法上添加@PreAuthorize,参数可以是任何 access()支持的表达式

/** * 成功后跳转页面 * @return */
@PreAuthorize("hasRole('ROLE_abc')")
@RequestMapping("/toMain")
public String toMain(){
   return "redirect:/main.html";
}

@PreAuthorize(“hasRole(‘ROLE_abc’)”),这里角色可以以ROLE_开头,也可以不以ROLE_开头,但是配置类不能以ROLE_开头

RememberMe功能实现

Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问

添加依赖

pring Security 实 现 Remember Me 功 能 时 底 层 实 现 依 赖Spring-JDBC,所以需要导入 Spring-JDBC。以后多使用 MyBatis 框架而很少直接导入 spring-jdbc,所以此处导入 mybatis 启动器同时还需要添加 MySQL 驱动

<!-- mybatis 依赖 -->
<dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
   <version>2.1.1</version>
</dependency>
<!-- mysql 数据库依赖 -->
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>8.0.18</version>
</dependency>

配置数据源

在 application.properties 中配置数据源。请确保数据库中已经存在security数据库

spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username= root
spring.datasource.password= root

编写配置

RememberMeConfig.java

@Configuration
public class RememberMeConfig {

   @Autowired
   private DataSource dataSource;

   @Bean
   public PersistentTokenRepository getPersistentTokenRepository(){
      JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
      jdbcTokenRepository.setDataSource(dataSource);
      //自动建表,第一次启动时需要,第二次启动时注释掉
      //如果不注释掉,那么多次启动,每一次都会执行一次建表语句,会报错
      jdbcTokenRepository.setCreateTableOnStartup(true);
      return jdbcTokenRepository;
   }

}

修改SecurityConfig.java

在SecurityConfig中添加RememberMeConfig和UserDetailsService实现类对象,并自动注入。

在 configure 中添加下面配置内容。

http.rememberMe()
      //登录逻辑交给哪个对象
      .userDetailsService(userService)
      // 持久层对象
      .tokenRepository(persistentTokenRepository);

在客户端页面添加复选框

在客户端登录页面中添加 remember-me 的复选框,只要用户勾选了复选框下次就不需要进行登录了。

<form action="/login" method="post">
    用户名:<input type="text" name="username" /><br/>
    密码:<input type="password" name="password" /><br/>
    <input type="checkbox" name="remember-me" value="true"/><br/>
    <input type="submit" value="登录" />
</form>

有效时间

默认情况下重启项目后登录状态失效了。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登录。

http.rememberMe()
      //失效时间,单位秒
      .tokenValiditySeconds(120)
      //登录逻辑交给哪个对象
      .userDetailsService(userService)
      // 持久层对象
      .tokenRepository(persistentTokenRepository);

每一次用户登录,通过token凭证,和登录的时间,当下一次用户访问时,取出数据库中上一次登录的时间,和失效时间比较,判断是否需要重新登录

Thymeleaf中SpringSecurity的使用

Spring Security 可以在一些视图技术中进行控制显示效果。例如:JSPThymeleaf。在非前后端分离且使用 Spring Boot 的项目中多使用 Thymeleaf作为视图展示技术。

Thymeleaf 对 Spring Security 的 支 持 都 放 在thymeleaf-extras-springsecurityX中,目前最新版本为 5。所以需要在项目中添加此 jar 包的依赖和 thymeleaf 的依赖。。

<!--thymeleaf springsecurity5 依赖-->
<dependency>
   <groupId>org.thymeleaf.extras</groupId>
   <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--thymeleaf依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在 html 页面中引入 thymeleaf 命名空间和 security 命名空间

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

获取属性

可以在html页面中通过sec:authentication=""获取

UsernamePasswordAuthenticationToken中所有 getXXX的内容,包含父类中的 getXXX的内容。

根据源码得出下面属性:

  • name:登录账号名称
  • principal:登录主体,在自定义登录逻辑中是 UserDetails
  • credentials:凭证
  • authorities:权限和角色
  • details:实际上是 WebAuthenticationDetails的实例。可以获取remoteAddress(客户端 ip)和 sessionId(当前 sessionId)
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
    private final Collection<GrantedAuthority> authorities;
    private Object details;
    private boolean authenticated = false;
....
}

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 550L;
    private final Object principal;
    private Object credentials;
....
}

public class WebAuthenticationDetails implements Serializable {
    private static final long serialVersionUID = 550L;
    private final String remoteAddress;
    private final String sessionId;
....
}

新建demo.html

在项目 resources 中新建 templates 文件夹,在 templates 中新建demo.html 页面

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登录账号:<span sec:authentication="name"></span><br/>
    登录账号:<span sec:authentication="principal.username"></span><br/>
    凭证:<span sec:authentication="credentials"></span><br/>
    权限和角色:<span sec:authentication="authorities"></span><br/>
    客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
    sessionId:<span sec:authentication="details.sessionId"></span><br/>
</body>
</html>

编写Controller

thymeleaf 页面需要控制转发,在控制器类中编写下面方法

@RequestMapping("/demo")
public String demo(){
   return "demo";
}

权限判断

设置用户角色和权限

设定用户具有 admin,/insert,/delete 权限 ROLE_abc 角色。

return new User(username,password, 
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_abc,/insert,/delete"));

控制页面显示效果

在页面中根据用户权限和角色判断页面中显示的内容

通过权限判断:
<button sec:authorize="hasAuthority('/insert')">新增</button>
<button sec:authorize="hasAuthority('/delete')">删除</button>
<button sec:authorize="hasAuthority('/update')">修改</button>
<button sec:authorize="hasAuthority('/select')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('abc')">新增</button>
<button sec:authorize="hasRole('abc')">删除</button>
<button sec:authorize="hasRole('abc')">修改</button>
<button sec:authorize="hasRole('abc')">查看</button>

退出登录

用户只需要向 Spring Security 项目中发送/logout退出请求即可。

实现退出非常简单,只要在页面中添加/logout 的超链接即可。

<a href="/logout">退出登录</a>

为了实现更好的效果,通常添加退出的配置。默认的退出 url 为/logout,退出成功后跳转到/login?logout

源码:

public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<LogoutConfigurer<H>, H> {
    private List<LogoutHandler> logoutHandlers = new ArrayList();
    //退出登录的处理器
    private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
   //退出成功后,跳转的url
    private String logoutSuccessUrl = "/login?logout";
    //退出成功后的处理器
    private LogoutSuccessHandler logoutSuccessHandler;
    //要退出登录需要发起的url---默认值如下
    private String logoutUrl = "/logout";
    //退出登录请求的url匹配器
    private RequestMatcher logoutRequestMatcher;
    private boolean permitAll;
    private boolean customLogoutSuccess;
    private LinkedHashMap<RequestMatcher, LogoutSuccessHandler> defaultLogoutSuccessHandlerMappings = new LinkedHashMap();
....
}

如果不希望使用默认值,可以通过下面的方法进行修改。

http.logout()
      .logoutUrl("/logout")
      .logoutSuccessUrl("/login.html");

logout其他常用配置源码解读

addLogoutHandler(LogoutHandler)

默认是 contextLogoutHandler

//创建默认的退出成功处理器
 private LogoutSuccessHandler createDefaultSuccessHandler() {
        SimpleUrlLogoutSuccessHandler urlLogoutHandler = new SimpleUrlLogoutSuccessHandler();
        //设置默认的退出成功后重定向的url
        urlLogoutHandler.setDefaultTargetUrl(this.logoutSuccessUrl);
        if (this.defaultLogoutSuccessHandlerMappings.isEmpty()) {
            return urlLogoutHandler;
        } else {
            DelegatingLogoutSuccessHandler successHandler = new DelegatingLogoutSuccessHandler(this.defaultLogoutSuccessHandlerMappings);
            //设置默认的退出成功处理器
            successHandler.setDefaultLogoutSuccessHandler(urlLogoutHandler);
            return successHandler;
        }
    }

SimpleUrlLogoutSuccessHandler

public class SimpleUrlLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {
    public SimpleUrlLogoutSuccessHandler() {
    }

    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
       //调用父类方法进行处理
        super.handle(request, response, authentication);
    }
}

AbstractAuthenticationTargetUrlRequestHandler 的handle方法:

protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String targetUrl = this.determineTargetUrl(request, response, authentication);
        if (response.isCommitted()) {
            this.logger.debug(LogMessage.format("Did not redirect to %s since response already committed.", targetUrl));
        } else {
        //退出后,重定向到指定的url
            this.redirectStrategy.sendRedirect(request, response, targetUrl);
        }
    }

LogoutFilter:专门来拦截/logout请求,然后使用处理器对当前登出请求进行处理

SecurityContextLogoutHandler : 处理器之一,还有上面介绍的处理器

public class SecurityContextLogoutHandler implements LogoutHandler {
    protected final Log logger = LogFactory.getLog(this.getClass());
    //是否销毁 HttpSession 对象,默认为 true
    private boolean invalidateHttpSession = true;
   //是否清除认证状态,默认为 true
    private boolean clearAuthentication = true;

    public SecurityContextLogoutHandler() {
    }

    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        Assert.notNull(request, "HttpServletRequest required");
        if (this.invalidateHttpSession) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                session.invalidate();
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug(LogMessage.format("Invalidated session %s", session.getId()));
                }
            }
        }

        if (this.clearAuthentication) {
            SecurityContext context = SecurityContextHolder.getContext();
            context.setAuthentication((Authentication)null);
        }

        SecurityContextHolder.clearContext();
    }
.....
}

处理完毕后,调用上面讲的退出成功处理器logoutSuccessHandler

也可以自己进行定义退出成功处理器。只要实现了LogoutSuccessHandler接口。与之前讲解的登录成功处理器和登录失败处理器极其类似。

SpringSecurity中的CSRF

从刚开始学习Spring Security时,在配置类中一直存在这样一行代码:http.csrf().disable();如果没有这行代码导致用户无法被认证。这行代码的含义是:关闭 csrf 防护。

从Spring Security 4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE方法进行防护。

什么是CSRF

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。

跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。

客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个 session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。

Spring Security中的CSRF

从 Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。

在默认配置下,即便已经登录了,页面中发起PATCH,POST,PUT和DELETE请求依然会被拒绝,并返回403,需要在请求接口的时候加入csrfToken才行。

案例

编写控制器方法,跳转到 templates 中 login.html 页面。

@RequestMapping("/showLogin")
public String showLogin(){
   return "login";
}

新建login.html

红色部分是必须存在的否则无法正常登录。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" >
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
    <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
    用户名:<input type="text" name="username" /><br/>
    密码:<input type="password" name="password" /><br/>
    <input type="submit" value="登录" />
</form>
</body>
</html>

修改配置类

在配置类中注释掉 CSRF 防护失效

//关闭csrf防护
// http.csrf().disable();

请求头,ajax发送token

如果你使用了freemarker之类的模板引擎或者jsp,针对表单提交,可以在表单中增加如下隐藏域:

<input  type = “hidden”  name = “${_csrf.parameterName}”  value = “${_csrf.token}” />

如果您使用的是JSON,则无法在HTTP参数中提交CSRF令牌。相反,您可以在HTTP头中提交令牌。一个典型的模式是将CSRF令牌包含在元标记中。下面显示了一个JSP示例:

<html> 
<head> 
    <meta  name = “_csrf” content = “${_csrf.token}” /> 
    <!-- 默认标题名称是X-CSRF-TOKEN  --> 
    <meta  name = “_csrf_header”  content = “${_csrf.headerName}” /> 
</ head>

然后,您可以将令牌包含在所有Ajax请求中。如果您使用jQuery,可以使用以下方法完成此操作:

var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$.ajax({
    url:url,
    type:'POST',
    async:false,
    dataType:'json',    //返回的数据格式:json/xml/html/script/jsonp/text
    beforeSend: function(xhr) {
        xhr.setRequestHeader(header, token);  //发送请求前将csrfToken设置到请求头中
    },
    success:function(data,textStatus,jqXHR){
    }
});

CSRF相关源码解析

自定义RequestMatcher的实现类CsrfSecurityRequestMatcher

这个类被用来自定义哪些请求是不需要进行拦截过滤的。如果配置csrf,所有http请求都被会CsrfFilter拦截,而CsrfFilter中有一个私有类DefaultRequiresCsrfMatcher。

private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
        private final HashSet<String> allowedMethods;
 
        private DefaultRequiresCsrfMatcher() {
            this.allowedMethods = new HashSet(Arrays.asList(new String[]{"GET", "HEAD", "TRACE", "OPTIONS"}));
        }
 
        public boolean matches(HttpServletRequest request) {
        //返回false表示需要验证token
            return !this.allowedMethods.contains(request.getMethod());
        }
    }

从这段源码可以发现,POST方法被排除在外了,也就是说只有GET|HEAD|TRACE|OPTIONS这4类方法会被放行,其它Method的http请求,都要验证_csrf的token是否正确,而通常post方式调用rest接口服务时,又没有_csrf的token,所以会导致我们的rest接口调用失败,我们需要自定义一个类对该类型接口进行放行。来看下我们自定义的过滤器:

public class CsrfSecurityRequestMatcher implements RequestMatcher {
    private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
    private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/rest/.*", null);
 
    @Override
    public boolean matches(HttpServletRequest request) {
        if(allowedMethods.matcher(request.getMethod()).matches()){
            return false;
        }
 
        return !unprotectedMatcher.matches(request);
    }
}

说明:一般我们定义的rest接口服务,都带上 /rest/ ,所以如果你的项目中不是使用的这种,或者项目中没有rest服务,这个类完全可以省略的。

post请求配置

一般我们的项目中都有一个通用的jsp文件,就是每个页面都会引用的,所以我们可以在通用文件中做如下配置:

<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
 
<script>
 
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $.ajaxSetup({
        beforeSend: function (xhr) {
            if(header && token ){
                xhr.setRequestHeader(header, token);
            }
        }}
    );
</script>

$.ajaxSetup的意思就是给我们所有的请求都加上这个header和token,或者放到form表单中。注意, _csrf这个要与spring security的配置文件中的配置相匹配,默认为_csrf。

源码解析—CsrfFilter的doFilterInternal方法

我们知道,既然配置了csrf,所有的http请求都会被CsrfFilter拦截到,所以看下CsrfFilter的源码就对原理一目了然了。这里我们只看具体过滤的方法即可:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
        if(missingToken) {//如果token为空,说明第一次访问,生成一个token对象
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
 
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
		//把token对象放到request中,注意这里key是csrfToken.getParameterName()= _csrf,所以我们页面上才那么写死。
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
		
		//这个macher就是我们在Spring配置文件中自定义的过滤器,也就是GET,HEAD, TRACE, OPTIONS和我们的rest都不处理
        if(!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
        } else {
            String actualToken = request.getHeader(csrfToken.getHeaderName());
            if(actualToken == null) {
                actualToken = request.getParameter(csrfToken.getParameterName());
            }
 
            if(!csrfToken.getToken().equals(actualToken)) {
                if(this.logger.isDebugEnabled()) {
                    this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
                }
 
                if(missingToken) {
                    this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
                } else {
                    this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
                }
 
            } else {
                filterChain.doFilter(request, response);
            }
        }
    }

从源码中可以看到,通过我们自定义的过滤器以外的post请求都需要进行token验证。

利用spring-security解决CSRF问题

相关文章

微信公众号

最新文章

更多

目录