本文正在参与「金石方案 . 分割6万现金大奖」

Spring Security系列文章

  • 认证与授权之Cookie、Session、Token、JWT
  • 根据Session的认证与授权实践
  • Spring Security入门学习

认识Spring Security

Spring Security 是为根据 Spring 的运用程序供给声明式安全保护的安全性结构。Spring Security 供给了完好的安全性解决方案,它能够在 Web 恳求等级和办法调用等级处理身份认证和授权。由于根据 Spring 结构,所以 Spring Security 充分利用了依靠注入(dependency injection, DI)和面向切面的技能。

中心功用

对于一个权限管理结构而言,无论是 Shiro 还是 Spring Security,最最中心的功用,无非便是两方面:

  • 认证
  • 授权

通俗点说,认证便是咱们常说的登录,授权便是权限辨别,看看恳求是否具备相应的权限。

认证(Authentication)

Spring Security 支撑多种不同的认证办法,这些认证办法有的是 Spring Security 自己供给的认证功用,有的是第三方标准组织制订的,主要有如下一些:

一些比较常见的认证办法:

  • HTTP BASIC authentication headers:根据IETF RFC 标准。
  • HTTP Digest authentication headers:根据IETF RFC 标准。
  • HTTP X.509 client certificate exchange:根据IETF RFC 标准。
  • LDAP:跨渠道身份验证。
  • Form-based authentication:根据表单的身份验证。
  • Run-as authentication:用户用户暂时以某一个身份登录。
  • OpenID authentication:去中心化认证。

除了这些常见的认证办法之外,一些比较冷门的认证办法,Spring Security 也供给了支撑。

  • Jasig Central Authentication Service:单点登录。
  • Automatic “remember-me” authentication:记住我登录(答应一些非敏感操作)。
  • Anonymous authentication:匿名登录。
  • ……

作为一个敞开的渠道,Spring Security 供给的认证机制不仅仅是上面这些。假如上面这些认证机制仍然无法满意你的需求,咱们也能够自己定制认证逻辑。当咱们需求和一些“老寒酸”的系统进行集成时,自定义认证逻辑就显得非常重要了。

授权(Authorization)

无论采用了上面哪种认证办法,都不影响在 Spring Security 中运用授权功用。Spring Security 支撑根据 URL 的恳求授权、支撑办法拜访授权、支撑 SpEL 拜访操控、支撑域目标安全(ACL),一起也支撑动态权限装备、支撑 RBAC 权限模型等,总之,咱们常见的权限管理需求,Spring Security 根本上都是支撑的。

项目实践

创立 maven 工程

项目依靠如下:

<dependencies>
  <!-- 以下是>spring boot依靠-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- 以下是>spring security依靠-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>
</dependencies>

供给一个简略的测验接口,如下:

@RestController
public class HelloController {
  @GetMapping("/hello")
  public String hello() {
    return "hello,hresh";
  }
  @GetMapping("/hresh")
  public String sayHello() {
    return "hello,world";
  }
}

再创立一个发动类,如下:

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

在 Spring Security 中,默许状况下,只要添加了依靠,咱们项目的一切接口就现已被通通保护起来了,现在发动项目,拜访 /hello 接口,就需求登录之后才干够拜访,登录的用户名是 user,暗码则是随机生成的,在项目的发动日志中,如下所示:

Using generated security password: 21596f81-e185-4b6a-a8ff-1b21e2a60c6f

咱们测验拜访 /hello 接口,由于该接口被 Spring Security 保护起来了,重定向到 /login 接口,如下图所示:

Spring Security入门学习

输入账号和暗码后,即可拜访 /hello 接口。

那么怎么自定义登录用户信息呢?以及 Spring Security 怎么知道咱们想要支撑根据表单的身份验证?

认证

启用web安全性功用

Spring Security 供给了用户名暗码登录、退出、会话管理等认证功用,只需求装备即可运用。

在 Spring Security 5.7版别之前,或许 SpringBoot2.7 之前,咱们都是承继 WebSecurityConfigurerAdapter 来装备。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  //定义用户信息服务(查询用户信息)
  @Bean
  public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    return manager;
  }
  //暗码编码器,不加密,字符串直接比较
  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
  @Override
  public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/hello");
  }
  //安全阻拦机制(最重要)
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
      .anyRequest().authenticated()
      .and()
      .formLogin()
      .and()
      .httpBasic();
  }
}

Spring Security 供给了这种链式的办法调用。上面装备指定了认证办法为 HTTP Basic 登录,并且一切恳求都需求进行认证。

