《spring实战》学习笔记 第四章 保护Spring

信息安全

4. 1   启用 Spring Security

保护 Spring 应用 的 第一步 就 是将 Spring Boot security starter 依赖 添加 到 构建 文件 中。

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

当 应用 启动 的 时候, 自动 配置 功能 会 探测 到 Spring Security 出 现在 了 类 路径 中, 因此 它 会 初始化 一些 基本 的 安全 配置。

  • 所有 的 HTTP 请求 路径 都 需要 认证;
  • 不需要 特定 的 角色 和 权限;
  • 没有 登录 页面;
  • 认证 过程 是 通过 HTTP basic 认证 对话框 实现 的;
  • 系统 只有 一个 用户, 用户 名为 user。

 

(但是光这些我怎么感觉屁用没有呢)

 

4. 2   配置 Spring Security

 

最近 几个 版本 的 Spring Security 都 支持 基于 Java 的 配置,

 

Spring Security需要配置类

@SuppressWarnings("deprecation")
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//……
}

配置后。 Spring Security 会为我 们提供 免费 的 简单 登录 页

 

Spring Security 为 配置 用户 存储 提供 了 多个 可选 方案, 包括:

 

  • 基于 内存 的 用户 存储;
  • 基于 JDBC 的 用户 存储;
  • 以 LDAP 作为 后端 的 用户 存储;
  • 自定义 用户 详情 服务。

 

不管 使用 哪种 用户 存储, 你都 可以 通过 覆盖 WebSecurityConfigurerAdapter 基础 配置 类 中 定义 的 configure() 方法 来 进行 配置。 首先, 我们 可以 将 如下 的 方法 添加 到 SecurityConfig 类 中:

 

@Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//……
}

4. 2. 1   基于 内存 的 用户 存储

 

用户 信息 可以 存储 在内 存 之中。 假设 我们 只有 数量 有限 的 几个 用户, 而且 这些 用户 几乎 不会 发生 变化, 在 这种 情况下, 将 这些 用户 定义 成 安全 配置 的 一部分 是非 常 简单 的。

案例代码

  @Override
  protected void configure(AuthenticationManagerBuilder auth)
      throws Exception {
    
    auth
      .inMemoryAuthentication()
        .withUser("buzz")
          .password("infinity")
          .authorities("ROLE_USER")
        .and()
        .withUser("woody")
          .password("bullseye")
          .authorities("ROLE_USER");
    
  }

应该没有人会用这玩意吧,跳过跳过

4. 2. 2   基于 JDBC 的 用户 存储

 

用户 信息 通常 会在 关系 型 数据库 中进 行 维护, 基于 JDBC 的 用户 存储 方案 会 更加 合理 一些。

案例代码

  @Autowired
  DataSource dataSource;
  
  @Override
  protected void configure(AuthenticationManagerBuilder auth)
      throws Exception {
    
    auth
      .jdbcAuthentication()
        .dataSource(dataSource);
    
  }

重写 默认 的 用户 查询 功能

 

案例代码

  @Override
  protected void configure(AuthenticationManagerBuilder auth)
      throws Exception {
    
    auth
      .jdbcAuthentication()
        .dataSource(dataSource)
        .usersByUsernameQuery(
            "select username, password, enabled from Users " +
            "where username=?")
        .authoritiesByUsernameQuery(
            "select username, authority from UserAuthorities " +
            "where username=?");
    
  }

在 本例 中, 我们 只 重写 了 认证 和 基本 权限 的 查询 语句, 但是 通过 调用 groupAuthorities ByUsername() 方法, 我们 也 能够 将 群组 权限 重写 为 自定义 的 查询 语句。

 

使用 转 码 后的 密码

 

上面 的 自定义认证 查询, 它 预期 用户 密码 存储 在 了 数据库 之中。 这里 唯一 的 问题在于 如果 密码 明文 存储, 就很 容易 受到 黑客 的 窃取。 但是, 如果 数据库 中的 密码 进 行了 转 码, 那么 认证 就会 失败,

 

为了 解决 这个 问题, 我们 需要 借助 passwordEncoder() 方法 指定 一个 密码 转 码 器( encoder):

 

