Spring Security和Angular教程(三)
资源服务器
在本节中,我们将继续讨论如何在“单页面应用程序”中使用带有Angular的Spring Security。在这里,我们首先将我们用作应用程序中的动态内容的“greeting”资源分解为一个单独的服务器,首先作为不受保护的资源,然后由不透明的令牌保护。这是一系列部分中的第三部分,您可以通过阅读第一部分来了解应用程序的基本构建块或从头开始构建它,或者您可以直接访问Github中的源代码,它位于两部分:一部分资源不受保护,另一部分受资产保护。
| 如果您正在使用示例应用程序完成此部分,请务必清除Cookie和HTTP Basic凭据的浏览器缓存。在Chrome中,为单个服务器执行此操作的最佳方法是打开新的隐身窗口。 |
一个单独的资源服务器
客户端更改
在客户端,将资源移动到不同的后端并没有太多的事情要做。这是上一节中的“home”组件:
home.component.ts
@Component({
templateUrl: './home.component.html'
})
export class HomeComponent {
title = 'Demo';
greeting = {};
constructor(private app: AppService, private http: HttpClient) {
http.get('resource').subscribe(data => this.greeting = data);
}
authenticated() { return this.app.authenticated; }
}我们需要做的就是更改URL。例如,如果我们要在localhost上运行新资源,它可能如下所示:
home.component.ts
http.get('http://localhost:9000').subscribe(data => this.greeting = data);服务器端更改
该UI服务器是微不足道的改变:我们只需要删除@RequestMapping的问候资源(这是“/资源”)。然后我们需要创建一个新的资源服务器,我们可以像使用Spring Boot Initializr在第一部分中那样做。例如在类似UN * X的系统上使用curl:
$ mkdir resource && cd resource
$ curl https://start.spring.io/starter.tgz -d style=web \
-d name=resource | tar -xzvf -然后,您可以将该项目(默认情况下是普通的Maven Java项目)导入您喜欢的IDE,或者只使用命令行中的文件和“mvn”。
只需@RequestMapping在主应用程序类中添加一个,从旧UI复制实现:
ResourceApplication.java
@SpringBootApplication
@RestController
class ResourceApplication {
@RequestMapping("/")
public Message home() {
return new Message("Hello World");
}
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class, args);
}
}
class Message {
private String id = UUID.randomUUID().toString();
private String content;
public Message(String content) {
this.content = content;
}
// ... getters and setters and default constructor
}完成后,您的应用程序将可以在浏览器中加载。在命令行上,您可以执行此操作
$ mvn spring-boot:run -Dserver.port=9000并转到http:// localhost:9000的浏览器,您应该看到带有问候语的JSON。您可以在端口更改中进行烘焙application.properties(在“src / main / resources”中):
application.properties
<span style="color:#34302d"><span style="color:#333333"><code class="language-properties"><span style="color:#000000">server</span><span style="color:#666600">.</span><span style="color:#000000">port</span><span style="color:#666600">:</span> <span style="color:#006666">9000</span></code></span></span>如果您尝试从浏览器中的UI(在端口8080上)加载该资源,您会发现它不起作用,因为浏览器不允许XHR请求。
CORS谈判
浏览器尝试与我们的资源服务器协商,以确定是否允许根据跨源资源共享协议访问它。这不是Angular的责任,所以就像cookie合同一样,它将与浏览器中的所有JavaScript一样工作。这两个服务器没有声明它们具有共同的来源,因此浏览器拒绝发送请求并且UI被破坏。
为了解决这个问题,我们需要支持CORS协议,该协议涉及“飞行前”OPTIONS请求和一些标题,列出调用者允许的行为。Spring 4.2有一些很好的细粒度CORS支持,所以我们可以在控制器映射中添加一个注释,例如:
ResourceApplication.java
@RequestMapping("/")
@CrossOrigin(origins="*", maxAge=3600)
public Message home() {
return new Message("Hello World");
} | 巧妙地使用 |
保护资源服务器
大!我们有一个新架构的工作应用程序。唯一的问题是资源服务器没有安全性。
添加Spring Security
我们还可以查看如何将安全性作为过滤器层添加到资源服务器,就像在UI服务器中一样。第一步非常简单:只需将Spring Security添加到Maven POM的类路径中:
的pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>重新启动资源服务器,嘿presto!它很安全:
$ curl -v localhost:9000
< HTTP/1.1 302 Found
< Location: http://localhost:9000/login
...我们正在重定向到(whitelabel)登录页面,因为curl没有发送与Angular客户端相同的头文件。修改命令以发送更多类似的标头:
$ curl -v -H "Accept: application/json" \
-H "X-Requested-With: XMLHttpRequest" localhost:9000
< HTTP/1.1 401 Unauthorized
...所以我们需要做的就是教会客户端发送每个请求的凭据。
令牌认证
互联网和人们的Spring后端项目充斥着基于自定义令牌的身份验证解决方案。Spring Security提供了一个准确的Filter实现,让您自己开始(参见例如AbstractPreAuthenticatedProcessingFilter和TokenService)。虽然Spring Security中没有规范的实现,但其中一个原因可能是更简单的方法。
请记住,本系列的第二部分中,Spring Security HttpSession默认使用它来存储身份验证数据。它不会直接与会话交互:中间有一个抽象层(SecurityContextRepository),您可以使用它来更改存储后端。如果我们可以在我们的资源服务器中将该存储库指向具有由我们的UI验证的身份验证的商店,那么我们可以在两个服务器之间共享身份验证。UI服务器已经有了这样的商店(HttpSession),所以如果我们可以分发该商店并将其打开到资源服务器,我们就拥有了大部分解决方案。
春季会议
使用Spring Session,解决方案的这一部分非常简单。我们所需要的只是一个共享数据存储(Redis和JDBC支持开箱即用),以及服务器中的几行配置来设置Filter。
在UI应用程序中,我们需要向POM添加一些依赖项:
的pom.xml
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>Spring Boot和Spring Session一起工作以连接到Redis并集中存储会话数据。
使用这一行代码并在localhost上运行Redis服务器,您可以运行UI应用程序,使用一些有效的用户凭据登录,并且会话数据(身份验证)将存储在redis中。
| 如果您没有在本地运行的redis服务器,您可以使用Docker轻松地启动它(在Windows或MacOS上这需要一个VM)。在Githubdocker-compose.yml的源代码中有一个文件,您可以在命令行上轻松运行 |
从UI发送自定义标记
唯一缺失的部分是商店中数据密钥的传输机制。关键是HttpSessionID,因此如果我们可以在UI客户端中获取该密钥,我们可以将其作为自定义标头发送到资源服务器。因此,“home”控制器需要进行更改,以便它将标头作为问候资源的HTTP请求的一部分发送。例如:
home.component.ts
constructor(private app: AppService, private http: HttpClient) {
http.get('token').subscribe(data => {
const token = data['token'];
http.get('http://localhost:9000', {headers : new HttpHeaders().set('X-Auth-Token', token)})
.subscribe(response => this.greeting = response);
}, () => {});
}(更优雅的解决方案可能是根据需要获取令牌,并使用我们RequestOptionsService将标头添加到资源服务器的每个请求。)
我们没有直接转到“http:// localhost:9000 [ http:// localhost:9000 ]”,而是在“/ token”处对UI服务器上的新自定义端点的调用成功回调中包含该调用。实现这一点很简单:
UiApplication.java
@SpringBootApplication
@RestController
public class UiApplication {
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
...
@RequestMapping("/token")
public Map<String,String> token(HttpSession session) {
return Collections.singletonMap("token", session.getId());
}
}因此,UI应用程序已准备就绪,并将会话ID包含在名为“X-Auth-Token”的标头中,用于对后端的所有调用。
资源服务器中的身份验证
资源服务器有一个微小的变化,它可以接受自定义标头。CORS配置必须将该标头指定为来自远程客户端的允许标头,例如
ResourceApplication.java
@RequestMapping("/")
@CrossOrigin(origins = "*", maxAge = 3600,
allowedHeaders={"x-auth-token", "x-requested-with", "x-xsrf-token"})
public Message home() {
return new Message("Hello World");
}从浏览器开始的飞行前检查现在将由Spring MVC处理,但是我们需要告诉Spring Security允许它通过:
ResourceApplication.java
public class ResourceApplication extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().authorizeRequests()
.anyRequest().authenticated();
}
... | 无需 |
剩下的就是在资源服务器中获取自定义令牌并使用它来验证我们的用户。事实证明这非常简单,因为我们需要做的就是告诉Spring Security会话存储库在哪里,以及在哪里查找传入请求中的令牌(会话ID)。首先我们需要添加Spring Session和Redis依赖项,然后我们可以设置Filter:
ResourceApplication.java
@SpringBootApplication
@RestController
class ResourceApplication {
...
@Bean
HeaderHttpSessionStrategy sessionStrategy() {
return new HeaderHttpSessionStrategy();
}
}这Filter创建是一个在UI服务器的镜像,所以它建立的Redis作为会话存储。唯一的区别是它使用HttpSessionStrategy在标题中查找的自定义(默认情况下为“X-Auth-Token”)而不是默认值(名为“JSESSIONID”的cookie)。我们还需要阻止浏览器在未经身份验证的客户端中弹出一个对话框 - 该应用程序是安全的,但WWW-Authenticate: Basic默认情况下会发送一个401 ,因此浏览器会响应用户名和密码的对话框。实现这一目标的方法不止一种,但我们已经让Angular发送了一个“X-Requested-With”标头,因此Spring Security会默认为它处理它。
资源服务器有一个最终更改,以使其与我们的新身份验证方案一起使用。Spring Boot默认安全性是无状态的,我们希望它在会话中存储身份验证,因此我们需要在application.yml(或application.properties)中显式:
application.yml
security:
sessions: NEVER这告诉Spring Security“永远不会创建会话,但如果它在那里则使用一个”(由于UI中的身份验证,它已经存在)。
重新启动资源服务器并在新的浏览器窗口中打开UI。
为什么不能全部使用Cookie?
我们必须使用自定义标头并在客户端中编写代码来填充标题,这并不是非常复杂,但它似乎与第二部分中的建议相矛盾,即尽可能使用cookie和会话。那里的论点是不这样做会带来额外的不必要的复杂性,而且我们现在实现的确实是迄今为止我们看到的最复杂的:解决方案的技术部分远远超过业务逻辑(这无疑是微不足道的)。这绝对是一个公平的批评(我们打算在本系列的下一部分中讨论),但我们只是简单地看一下为什么它不像只使用cookie和会话一样简单。
至少我们仍在使用会话,这是有道理的,因为Spring Security和Servlet容器知道如何做到这一点,我们不费吹灰之力。但我们不能继续使用cookie来传输身份验证令牌吗?它会很好,但有一个原因它不起作用,那就是浏览器不会让我们。您可以从JavaScript客户端浏览浏览器的cookie存储区,但是有一些限制,并且有充分的理由。特别是,您无权访问服务器以“HttpOnly”发送的cookie(默认情况下,您将看到会话cookie的情况)。您也无法在传出请求中设置cookie,因此我们无法设置“SESSION”cookie(这是Spring Session的默认cookie名称),我们必须使用自定义的“X-Session” 头。这些限制都是为了您自己的保护,因此如果没有适当的授权,恶意脚本无法访问您的资源。
TL; DR UI和资源服务器没有共同的来源,所以他们不能共享cookie(即使我们可以使用Spring Session来强制他们共享会话)。
结论
我们在本系列的第II部分中复制了应用程序的功能:从远程后端获取问候语的主页,导航栏中有登录和注销链接。不同之处在于问候语来自独立的资源服务器,而不是嵌入在UI服务器中。这增加了实现的复杂性,但好消息是我们有一个主要基于配置(实际上是100%声明)的解决方案。我们甚至可以通过将所有新代码提取到库中来使解决方案100%声明(Spring配置和Angular自定义指令)。在接下来的几期分期之后,我们将推迟这项有趣的任务。在下一节中 我们将看一个不同的非常好的方法来减少当前实现中的所有复杂性:API网关模式(客户端将其所有请求发送到一个地方并在那里处理身份验证)。
| 我们在这里使用Spring Session来共享两个逻辑上不同的应用程序服务器之间的会话。这是一个巧妙的技巧,并且“常规”JEE分布式会话无法实现。 |
原文地址:https://spring.io/guides/tutorials/spring-security-and-angular-js
下载代码:https:///spring-guides/tut-spring-security-and-angular-js/tree/master/spring-session
运行Redis服务

在resource和ui文件夹中,依次执行mvn clean,mvn install,mvn spring-boot:run命令,运行程序。
如果执行命令报错,可终止后再次执行。
在浏览器中输入http://localhost:8080/,访问程序。

在登录框中输入用户名:user,密码:password,

登录成功后,如下图所示:

















