mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 16:56:40 +08:00
1545 lines
33 KiB
HTML
1545 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>18 用户认证:如何基于 Spring Security 构建用户认证体系?.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/00 开篇词 从零开始:为什么要学习 Spring Boot?.md.html">00 开篇词 从零开始:为什么要学习 Spring Boot?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/01 家族生态:如何正确理解 Spring 家族的技术体系?.md.html">01 家族生态:如何正确理解 Spring 家族的技术体系?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/02 案例驱动:如何剖析一个 Spring Web 应用程序?.md.html">02 案例驱动:如何剖析一个 Spring Web 应用程序?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/03 多维配置:如何使用 Spring Boot 中的配置体系?.md.html">03 多维配置:如何使用 Spring Boot 中的配置体系?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/04 定制配置:如何创建和管理自定义的配置信息?.md.html">04 定制配置:如何创建和管理自定义的配置信息?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/05 自动配置:如何正确理解 Spring Boot 自动配置实现原理?.md.html">05 自动配置:如何正确理解 Spring Boot 自动配置实现原理?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/06 基础规范:如何理解 JDBC 关系型数据库访问规范?.md.html">06 基础规范:如何理解 JDBC 关系型数据库访问规范?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/07 数据访问:如何使用 JdbcTemplate 访问关系型数据库?.md.html">07 数据访问:如何使用 JdbcTemplate 访问关系型数据库?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/08 数据访问:如何剖析 JdbcTemplate 数据访问实现原理?.md.html">08 数据访问:如何剖析 JdbcTemplate 数据访问实现原理?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/09 数据抽象:Spring Data 如何对数据访问过程进行统一抽象?.md.html">09 数据抽象:Spring Data 如何对数据访问过程进行统一抽象?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/10 ORM 集成:如何使用 Spring Data JPA 访问关系型数据库?.md.html">10 ORM 集成:如何使用 Spring Data JPA 访问关系型数据库?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/11 服务发布:如何构建一个 RESTful 风格的 Web 服务?.md.html">11 服务发布:如何构建一个 RESTful 风格的 Web 服务?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/12 服务调用:如何使用 RestTemplate 消费 RESTful 服务?.md.html">12 服务调用:如何使用 RestTemplate 消费 RESTful 服务?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/13 服务调用:如何正确理解 RestTemplate 远程调用实现原理?.md.html">13 服务调用:如何正确理解 RestTemplate 远程调用实现原理?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/14 消息驱动:如何使用 KafkaTemplate 集成 Kafka?.md.html">14 消息驱动:如何使用 KafkaTemplate 集成 Kafka?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/15 消息驱动:如何使用 JmsTemplate 集成 ActiveMQ?.md.html">15 消息驱动:如何使用 JmsTemplate 集成 ActiveMQ?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/16 消息驱动:如何使用 RabbitTemplate 集成 RabbitMQ?.md.html">16 消息驱动:如何使用 RabbitTemplate 集成 RabbitMQ?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/17 安全架构:如何理解 Spring 安全体系的整体架构?.md.html">17 安全架构:如何理解 Spring 安全体系的整体架构?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/Spring Boot 实战开发/18 用户认证:如何基于 Spring Security 构建用户认证体系?.md.html">18 用户认证:如何基于 Spring Security 构建用户认证体系?.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/19 服务授权:如何基于 Spring Security 确保请求安全访问?.md.html">19 服务授权:如何基于 Spring Security 确保请求安全访问?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/20 服务监控:如何使用 Actuator 组件实现系统监控?.md.html">20 服务监控:如何使用 Actuator 组件实现系统监控?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/21 指标定制:如何实现自定义度量指标和 Actuator 端点?.md.html">21 指标定制:如何实现自定义度量指标和 Actuator 端点?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/22 运行管理:如何使用 Admin Server 管理 Spring 应用程序?.md.html">22 运行管理:如何使用 Admin Server 管理 Spring 应用程序?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/23 数据测试:如何使用 Spring 测试数据访问层组件?.md.html">23 数据测试:如何使用 Spring 测试数据访问层组件?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/24 服务测试:如何使用 Spring 测试 Web 服务层组件?.md.html">24 服务测试:如何使用 Spring 测试 Web 服务层组件?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/结束语 以终为始:Spring Boot 总结和展望.md.html">结束语 以终为始:Spring Boot 总结和展望.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>18 用户认证:如何基于 Spring Security 构建用户认证体系?</h1>
|
||
|
||
<p>在 17 讲中,我们梳理了 Web 应用程序的安全性需求,并引出了 Spring Security 这款 Spring 家族中专门用于处理安全性需求的开发框架,同时也明确了认证和授权是安全性框架的核心功能。</p>
|
||
|
||
<p>这一讲我们将先讨论与认证相关的话题,并给出 Spring Security 认证机制及其使用方法。因为 Spring Security 是日常开发过程中的基础组件,所以我们也会对如何实现数据加解密的过程做一些展开。</p>
|
||
|
||
<p>在 Spring Boot 中整合 Spring Security 框架的方式非常简单,我们只需要在 pom 文件中引入 spring-boot-starter-security 依赖即可,这与以往需要提供很多配置才能与 Spring Security 完成集成的开发过程不同,如下代码所示:</p>
|
||
|
||
<pre><code><dependency>
|
||
|
||
|
||
|
||
<groupId>org.springframework.boot</groupId>
|
||
|
||
|
||
|
||
<artifactId>spring-boot-starter-security</artifactId>
|
||
|
||
|
||
|
||
</dependency>
|
||
|
||
</code></pre>
|
||
|
||
<hr />
|
||
|
||
<p><strong>请注意,只要我们在代码工程中添加了上述依赖,包含在该工程中的所有 HTTP 端点都将被保护起来。</strong></p>
|
||
|
||
<p>例如,在 SpringCSS 案例的 account-service 中,我们知道存在一个 AccountController ,且它暴露了一个“accounts/ /{accountId}”端点。现在,我们启动 account-service 服务并访问上述端点,弹出了如下图所示的界面内容:</p>
|
||
|
||
<p><img src="assets/CgqCHmABN02AZfgTAAAR0uEiE5U744.png" alt="Drawing 0.png" /></p>
|
||
|
||
<p>添加 Spring Security 之后自动出现的登录界面</p>
|
||
|
||
<p>同时,在系统的启动控制台日志中,我们发现了如下所示的新的日志信息。</p>
|
||
|
||
<pre><code>Using generated security password: 17bbf7c4-456a-48f5-a12e-a680066c8f80
|
||
|
||
</code></pre>
|
||
|
||
<p>在这里可以看到,Spring Security 为我们自动生成了一个密码,我们可以基于“user”这个账号及上述密码登录这个界面,抽空你也可以尝试下。</p>
|
||
|
||
<p>如果我们使用了 Postman 可视化 HTTP 请求工具,可以设置授权类型为“Basic Auth”并输入对应的用户名和密码完成对 HTTP 端点的访问,设置界面如下图所示:</p>
|
||
|
||
<p><img src="assets/CgqCHmABN1uAC5vsAAA3VhGEBJY812.png" alt="Drawing 1.png" /></p>
|
||
|
||
<p>使用 Postman 来完成认证信息的设置</p>
|
||
|
||
<p>事实上,在引入 spring-boot-starter-security 依赖之后,Spring Security 会默认创建一个用户名为“user”的账号。很显然,每次启动应用时,通过 Spring Security 自动生成的密码都会有所变化,因此它不适合作为一种正式的应用方法。</p>
|
||
|
||
<p>如果我们想设置登录账号和密码,最简单的方式是通过配置文件。例如,我们可以在 account-service 的 application.yml 文件中添加如下代码所示的配置项:</p>
|
||
|
||
<pre><code>spring:
|
||
|
||
|
||
|
||
security:
|
||
|
||
|
||
|
||
user:
|
||
|
||
|
||
|
||
name: springcss
|
||
|
||
|
||
|
||
password: springcss_password
|
||
|
||
</code></pre>
|
||
|
||
<p>重启 account-service 服务后,我们就可以使用上述用户名和密码完成登录。</p>
|
||
|
||
<p>虽然基于配置文件的用户信息存储方案简单且直接,但是显然缺乏灵活性,因此 Spring Security 为我们提供了多种存储和管理用户认证信息的方案,我们一起来看一下。</p>
|
||
|
||
<h3>配置 Spring Security</h3>
|
||
|
||
<p>在 SpringSecurity 中,初始化用户信息所依赖的配置类是 WebSecurityConfigurer 接口,该接口实际上是一个空接口,继承了更为基础的 SecurityConfigurer 接口。</p>
|
||
|
||
<p>在日常开发中,我们往往不需要自己实现这个接口,而是使用 WebSecurityConfigurerAdapter 类简化该配置类的使用方式。比如我们可以通过继承 WebSecurityConfigurerAdapter 类并且覆写其中的 configure(AuthenticationManagerBuilder auth) 的方法完成配置工作。</p>
|
||
|
||
<p>关于 WebSecurityConfigurer 配置类,首先我们需要明确配置的内容。实际上,初始化所使用的用户信息非常简单,只需要指定用户名(Username)、密码(Password)和角色(Role)这三项数据即可。</p>
|
||
|
||
<p>在 WebSecurityConfigurer 类中,使用 AuthenticationManagerBuilder 类创建一个 AuthenticationManager 就能够轻松实现基于内存、LADP 和 JDBC 的验证。</p>
|
||
|
||
<p>接下来,我们就围绕 AuthenticationManagerBuilder 提供的功能实现多种用户信息存储方案。</p>
|
||
|
||
<h4>使用基于内存的用户信息存储方案</h4>
|
||
|
||
<p>我们先来看看如何使用 AuthenticationManagerBuilder 完成基于内存的用户信息存储方案。</p>
|
||
|
||
<p>实现方法是调用 AuthenticationManagerBuilder 的 inMemoryAuthentication 方法,示例代码如下所示:</p>
|
||
|
||
<pre><code>@Override
|
||
|
||
|
||
|
||
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
builder.inMemoryAuthentication()
|
||
|
||
|
||
|
||
.withUser("springcss_user").password("password1")
|
||
|
||
|
||
|
||
.roles("USER")
|
||
|
||
|
||
|
||
.and()
|
||
|
||
|
||
|
||
.withUser("springcss_admin").password("password2")
|
||
|
||
|
||
|
||
.roles("USER", "ADMIN");
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>从上面的代码中,我们看到系统中存在"springcss _user"和"springcss _admin"这两个用户,其密码分别是"password1"和"password2",分别代表着普通用户 USER 及管理员 ADMIN 这两个角色。</p>
|
||
|
||
<p>在 AuthenticationManagerBuilder 中,上述 inMemoryAuthentication 的方法的实现过程如下代码所示:</p>
|
||
|
||
<pre><code>public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
|
||
|
||
|
||
|
||
throws Exception {
|
||
|
||
|
||
|
||
return apply(new InMemoryUserDetailsManagerConfigurer<>());
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里的 InMemoryUserDetailsManagerConfigurer 内部又使用到了 InMemoryUserDetailsManager 对象,而通过深入该类,我们可以获取 Spring Security 中与用户认证相关的一大批核心对象,它们之间的关系如下图所示:</p>
|
||
|
||
<p><img src="assets/Cip5yGABYdOAHvU6AADbh8te5-8316.png" alt="图片3" /></p>
|
||
|
||
<p>Spring Security 中用户认证相关类结构图</p>
|
||
|
||
<p>首先,我们来看上图中代表用户详细信息的 UserDetails 接口,如下代码所示:</p>
|
||
|
||
<pre><code>public interface UserDetails extends Serializable {
|
||
|
||
|
||
|
||
//获取该用户的权限信息
|
||
|
||
|
||
|
||
Collection<? extends GrantedAuthority> getAuthorities();
|
||
|
||
|
||
|
||
//获取密码
|
||
|
||
|
||
|
||
String getPassword();
|
||
|
||
|
||
|
||
//获取用户名
|
||
|
||
|
||
|
||
String getUsername();
|
||
|
||
|
||
|
||
//判断该账户是否已失效
|
||
|
||
|
||
|
||
boolean isAccountNonExpired();
|
||
|
||
|
||
|
||
//判断该账户是否已被锁定
|
||
|
||
|
||
|
||
boolean isAccountNonLocked();
|
||
|
||
|
||
|
||
//判断该账户的凭证信息是否已失效
|
||
|
||
|
||
|
||
boolean isCredentialsNonExpired();
|
||
|
||
|
||
|
||
//判断该用户是否可用
|
||
|
||
|
||
|
||
boolean isEnabled();
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在上述代码中,我们发现 UserDetails 存在一个子接口 MutableUserDetails,从命名上不难看出,后者是一个可变的 UserDetails,而可变的内容就是密码。</p>
|
||
|
||
<p>关于 MutableUserDetails 接口的定义如下代码所示:</p>
|
||
|
||
<pre><code>interface MutableUserDetails extends UserDetails {
|
||
|
||
|
||
|
||
//设置密码
|
||
|
||
|
||
|
||
void setPassword(String password);
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在 Spring Security 中,针对 UserDetails 还存在一个专门的 UserDetailsService,该接口专门用来管理 UserDetails,它的定义如下代码所示:</p>
|
||
|
||
<pre><code>public interface UserDetailsService {
|
||
|
||
|
||
|
||
//根据用户名获取用户信息
|
||
|
||
|
||
|
||
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>而 UserDetailsManager 继承了 UserDetailsService,并提供了一批针对 UserDetails 的操作接口,如下代码所示:</p>
|
||
|
||
<pre><code>public interface UserDetailsManager extends UserDetailsService {
|
||
|
||
|
||
|
||
//创建用户
|
||
|
||
|
||
|
||
void createUser(UserDetails user);
|
||
|
||
|
||
|
||
//更新用户
|
||
|
||
|
||
|
||
void updateUser(UserDetails user);
|
||
|
||
|
||
|
||
//删除用户
|
||
|
||
|
||
|
||
void deleteUser(String username);
|
||
|
||
|
||
|
||
//修改密码
|
||
|
||
|
||
|
||
void changePassword(String oldPassword, String newPassword);
|
||
|
||
|
||
|
||
//判断指定用户名的用户是否存在
|
||
|
||
|
||
|
||
boolean userExists(String username);
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>介绍完 UserDetailsManager 后,我们再回到 InMemoryUserDetailsManager 类,它实现了 UserDetailsManager 接口中的所有方法,这些方法主要用来对用户信息进行维护,从而形成一条代码支线。</p>
|
||
|
||
<p>为了完成用户信息的配置,还存在另外一条代码支线,即 UserDetailsManagerConfigurer。该类维护了一个 UserDetails 列表,并提供了一组 withUser 方法完成用户信息的初始化,如下代码所示:</p>
|
||
|
||
<pre><code>private final List<UserDetails> users = new ArrayList<>();
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
public final C withUser(UserDetails userDetails) {
|
||
|
||
|
||
|
||
this.users.add(userDetails);
|
||
|
||
|
||
|
||
return (C) this;
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>从上述代码中,我们看到 withUser 方法返回的是一个 UserDetailsBuilder 对象,通过该对象可以实现类似 .withUser("springcss_user").password("password1").roles("USER") 这样的链式语法,从而完成用户信息的设置。</p>
|
||
|
||
<p><strong>请注意,这里的 .roles() 方法实际上是 .authorities() 方法的一种简写,因为 Spring Security 会在每个角色名称前自动添加“ROLE_”前缀</strong>,我们可以通过如下所示的代码实现同样的功能:</p>
|
||
|
||
<pre><code>@Override
|
||
|
||
|
||
|
||
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
builder.inMemoryAuthentication()
|
||
|
||
|
||
|
||
.withUser("springcss_user").password("password1")
|
||
|
||
|
||
|
||
.authorities("ROLE_USER")
|
||
|
||
|
||
|
||
.and()
|
||
|
||
|
||
|
||
.withUser("springcss_admin").password("password2")
|
||
|
||
|
||
|
||
.authorities("ROLE_USER", "ROLE_ADMIN");
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>我们可以看到,基于内存的用户信息存储方案也比较简单,但是由于用户信息写死在代码中,因此同样缺乏灵活性。</p>
|
||
|
||
<p>接下来我们将引出另一种更为常见的用户信息存储方案——数据库存储。</p>
|
||
|
||
<h4>使用基于数据库的用户信息存储方案</h4>
|
||
|
||
<p>既然是将用户信息存储在数据库中,我们势必需要创建表结构。因此,在 Spring Security 的源文件中,我们可以找到对应的 SQL 语句,如下代码所示:</p>
|
||
|
||
<pre><code>create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
create unique index ix_auth_username on authorities (username,authority);
|
||
|
||
</code></pre>
|
||
|
||
<p>一旦在自己的数据库中创建了这两张表,且添加了相应数据,我们就可以直接注入一个 DataSource 对象查询用户数据,如下代码所示:</p>
|
||
|
||
<pre><code>@Autowired
|
||
|
||
|
||
|
||
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=?")
|
||
|
||
|
||
|
||
.passwordEncoder(new BCryptPasswordEncoder());
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里使用了 AuthenticationManagerBuilder 的 jdbcAuthentication 方法配置数据库认证方式,而内部则使用了 JdbcUserDetailsManager 工具类。</p>
|
||
|
||
<p>围绕 JdbcUserDetailsManager 整条代码链路的类层结构与 InMemoryUserDetailsManager 非常一致,在该类中定义了各种用户数据库查询的 SQL 语句,以及使用 JdbcTemplate 完成数据库访问的具体实现方法。这里我们不再具体展开,你可以对照前面给出的 InMemoryUserDetailsManager 类层结构图进行分析。</p>
|
||
|
||
<hr />
|
||
|
||
<p><strong>注意,在上述方法中,通过 jdbcAuthentication() 方法验证用户信息时,我们必须集成加密机制,即使用 passwordEncoder() 方法嵌入一个 PasswordEncoder 接口的实现类。</strong></p>
|
||
|
||
<p>在 Spring Security 中,PasswordEncoder 接口代表一种密码编码器,定义如下代码所示:</p>
|
||
|
||
<pre><code>public interface PasswordEncoder {
|
||
|
||
|
||
|
||
//对原始密码进行编码
|
||
|
||
|
||
|
||
String encode(CharSequence rawPassword);
|
||
|
||
|
||
|
||
//对提交的原始密码与库中存储的加密密码进行比对
|
||
|
||
|
||
|
||
boolean matches(CharSequence rawPassword, String encodedPassword);
|
||
|
||
|
||
|
||
//判断加密密码是否需要再次进行加密,默认返回false
|
||
|
||
|
||
|
||
default boolean upgradeEncoding(String encodedPassword) {
|
||
|
||
|
||
|
||
return false;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>Spring Security 中内置了一大批 PasswordEncoder 接口的实现类,如下图所示:</p>
|
||
|
||
<p><img src="assets/CgqCHmABN4qACniMAAB2uCwQA6M741.png" alt="Drawing 3.png" /></p>
|
||
|
||
<p>Spring Security 中的 PasswordEncoder 实现类</p>
|
||
|
||
<p>上图中,比较常用的算法如 SHA-256 算法的 StandardPasswordEncoder、bcrypt 强哈希算法的 BCryptPasswordEncoder 等。而在实际案例中,我们使用的是 BCryptPasswordEncoder,它的 encode 方法如下代码所示:</p>
|
||
|
||
<pre><code>public String encode(CharSequence rawPassword) {
|
||
|
||
|
||
|
||
String salt;
|
||
|
||
|
||
|
||
if (random != null) {
|
||
|
||
|
||
|
||
salt = BCrypt.gensalt(version.getVersion(), strength, random);
|
||
|
||
|
||
|
||
} else {
|
||
|
||
|
||
|
||
salt = BCrypt.gensalt(version.getVersion(), strength);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
return BCrypt.hashpw(rawPassword.toString(), salt);
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>可以看到,上述 encode 方法执行了两个步骤,第一步是生成盐,第二步是根据盐和明文密码生成最终的密文密码。</p>
|
||
|
||
<h3>实现定制化用户认证方案</h3>
|
||
|
||
<p>通过前面内容的分析,我们明确了用户信息存储的实现过程实际上是完全可定制化,而 Spring Security 所做的工作只是把常见、符合一般业务场景的实现方式嵌入框架中。如果存在特殊的场景,开发人员完全可以通过自定义用户信息存储方案进行实现。</p>
|
||
|
||
<p>在前面的内容中,我们介绍了 UserDetails 接口代表用户详细信息,而 UserDetailsService 接口负责对 UserDetails 进行各种操作 。因此,实现定制化用户认证方案的关键是实现 UserDetails 和 UserDetailsService 这两个接口。</p>
|
||
|
||
<h4>扩展 UserDetails</h4>
|
||
|
||
<p>扩展 UserDetails 的方法的实质是直接实现该接口,例如我们可以构建如下所示的 SpringCssUser 类:</p>
|
||
|
||
<pre><code>public class SpringCssUser implements UserDetails {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
private static final long serialVersionUID = 1L;
|
||
|
||
|
||
|
||
private Long id;
|
||
|
||
|
||
|
||
private final String username;
|
||
|
||
|
||
|
||
private final String password;
|
||
|
||
|
||
|
||
private final String phoneNumber;
|
||
|
||
|
||
|
||
//省略getter/setter
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public String getUsername() {
|
||
|
||
|
||
|
||
return username;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public String getPassword() {
|
||
|
||
|
||
|
||
return password;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||
|
||
|
||
|
||
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public boolean isAccountNonExpired() {
|
||
|
||
|
||
|
||
return true;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public boolean isAccountNonLocked() {
|
||
|
||
|
||
|
||
return true;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public boolean isCredentialsNonExpired() {
|
||
|
||
|
||
|
||
return true;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public boolean isEnabled() {
|
||
|
||
|
||
|
||
return true;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>显然,这里我们使用了一种更简单的方法满足 UserDetails 中各个接口的实现需求。一旦我们构建了一个 SpringCssUser 类,就可以创建对应的表结构存储类中所定义的字段。同时,我们也可以基于 Spring Data JPA 创建一个自定义的 Repository,如下代码所示:</p>
|
||
|
||
<pre><code>public interface SpringCssUserRepository extends CrudRepository<SpringCssUser, Long> {
|
||
|
||
|
||
|
||
SpringCssUser findByUsername(String username);
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>SpringCssUserRepository 扩展了 CrudRepository 接口,并提供了一个方法名衍生查询 findByUsername。</p>
|
||
|
||
<p>关于 Spring Data JPA 的使用方法,你还可以回顾《ORM 集成:如何使用 Spring Data JPA 访问关系型数据库?》。</p>
|
||
|
||
<h4>扩展 UserDetailsService</h4>
|
||
|
||
<p>接着,我们来实现 UserDetailsService 接口,如下代码所示:</p>
|
||
|
||
<pre><code>@Service
|
||
|
||
|
||
|
||
public class SpringCssUserDetailsService
|
||
|
||
|
||
|
||
implements UserDetailsService {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
private SpringCssUserRepository repository;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public UserDetails loadUserByUsername(String username)
|
||
|
||
|
||
|
||
throws UsernameNotFoundException {
|
||
|
||
|
||
|
||
SpringCssUser user = repository.findByUsername(username);
|
||
|
||
|
||
|
||
if (user != null) {
|
||
|
||
|
||
|
||
return user;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
throw new UsernameNotFoundException(
|
||
|
||
|
||
|
||
"SpringCSS User '" + username + "' not found");
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>在 UserDetailsService 接口中,我们只需要实现 loadUserByUsername 方法就行。因此,我们可以基于 SpringCssUserRepository 的 findByUsername 方法,再根据用户名从数据库中查询数据。</p>
|
||
|
||
<h4>整合定制化配置</h4>
|
||
|
||
<p>最后,我们再次回到 SpringCssSecurityConfig 类。</p>
|
||
|
||
<p>这次我们将使用自定义的 SpringCssUserDetailsService 完成用户信息的存储和查询,此时我们只需要对配置策略做一些调整,调整后的完整 SpringCssSecurityConfig 类如下代码所示:</p>
|
||
|
||
<pre><code>@Configuration
|
||
|
||
|
||
|
||
public class SpringCssSecurityConfig extends WebSecurityConfigurerAdapter {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
SpringCssUserDetailsService springCssUserDetailsService;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
auth.userDetailsService(springCssUserDetailsService);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里我们注入了 SpringCssUserDetailsService,并将其添加到 AuthenticationManagerBuilder 中,这样 AuthenticationManagerBuilder 将基于自定义的 SpringCssUserDetailsService 完成 UserDetails 的创建和管理。</p>
|
||
|
||
<h3>小结与预告</h3>
|
||
|
||
<p>这一讲我们详细介绍了如何使用 Spring Security 构建用户认证体系的系统方法。</p>
|
||
|
||
<p>一方面,我们可以分别基于内存和数据库方案存储用户信息,这两种方案都是 Spring Security 内置的。另一方面,我们可以通过扩展 UserDetails 接口的方式实现定制化用户的认证方案。同时,为了方便你理解和掌握这部分内容,我们还梳理了与用户认证相关的核心类。</p>
|
||
|
||
<p>介绍完用户认证信息后,19 讲我们将介绍如何基于 Spring Security 确保 Web 请求的安全访问。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/17 安全架构:如何理解 Spring 安全体系的整体架构?.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/Spring Boot 实战开发/19 服务授权:如何基于 Spring Security 确保请求安全访问?.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099751f0fa83d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|