Spring Security 最佳实践,看了必懂(保姆级教程!!!)

目录

一、认识Spring Security

1.1 核心功能

1.1.1 认证(Authentication)

1.1.2 授权(Authorization)

二、项目实践

2.1 创建 maven 工程

2.2 认证

2.2.1 启用web安全性功能

2.2.2 测试

2.2.3 认证方式选择

2.3 授权

2.4  Spring Security 常见注解

2.4.1 @Secured

2.4.2 @PreAuthorize

2.4.3 @PostAuthorize

2.5 Spring Security中CSRF


一、认识Spring Security

Spring Security 是为基于 Spring 的应用程序提供声明式安全保护的安全性框架。Spring Security 提供了完整的安全性解决方案,它能够在 Web 请求级别和方法调用级别处理身份认证和授权。因为基于 Spring 框架,所以 Spring Security 充分利用了依赖注入(dependency injection, DI)和面向切面的技术。

1.1 核心功能

对于一个权限管理框架而言,无论是 Shiro 还是 Spring Security,最最核心的功能,无非就是两方面:

  • 认证

  • 授权

通俗点说,认证就是我们常说的登录,授权就是权限鉴别,看看请求是否具备相应的权限。

1.1.1 认证(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 提供的认证机制不仅仅是上面这些。如果上面这些认证机制依然无法满足你的需求,我们也可以自己定制认证逻辑。当我们需要和一些“老破旧”的系统进行集成时,自定义认证逻辑就显得非常重要了。

1.1.2 授权(Authorization)

无论采用了上面哪种认证方式,都不影响在 Spring Security 中使用授权功能。Spring Security 支持基于 URL 的请求授权、支持方法访问授权、支持 SpEL 访问控制、支持域对象安全(ACL),同时也支持动态权限配置、支持 RBAC 权限模型等,总之,我们常见的权限管理需求,Spring Security 基本上都是支持的。

二、项目实践

2.1 创建 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 接口,如下图所示:

图片

输入账号和密码后,即可访问 /hello 接口。 那么如何自定义登录用户信息呢?以及 Spring Security 如何知道我们想要支持基于表单的身份验证?

2.2 认证

2.2.1 启用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 来配置我们的 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 是关于过滤器链配置的,与我们理论知识提到的过滤器知识是一致的。

2.2.2 测试

访问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")
  1. 表单登录时,账号密码默认字段为 username 和 password。

  2. 按理来说,登录成功之后是跳到/页面,失败跳转到登录页,但因为我们这是 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();

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

图片

  1. 自定义登录页面,在 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 从何而来,登录成功后一律跳转到指定的地址。

2.2.3 认证方式选择

在 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 容器中。

2.3 授权

  1. 首先在 HelloController 中增加 r1 和 r2 资源。

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

@GetMapping(value = "/r/r2")
public String r2() {
  return " 访问资源2";
}
  1. 修改 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 资源,需要对应的权限,而且其他接口则只需要认证,并不需要授权。

  1. 测试 访问http://localhost:8086 ,进入登录页面,输入正确的账号密码,提交后页面返回“登录成功”,如果是 zhangsan,则可以访问 r1资源,访问 r2则会报错,我们暂时未处理错误如下:

图片

2.4  Spring Security 常见注解

2.4.1 @Secured

角色校验 ,请求到来访问控制单元方法时必须包含XX角色才能访问

角色必须添加ROLE_前缀

  @Secured({"ROLE_管理员","ROLE_访客"})
  @RequestMapping("/toMain")
  public String toMain(){
      return "main";
  }

使用注解@Secured需要在配置类中添加注解 使@Secured注解生效

@EnableGlobalMethodSecurity(securedEnabled = true)

2.4.2 @PreAuthorize

权限检验,请求到来访问控制单元之前必须包含xx权限才能访问,控制单元方法执行前进行角色校验

   /**
     * [ROLE_管理员, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
     * @PreAuthorize   角色 、权限 校验 方法执行前进行角色校验
     *
     *  hasAnyAuthority() 
     *  hasAuthority()
     *
     *  hasPermission()
     *
     *
     *  hasRole()   
     *  hasAnyRole()
     * */

    @PreAuthorize("hasAnyRole('ROLE_管理员','ROLE_访客')")
    @RequestMapping("/toMain")
    @PreAuthorize("hasAuthority('admin:write')")
    public String toMain(){
        return "main";
    }

使用@PreAuthorize@PostAuthorize 需要在配置类中配置注解@EnableGlobalMethodSecurity 才能生效

@EnableGlobalMethodSecurity(prePostEnabled = true)

2.4.3 @PostAuthorize

权限检验,请求到来访问控制单元之后必须包含xx权限才能访问 ,控制单元方法执行完后进行角色校验

   /**
     * [ROLE_管理员, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
     * @PostAuthorize  角色 、权限 校验 方法执行后进行角色校验
     *
     *  hasAnyAuthority()
     *  hasAuthority()
     *  hasPermission()
     *  hasRole()
     *  hasAnyRole()
     * */

    @PostAuthorize("hasRole('ROLE_管理员')")
    @RequestMapping("/toMain")
    @PreAuthorize("hasAuthority('admin:write')")
    public String toMain(){
        return "main";
    }

2.5 Spring Security中CSRF

什么是CSRF?

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

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

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

通俗解释:

CSRF就是别的网站非法获取我们网站Cookie值,我们项目服务器是无法区分到底是不是我们的客户端,只有请求中有Cookie,认为是自己的客户端,所以这个时候就出现了CSRF。

 

非常感谢您的提问!JavaEE大作业是一个非常重要的项目,因此在完成该项目之前,您需要掌握一些基本的知识和技能。以下是一些保姆教程,以帮助您顺利完成JavaEE大作业。 1. JavaEE基础知识 在开始编写JavaEE大作业之前,您需要掌握JavaEE基础知识。这包括Java基础、Servlet、JSP、JDBC、JavaBean等。您可以通过一些在线教程或书籍来学习这些知识。 2. 数据库设计与操作 JavaEE大作业通常需要与数据库交互,因此您需要掌握数据库设计和操作的基本知识。您可以学习SQL语言、MySQL数据库、Oracle数据库等。 3. Spring框架 Spring框架是一种流行的JavaEE框架,它提供了很多功能和工具,可以帮助您快速开发JavaEE应用程序。您可以学习Spring框架的基本知识,如Spring MVC、Spring Boot、Spring Security等。 4. 前端技术 JavaEE大作业通常需要开发一个漂亮的用户界面,因此您需要掌握一些前端技术。您可以学习HTML、CSS、JavaScript、jQuery等技术,以及一些流行的前端框架,如Angular、React等。 5. 项目管理工具 在开发JavaEE大作业时,您需要使用一些项目管理工具,如Maven、Gradle等。这些工具可以帮助您管理项目依赖关系、构建和部署项目等。 以上是一些基本的保姆教程,以帮助您顺利完成JavaEE大作业。当然,这只是一个大概的指导,具体还需要根据您的项目需求和实际情况来进行学习和实践。祝您成功!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值