hao同学的技术博客

  • 首页
  • Java
    • Java
    • JVM教程
    • Java面试
    • Java并发入门
    • Java并发进阶
  • 项目
    • 从零打造项目
  • Python
    • Python
    • Python爬虫
    • 算法
  • Java框架
    • Spring
    • SpringBoot
  • 前端
    • Angular
  • 其他
    • Linux
    • SQL
  • 随笔
分享技术,记录人生
一个痴迷于技术的厨艺爱好者
  1. 首页
  2. 从零打造项目
  3. 正文

Spring Security入门学习

2022年12月8日 110点热度 0人点赞 0条评论

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版本官方文档是如何解释的:

WebSecurityConfigurerAdapter介绍

以前我们自定义类继承自 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();

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

SpringSecurity表单登录成功

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 中 defaultSuccessUrl 和 successForwardUrl 的区别:

假定在 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则会报错,我们暂时未处理错误如下:

SpringSecurity无权访问

总结

关于 Spring Security 的学习先到这里,基本了解如何使用即可,我们继续后面的学习。

如果想要深入学习 Spring Security,推荐阅读《深入浅出Spring Security》,还包括配套的代码示例。

参考文献

Spring Security 核心过滤器链分析

spring security的认证和授权流程

Spring Security -- Spring Boot中开启Spring Security

本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可
标签: SpringBoot SpringSecurity
最后更新:2022年12月8日

hresh

这是一个专注于IT技术学习交流的个人技术博客网站,包括Java学习、Python爬虫、Web开发实践等领域,深耕Java领域,内容涵盖Java基础、Java并发编程、Java虚拟机、Java面试等核心知识点。

点赞
< 上一篇
下一篇 >

文章评论

取消回复

hresh

这是一个专注于IT技术学习交流的个人技术博客网站,包括Java学习、Python爬虫、Web开发实践等领域,深耕Java领域,内容涵盖Java基础、Java并发编程、Java虚拟机、Java面试等核心知识点。

文章目录
  • 认识Spring Security
    • 核心功能
  • 项目实践
    • 创建 maven 工程
    • 认证
    • 授权
  • 总结
  • 参考文献
最新 热点 随机
最新 热点 随机
后端必知:遵循Google Java规范并引入checkstyle检查 Spring Security结合Redis实现缓存功能 Spring Security结合JWT实现认证与授权 Spring Security自定义认证逻辑实现图片验证码登录 Spring Security进阶学习 Spring Security入门学习
Spring IoC之AbstractBeanFactory 关于计算机的一些知识 Python 中 with 语句的学习 GC调优基础以及初识jstat命令 基于Session的认证与授权实践 Java并发编程入门学习之线程与锁

COPYRIGHT © 2022 hao同学的技术博客. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

鄂ICP备2022007381号

鄂公网安备 42010302002449号