案例代码

  @Override
  protected void configure(AuthenticationManagerBuilder auth)
      throws Exception {
    
    auth
      .jdbcAuthentication()
        .dataSource(dataSource)
        .usersByUsernameQuery(
            "select username, password, enabled from Users " +
            "where username=?")
        .authoritiesByUsernameQuery(
            "select username, authority from UserAuthorities " +
            "where username=?")
        .passwordEncoder(new StandardPasswordEncoder("53cr3t");
    
  }

passwordEncoder() 方法 可以 接受 Spring Security 中 PasswordEncoder 接口 的 任意 实现。 Spring Security 的 加密 模块 包括了 多个 这样 的 实现。

 

  • BCryptPasswordEncoder: 使用 bcrypt 强 哈 希 加密。
  • NoOpPasswordEncoder: 不进 行 任何 转 码。
  • Pbkdf2PasswordEncoder: 使用 PBKDF2 加密。
  • SCryptPasswordEncoder: 使用 scrypt 哈 希 加密。
  • StandardPasswordEncoder: 使用 SHA- 256 哈 希 加密。

 

如果 内置 的 实现 无法 满足 需求 时, 你 甚至 可以 提供 自定义 的 实现。

 

4. 2. 3   以 LDAP 作为 后端 的 用户 存储

 

我不大懂LDAP,这块没怎么看明白……

 

4. 2. 4   自定义 用户 认证

这部分就是把上一章的代码用前面的知识进行改动,代码太多,知识点较少,跳过跳过。

核心改动:

让用户类实现UserDetails接口,通过 实现 UserDetails 接口, 我们 能够 提供 更多 信息 给 框架, 比如 用户 都被 授予 了 哪些 权限 以及 用户 的 账号 是否 可用。

UserRepository接口新增一个查询用户方法,因为spring data jpa的关系,可以只写接口不写实现。

新增service层的UserRepositoryUserDetailsService类,来进行查找用户

 

4. 3   保护 Web 请求

 

主页、 登录 页 和 注册 页 应该 对 未 认证 的 用户 开放。 为了 配置 这些 安全性 规则, 需要 介绍 一下 WebSecurityConfigurerAdapter 的 其他 configure() 方法:

 

  @Override
  protected void configure(HttpSecurity http) throws Exception {
//……
}

功能:

  • 在 为 某个 请求 提供 服务 之前, 需要 预先 满足 特定 的 条件;
  • 配置 自定义 的 登录 页;
  • 支持 用户 退出 应用;
  • 预防 跨 站 请求 伪造。

 

4. 3. 1   保护 请求

 

我们 需要 确保 只有 认证 过 的 用户 才能 发起 对“/ design” 和“/ orders” 的 请求, 而 其他 请求 对 所有 用户 均可 用。 如下 的 configure() 实现 就能 实现 这一点:

 

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .antMatchers("/design", "/orders")
          .hasRole("ROLE_USER")
        .antMatchers("/", "/**").permitAll()
        ;
}

对 authorizeRequests() 的 调用 会 返回 一个 对象( ExpressionInterceptUrlRegistry), 基于 它 我们 可以 指定 URL 路径 和 这些 路径 的 安全 需求。 在 本例 中, 我们 指定 了 两条 安全 规则:

  • 具备 ROLE_ USER 权限 的 用户 才能 访问“/ design” 和“/ orders”;
  • 其他 的 请求 允许 所有 用户 访问。

这些 规则 的 顺序 是 很重 要的。 声明 在前 面的 安全 规则 比 后面 声明 的 规则 有 更高 的 优先级。

声明 请求 路径 的 方法有很多,具体的太多了。自己看书或者百度

 

除此之外, 我们 还可以 使用 access() 方法, 通过 为 其 提供 SpEL 表达式 来 声明 更 丰富 的 安全 规则。只写表达式更加灵活。

 

重写代码

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .antMatchers("/design", "/orders")
          .access("hasRole('ROLE_USER')")
        .antMatchers("/", "/**").access("permitAll")
        ;
}

4. 3. 2   创建 自定义 的 登录 页

 

为了 替换 内置 的 登录 页, 我们 首先 需要 告诉 Spring Security 自定义 登录 页 的 路径 是什么。 这可 以 通过 调用 传入 到 configure() 中的 HttpSecurity 对象 的 formLogin() 方法 来 实现:.and().formLogin().loginPage("/ login")

前端页面代码:略

默认 情况下, 登录 成功 之后, 用户 将会 被 导航 到 Spring Security 决定 让 用户 登录 之前 的 页面。

但是, 我们 可以 通过 指定 默认 的 成功 页 来 更改 这种 行为:

.and().formLogin().loginPage("/login").defaultSuccessUrl("/design")

总之这个配置还能玩出很多花样

 

4. 3. 3   退出

退出 和 应用 的 登录 是 同等 重要的。 为了 启用 退出 功能, 我们 只需 在 HttpSecurity 对象 上 调用 logout 方法:

.and().logout().logoutSuccessUrl("/")

 

这样 会 搭建 一个 安全 过滤器, 该过 滤器 会 拦截 对“/ logout” 的 请求。 所以, 为了 提供 退出 功能, 我们 需要 为 应用 的 视图 添加 一个 退出 表单 和 按钮:

 

    <form method="POST" th:action="@{/logout}" id="logoutForm">
      <input type="submit" value="Logout"/>
    </form>

当 用户 点击 按钮 的 时候, 他们的 session 将会 被 清理, 这样 他们 就 退出 应用 了。 默认 情况下, 用户 会被 重定 向到 登录 页面, 这样 他们 可以 重新 登录。 但是, 如果 你想 要将 他们 导航 至 不同 的 页面, 那么 可以 调用 logoutSuccessUrl() 指定 退出 后的 不同 页面:.and().logout().logoutSuccessUrl("/") 这句就会返回首页。

 

 

