信息安全
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 注解 将其 注入 到 控制器 中。