这里有一点需求注意,我没并没有在 Spring Security 装备类上运用@EnableWebSecurity 注解。这是由于在非 Spring Boot 的 Spring Web MVC 运用中,注解@EnableWebSecurity 需求开发人员自己引入以启用 Web 安全。而在根据 Spring Boot 的 Spring Web MVC 运用中,开发人员没有必要再次引用该注解,Spring Boot 的自动装备机制 WebSecurityEnablerConfiguration 现已引入了该注解,如下所示:

package org.springframework.boot.autoconfigure.security.servlet;
// 省略 imports 行
@Configuration(
  proxyBeanMethods = false
)
@ConditionalOnMissingBean(
  name = {"springSecurityFilterChain"}
)
@ConditionalOnClass({EnableWebSecurity.class})
@ConditionalOnWebApplication(
  type = Type.SERVLET
)
@EnableWebSecurity
class WebSecurityEnablerConfiguration {
  WebSecurityEnablerConfiguration() {
  }
}

实践上,一个 Spring Web 运用中,WebSecurityConfigurerAdapter 可能有多个 , @EnableWebSecurity 能够不用在任何一个WebSecurityConfigurerAdapter 上,能够用在每个 WebSecurityConfigurerAdapter 上,也能够只用在某一个WebSecurityConfigurerAdapter 上。多处运用@EnableWebSecurity 注解并不会导致问题,其终究运行时作用跟运用@EnableWebSecurity 一次作用是相同的。

在 userDetailsService()办法中,咱们回来了一个 UserDetailsService 给 Spring 容器,Spring Security 会运用它来获取用户信息。咱们暂时运用 InMemoryUserDetailsManager 完成类,并在其间分别创立了zhangsan、lisi两个用户,并设置暗码和权限。

configure(HttpSecurity http)办法中进入如下装备:

  • 保证对咱们的运用程序的任何恳求都要求用户进行身份验证
  • 答运用户运用根据表单的登录进行身份验证
  • 答运用户运用HTTP根本身份验证进行身份验证

注意上述还有一个 passwordEncoder()办法,在 IDEA 中会提示 NoOpPasswordEncoder 已过期。这是由于 Spring Security 5对 PasswordEncoder 做了相关的重构,原先默许装备的 PlainTextPasswordEncoder(明文暗码)被移除了,想要做到明文存储暗码,只能运用一个过期的类来过渡。

//加入
//已过期
@Bean
PasswordEncoder passwordEncoder(){
    return NoOpPasswordEncoder.getInstance();
}

Spring Security 供给了多品种来进行暗码编码,并作为了相关装备的默许装备,只不过没有暴露为全局的 Bean。在实践运用中运用明文校验暗码肯定是存在风险的,NoOpPasswordEncoder 只能存在于 demo 中。

//实践运用
@Bean
PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
//加密办法与对应的类
bcrypt - BCryptPasswordEncoder (Also used for encoding)
ldap - LdapShaPasswordEncoder
MD4 - Md4PasswordEncoder
MD5 - new MessageDigestPasswordEncoder("MD5")
noop - NoOpPasswordEncoder
pbkdf2 - Pbkdf2PasswordEncoder
scrypt - SCryptPasswordEncoder
SHA-1 - new MessageDigestPasswordEncoder("SHA-1")
SHA-256 - new MessageDigestPasswordEncoder("SHA-256")
sha256 - StandardPasswordEncoder

可是在 Spring Security 5.7版别之后(包含5.7版别),或许 SpringBoot2.7 之后,WebSecurityConfigurerAdapter 就过期了,虽然能够持续运用,但看着比较别扭。

看 5.7版别官方文档是怎么解释的:

Spring Security入门学习

以前咱们自定义类承继自 WebSecurityConfigurerAdapter 来装备咱们的 Spring Security,咱们主要是装备两个东西:

  • configure(HttpSecurity)
  • configure(WebSecurity)

前者主要是装备 Spring Security 中的过滤器链,后者则主要是装备一些途径放行规矩。

现在在 WebSecurityConfigurerAdapter 的注释中,人家现已把意思说的很理解了:

  • 今后假如想要装备过滤器链,能够经过自定义 SecurityFilterChain Bean 来完成。
  • 今后假如想要装备 WebSecurity,能够经过 WebSecurityCustomizer Bean 来完成。

咱们对上文中的 SecurityConfig 文件做一下改动,试试新版中该怎么装备。