4. 3. 4   防止 跨 站 请求 伪造

跨 站 请求 伪造( Cross- Site Request Forgery, CSRF) 是一 种 常见 的 安全 攻击。 它 会 让 用户 在 一个 恶意 的 Web 页面 上 填写 信息, 然后 自动( 通常 是 秘密 的) 将 表单 以 攻击 受害者 的 身份 提交 到 另外 一个 应用 上。

 

为了 防止 这种 类型 的 攻击, 应用 可以 在 展现 表单 的 时候 生成 一个 CSRF token, 并 放到 隐藏 域 中, 然后 将其 临时 存储 起来, 以便 后续 在 服务器 上 使用。 在 提交 表单 的 时候, token 将 和 其他 的 表单 数据 一起 发送 至 服务器 端。 请求 会被 服务器 拦截, 并与 最初 生成 的 token 进行 对比。 如果 token 匹配, 那么 请求 将会 允许 处理; 否则, 表单 肯定 是由 恶意 网 站 渲染 的, 因为 它不 知道 服务器 所 生成 的 token。

 

Spring Security 提供 了 内置 的CSRF 保护。 更 幸运 的 是, 默认 它 就是 启用 的, 我们 不需要 显 式 配置 它。 我们 唯一 需 要做 的 就是 确保 应用 中的 每个 表单 都要 有一个 名为“_ csrf” 的 字段, 它 会 持有 CSRF token。Spring Security 甚至 进一步 简化 了 将 token 放到 请求 的“_ csrf” 属性 中 这一 任务。 在 Thymeleaf 模板 中, 我们 可以 按照 如下 的 方式 在 隐藏 域 中 渲染 CSRF token:

 

<input type="hidden" name="_csrf" th:value="${_ csrf.token}"/>

如果 你 使用 Spring MVC 的 JSP 标签 库 或者 Spring Security 的 Thymeleaf 方言, 那么 甚至 都不 用 明确 包含 这个 隐藏 域( 这个 隐藏 域 会 自动 生成)。

 

在 Thymeleaf 中, 我们 只需 要 确保< form> 的 某个 属性 带有 Thymeleaf 属性 前缀 即可。 通常 这 并不是 什么 问题, 因为 我们 一般 会 使用 Thymeleaf 渲染 相对于 上下文 的 路径。 例如, 为了 让 Thymeleaf 渲染 隐藏 域, 我们 只需 要 使用 th: action 属性 就可以 了:

<form method="POST" th:action="@{/login}" id="loginForm">

4. 4   了解 用户 是 谁

我们有 多种 方式 确定 用户 是 谁, 常用 的 方式 如下:

  • 注入 Principal 对象 到 控制器 方法 中;
  • 注入 Authentication 对象 到 控制器 方法 中;
  • 使用 SecurityContextHolder 来 获取 安全 上下文;
  • 使用@ AuthenticationPrincipal 注解 来 标注 方法。

 

最 整洁 的 方案 可能 是在 processOrder() 中直 接接 受 一个 User 对象, 不过 我们 需要 为 其 添加@ AuthenticationPrincipal 注解, 这样 它 才会 变成 认证 的 principal:

 

  @PostMapping
  public String processOrder(@Valid Order order, Errors errors, 
      SessionStatus sessionStatus, 
      @AuthenticationPrincipal User user) {
    
    if (errors.hasErrors()) {
      return "orderForm";
    }
    
    order.setUser(user);
    
    orderRepo.save(order);
    sessionStatus.setComplete();
    
    return "redirect:/";
  }

在 processOrder() 得到 User 对象 之后, 我们 就可以 使用 它 并 赋值 给 Order 了。

 

 

还有 另外 一种 方式 能够 识别 当前 认证 用户 是 谁, 但是 这种 方式 有点 麻烦, 它 会 包含 大量 安全性 相关 的 代码。 我们 可以 从 安全 上下 文中 获取 一个 Authentication 对象, 然后 像 下面 这样 获取 它的 principal:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 
User user = (User)authentication.getPrincipal();

它可 以在 应用 程序 的 任何 地方 使用, 而 不仅仅 是在 控制器 的 处理器 方法 中。 这 使得 它 非常 适合 在 较低级 别的 代码 中 使用。

 

4. 5   小结

  • Spring Security 的 自动 配置 是 实现 基本 安全性 功能 的 好 办法, 但是 大多数 的 应用 都 需要 显 式 的 安全 配置, 这样 才能 满足 特定 的 安全 需求。
  • 用户 详情 可以 通过 用户 存储 进行 管理, 它的 后端 可以 是 关系 型 数据库、 LDAP 或 完全 自定义 实现。
  • Spring Security 会 自动 防范 CSRF 攻击。
  • 已 认证 用户 的 信息 可以 通过 SecurityContext 对象( 该 对象 可由 SecurityContextHolder. getContext() 返回) 来 获取, 也可以 借助@ AuthenticationPrincipal 注解 将其 注入 到 控制器 中。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值