@Configuration
public class SecurityConfig {
  @Bean
  public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    return manager;
  }
  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
  @Bean
  WebSecurityCustomizer webSecurityCustomizer() {
    return web -> web.ignoring().antMatchers("/hello");
  }
  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .permitAll()
        .and()
        .csrf().disable();
    return http.build();
  }
}

此时重启项目,你会发现 /hello 也是能够直接拜访的,便是由于这个途径不经过任何过滤器。

个人觉得新写法愈加直观,能够清楚的看到 SecurityFilterChain 是关于过滤器链装备的,与咱们理论知识提到的过滤器知识是一致的。

测验

拜访 http://localhost:8086/hello,能够直接看到页面内容,不需求输入账号暗码。

拜访 http://localhost:8086/hresh,则需求账号暗码,即咱们装备的 zhangsan 和 lisi 用户。

在测验过程中,你可能会发现这样几个问题:

1、直接拜访 http://localhost:8086,默许会跳转到 /login 页面,该装备坐落 UsernamePasswordAuthenticationFilter 类文件中,假如你想自定义登录页面,能够这样修正:

    http.authorizeRequests()
        .anyRequest().authenticated()
        .and()
        .formLogin()
//        .loginPage("/login.html")	
        .loginProcessingUrl("/login")

2、表单登录时,账号暗码默许字段为 username 和 password。

3、按理来说,登录成功之后是跳到/页面,失利跳转到登录页,但由于咱们这是 SpringBoot 项目,咱们能够让它登录成功时回来json数据,而不是重定向到某个页面。默许状况下,账号暗码输入过错会自动回来登录页面,所以此处咱们就不处理失利的状况。

@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
  private static ObjectMapper objectMapper = new ObjectMapper();
  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException {
    response.setContentType("application/json;charset=utf-8");
    response.getWriter().write(objectMapper.writeValueAsString("登录成功"));
  }
}

接着修正 securityFilterChain()办法

http.authorizeRequests()
  .anyRequest().authenticated()
  .and()
  .formLogin()
  .successHandler(myAuthenticationSuccessHandler)
  .and()
  .csrf().disable();

再次重启项目,在登录页面输入账号暗码后,回来成果如下所示:

Spring Security入门学习

4、自定义登录页面,在 resource 目录下新建 static 目录,里边添加 login.html 文件,暂时未添加款式

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>登录</title>
</head>
<body>
<form action="/doLogin" method="post">
  <div class="input">
    <label for="name">用户名</label>
    <input type="text" name="name" id="name">
    <span class="spin"></span>
  </div>
  <div class="input">
    <label for="pass">暗码</label>
    <input type="password" name="passwd" id="pass">
    <span class="spin"></span>
  </div>
  <div class="button login">
    <button type="submit">
      <span>登录</span>
      <i class="fa fa-check"></i>
    </button>
  </div>
</form>
</body>
</html>

修正 securityFilterChain()办法

http.authorizeRequests()  //表明开启权限装备
  .antMatchers("/login.html").permitAll()
  .anyRequest().authenticated() //表明一切的恳求都要经过认证之后才干拜访
  .and()  // 链式编程写法
  .formLogin()  //开启表单登录装备
  .loginPage("/login.html") // 装备登录页面地址
  .loginProcessingUrl("/doLogin")
  .permitAll()
  .and()
  .csrf().disable();

重启项目后, 再次拜访 http://localhost:8086/,会重定向到 http://localhost:8086/login.html。

最后,总结一下 HttpSecurity 的装备,示例如下:

    http.authorizeRequests()  //表明开启权限装备
        .anyRequest().authenticated() //表明一切的恳求都要经过认证之后才干拜访
        .and()  // 链式编程写法
        .formLogin()  //开启表单登录装备
        .loginPage("/login.html") // 装备自定义登录页面地址
        .loginProcessingUrl("/login") //装备登录接口地址
//        .defaultSuccessUrl()  //登录成功后的跳转页面
//        .failureUrl() //登录失利后的跳转页面
//        .usernameParameter("username")  //登录用户名的参数称号
//        .passwordParameter("password")  // 登录暗码的参数称号
//        .successHandler(
//            myAuthenticationSuccessHandler) //前后端别离的状况,并不想经过defaultSuccessUrl进行页面跳转,只需求回来一个json数据来告知前端
//        .failureHandler(myAuthenticationFailureHandler) // 同理,替代failureUrl
//        .permitAll()
        .and()
				.csrf().disable();// 禁用CSRF防护功用,测验能够先关闭

表单验证时,loginPage 与 loginProcessingUrl 区别:

  • loginPage 装备自定义登录页面地址,loginProcessingUrl 默许与表单 action 地址一致;
  • 假如只装备 loginPage 而不装备 loginProcessingUrl,那么 loginProcessingUrl 默许便是 loginPage;
  • 假如只装备 loginProcessUrl,就会用不了自定义登陆页面,Security 会运用自带的默许登陆页面;

假如 loginProcessingUrl 默许与表单 action 地址不一致,那么它需求指向一个有用的地址,比如说 /doLogin.html,这要求咱们在 static 目录下创立一个 doLogin.html 页面,此外,还需求在 controller 文件中添加如下办法:

  @PostMapping("/doLogin")
  public String doLogin() {
    return "我登录成功了";
  }

可是登录成功后并不会显现 doLogin.html 页面的内容,而是显现 /doLogin 的回来成果。同理,不装备 loginProcessingUrl,那么 loginProcessingUrl 默许便是 loginPage,即 loginProcessingUrl=login.html,与 doLogin.html 作用相同。

另外再介绍一下 Spring Security 中 defaultSuccessUrlsuccessForwardUrl 的区别:

假定在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index,那么存在两种状况:

  • ① 浏览器中输入的是登录地址,登录成功后,则直接跳转到 /index
  • ② 假如浏览器中输入了其他地址,例如 http://localhost:8080/elseUrl,若登录成功,就不会跳转到 /index,而是来到 /elseUrl 页面。

defaultSuccessUrl 便是说,它会默许跳转到 Referer 来历页面,假如 Referer 为空,没有来历页,则跳转到默许设置的页面。

successForwardUrl 表明不论 Referer 从何而来,登录成功后一概跳转到指定的地址。

认证办法挑选

在 WebSecurityConfigurerAdapter 类中有许多 configure()办法,除了上文提到的 HttpSecurity 和 WebSecurity 参数,还有一个 AuthenticationManagerBuilder 参数,源码如下:

protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  this.disableLocalConfigureAuthenticationBldr = true;
}
protected AuthenticationManager authenticationManager() throws Exception {
  if (!this.authenticationManagerInitialized) {
    this.configure(this.localConfigureAuthenticationBldr);
    if (this.disableLocalConfigureAuthenticationBldr) {
      this.authenticationManager = this.authenticationConfiguration.getAuthenticationManager();
    } else {
      this.authenticationManager = (AuthenticationManager)this.localConfigureAuthenticationBldr.build();
    }
    this.authenticationManagerInitialized = true;
  }
  return this.authenticationManager;
}

该类用于设置各种用户想用的认证办法,设置用户认证数据库查询服务 UserDetailsService 类以及添加自定义 AuthenticationProvider 类实例等

Spring Security 为装备用户存储供给了多个可选解决方案,包含:

  • 根据内存的用户存储
  • 根据 JDBC 的用户存储
  • 以 LDAP 作为后端的用户存储
  • 自定义用户概况服务

关于这四种办法就不详细介绍了,能够重点重视方案二和方案四,而在本项目中,咱们直接在 SecurityConfig 中重写 userDetailsService 办法,并将 UserDetailsService 目标注入到 Spring 容器中。

授权

1、首先在 HelloController 中添加 r1 和 r2 资源。

@GetMapping(value = "/r/r1")
public String r1() {
  return " 拜访资源1";
}
@GetMapping(value = "/r/r2")
public String r2() {
  return " 拜访资源2";
}

2、修正 SecurityConfig 文件中的 securityFilterChain()办法

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  http.authorizeRequests()
    .antMatchers("/r/r1").hasAuthority("p1")
    .antMatchers("/r/r2").hasAuthority("p2")
    .anyRequest().authenticated()
    .and()
    .formLogin()
    .successHandler(myAuthenticationSuccessHandler)
    .permitAll()
    .and()
    .csrf().disable();
  return http.build();
}

拜访 r1、r2 资源,需求对应的权限,而且其他接口则只需求认证,并不需求授权。

3、测验

拜访 http://localhost:8086 ,进入登录页面,输入正确的账号暗码,提交后页面回来“登录成功”,假如是 zhangsan,则能够拜访 r1资源,拜访 r2则会报错,咱们暂时未处理过错如下:

Spring Security入门学习

总结

关于 Spring Security 的学习先到这里,根本了解怎么运用即可,咱们持续后边的学习。

假如想要深入学习 Spring Security,引荐阅览《深入浅出Spring Security》,还包含配套的代码示例。

参考文献

Spring Security 中心过滤器链分析

spring security的认证和授权流程

Spring Security — Spring Boot中开启Spring Security