每一个 API 开发者都在寻找能更加安全的管理他们的应用程序的方法,而不用牺牲速度或者放弃新功能的实现。为此,我们最近更新了核心的 Stormpath 产品 —— 这是我们的 REST API , 它可以借助 Spring Boot 来使用。一直以来我们都会利用很多重要的效率工具,它们值得人们用来借助 Spring Boot 开发一个API。
许多团队都会发现要对其 API进行认证管理和访问控制有点困难,因此我们就想到要分享一些源自我们在进行迁移的过程中所积累的架构原则,让你能更轻松的管理你的 Spring Boot API。
注意: 下面我们会使用命令行工具 httpie (https://github.com/jkbrzt/httpie) 来进行示例的练习。
1. 使用 @RESTController 注解
可以使用 @RestController (而不是简单的 @Controller) 来确保你是要返回一个Java 对象,而不是到一个 HTML 模板的引用。如下所示:
1 2 3 4 5 6 7 8 | @RestController
public class HelloController {
@RequestMapping ( "/" )
public String home() {
return "hello" ;
}
}
|
执行命令 : http -v localhost:8080
1 2 3 4 5 6 7 8 9 10 11 | HTTP/ 1.1 200 OK
Content-Length: 5
Content-Type: text/plain;charset=UTF- 8
Date: Tue, 14 Jun 2016 23 : 55 : 16 GMT
Server: Apache-Coyote/ 1.1
hello
|
2. 利用 POJO 向 JSON 自动转换的便利
Spring Boot 会自动地为你将 POJO(plain old Java classes)转换成JSON!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | @RestController
public class HelloController {
@RequestMapping ( "/" )
public ApiResponse home() {
return new ApiResponse( "SUCCESS" , "hello" );
}
}
public class ApiResponse {
private String status;
private String message;
public ApiResponse(String status, String message) {
this .status = status;
this .message = message;
}
public String getStatus() {
return status;
}
public String getMessage() {
return message;
}
}
|
执行命令 : http -v localhost:8080
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | HTTP/ 1.1 200 OK
Content-Type: application/json;charset=UTF- 8
Date: Tue, 14 Jun 2016 23 : 54 : 19 GMT
Server: Apache-Coyote/ 1.1
Transfer-Encoding: chunked
{
"message" : "hello" ,
"status" : "SUCCESS"
}
|
3. 使用带有自动绑定服务的依赖注入
不需对 Java 对象进行复杂的设置、配置或者初始化,自动绑定服务就能够对你的业务逻辑进行抽象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @Service
public class HelloService {
public String getGreeting(HttpServletRequest req) {
String greeting = "World" ;
Account account = AccountResolver.INSTANCE.getAccount(req);
if (account != null ) {
greeting = account.getGivenName();
}
return greeting;
}
}
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping ( "/" )
public ApiResponse home(HttpServletRequest req) {
String greeting = helloService.getGreeting(req);
return new ApiResponse( "SUCCESS" , "Hello " + greeting);
}
}
|
这个示例利用了 Stormpath 来在你认证通过后返回一个个性化的问候语。要进行联系你首先需要设置一个 Stormpath 账户,入口在这里。如果你按照这里的指示将你的 Stormpath API Key 文件放到指定的位置 (~/.stormpath/apiKey.properties) ,就不需要再做其它的什么事情了!
启动应用并执行这个命令 :http -v localhost:8080
1 2 3 4 5 6 7 8 9 10 | HTTP/ 1.1 200 OK
Content-Type: application/json;charset=UTF- 8
Date: Wed, 15 Jun 2016 00 : 56 : 46 GMT
Server: Apache-Coyote/ 1.1
Transfer-Encoding: chunked
{
"message" : "Hello World" ,
"status" : "SUCCESS"
}
|
接下来我们需要进行认证,这样我们才可以进入到我们的示例,因此我们会联系使用 Stormpath 内置的 OAuth 2.0 功能来进行认证并获得一条个性化的信息。你要确保已经在管理员控制台为你的 Stormpath 应用程序创建了一个用户。想要详细了解 Java SDK 中 Stormpath 的 OAuth支持,可以看看我们的《Java 产品文档》。
1 2 3 4 5 | http -v -f POST localhost: 8080 /oauth/token \
Origin:http:
grant_type=password \
username=<email address of the user you setup> \
password=<password of the user you setup>
|
响应消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | HTTP/ 1.1 200 OK
Cache-Control: no-store
Content-Length: 938
Content-Type: application/json;charset=UTF- 8
Date: Wed, 15 Jun 2016 00 : 59 : 43 GMT
Pragma: no-cache
Server: Apache-Coyote/ 1.1
{
"access_token" : "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoiYWNjZXNzIiwiYWxnIjoiSFMyNTYifQ.eyJqdGkiOiIzVFhQZ01Ld0NiQTk1VEp6VzBXTzRWIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDY1OTU1OTgzLCJydGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIn0.o_pIHZVDZWogNuhJN2dmG4UKxACoWFxpRpp5OCyh6C4" ,
"expires_in" : 3600 ,
"refresh_token" : "eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoicmVmcmVzaCIsImFsZyI6IkhTMjU2In0.eyJqdGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDcxMTM2MzgzfQ.mJBfCgv4Sdnw7Ubzup7CZ1xdAIC9iO31AJE3NMmp05E" ,
"token_type" : "Bearer"
}
|
做完之后,要保存这个访问令牌供我们的应用程序使用:
1 2 3 | ACCESS_TOKEN=eyJraWQiOiJSOTJTQkhKQzFVNERBSU1HUTNNSE9HVk1YIiwic3R0IjoiYWNjZXNzIiwiYWxnIjoiSFMyNTYifQ.
eyJqdGkiOiIzVFhQZ01Ld0NiQTk1VEp6VzBXTzRWIiwiaWF0IjoxNDY1OTUyMzgzLCJpc3MiOiJodHRwczovL2FwaS5zdG9ybXBhdGguY29tL3YxL2FwcGxpY2F0aW9ucy82dkZUNEFSZldDbXVIVlY4Vmt0alRvIiwic3ViIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hY2NvdW50cy8zcVlHbUl6VWh4UEtZTzI4a04wSWJSIiwiZXhwIjoxNDY1OTU1OTgzLCJydGkiOiIzVFhQZ0owckkwckFTZUU4SmtmN1NSIn0.
o_pIHZVDZWogNuhJN2dmG4UKxACoWFxpRpp5OCyh6C4
|
现在,让我们使用认证信息来再一次访问我们的应用程序:
1 2 3 4 5 6 7 8 9 10 11 12 | http -v localhost: 8080 Authorization: "Bearer $ACCESS_TOKEN"
HTTP/ 1.1 200 OK
Content-Type: application/json;charset=UTF- 8
Date: Wed, 15 Jun 2016 01 : 05 : 35 GMT
Server: Apache-Coyote/ 1.1
Transfer-Encoding: chunked
{
"message" : "Hello Micah" ,
"status" : "SUCCESS"
}
|
现在,多亏了依赖注入的帮助,我们就可以得到来自控制器可以访问到的服务的个性化响应信息了。
4. Spring安全层
Spring安全层在Spring应用上增加了授权过程,使得应用可以容易地决定谁可以访问,访问哪些内容. 安全层在用户组成员和细粒度的权限的基础上,使用声明式配置语法和注解来限制用户访问方法. 如果你有兴趣学习更多知识,我曾写过一篇相关教程 in-depth Stormpath + Spring Security tutorial.。同时,我们有另外一个非常不错的教程Spring Security + Spring Boot WebMVC app in our open-source Java SDK project,带领你从零开始实现完整功能,教程文档可在此处找到. 默认情况下, 在Spring Security中任何资源都应该被锁定,Stormpath Spring Security 示例很好地遵循了这一惯例.。想要使用Stormpath尝试SpringSecurity , 你只需使用Stormpath进行如下配置: 1 2 3 4 5 6 7 8 9 10 | @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.apply(stormpath()).and()
.authorizeRequests()
.antMatchers( "/" ).permitAll();
}
}
|
http.apply(stormpath())是唯一需要配置的基于Stormpath的Spring Security. 下面两行是使用“/”结束符允许未认证用户访问资源. 现在我们看下安全框架影响控制器的方法之一: 1 2 3 4 5 6 7 8 | @RequestMapping ( "/restricted" )
public ApiResponse restricted(HttpServletRequest req) {
return new ApiResponse(
"SUCCESS" ,
"Hello " + AccountResolver.INSTANCE.getAccount(req).getGivenName()
);
}
|
在这个例子中, 由于我们知道认证后进入方法的的方式是唯一的,没必要对账号执行非空检查。例如: 1 2 3 4 5 6 7 8 | http localhost: 8080 /restricted
HTTP/ 1.1 302 Found
Cache-Control: no-cache, no-store, max-age= 0 , must-revalidate
Content-Length: 0
Date: Wed, 15 Jun 2016 17 : 32 : 31 GMT
Expires: 0
Location: http:
|
因为没有认证重定向到/login.如果我像之前一样使用访问令牌,它看起来像这样: 1 2 3 4 5 6 7 8 9 10 11 12 13 | http localhost: 8080 /restricted Authorization: "Bearer $ACCESS_TOKEN"
HTTP/ 1.1 200 OK
Cache-Control: no-cache, no-store, max-age= 0 , must-revalidate
Content-Type: application/json;charset=UTF- 8
Date: Wed, 15 Jun 2016 17 : 34 : 34 GMT
Expires: 0
Pragma: no-cache
{
"message" : "Hello Micah" ,
"status" : "SUCCESS"
}
|
|
5. 统一的错误处理
好的api设计需要你的API返回一个通用的响应,尤其是出现问题的时候。这使得解析和编组JSON到Java对象更加容易和更可靠。
让我们举个例子。 在这里,我们拟个标题:Custom-Header。 如果该标题不存在,则会抛出异常:
1 2 3 4 5 6 7 8 9 10 11 | @RequestMapping ( "/custom-header" )
public ApiResponse customHeader(HttpServletRequest req) throws MissingCustomHeaderException {
String customHeader = req.getHeader( "Custom-Header" );
if (customHeader == null ) {
throw new MissingCustomHeaderException(
"'Custom-Header' on the request is required."
);
}
return new ApiResponse( "SUCCESS" , "Found Custom-Header: " + customHeader);
}
|
如果我们看正常的流程,一切都没有问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 | http localhost: 8080 /custom-header \
Custom-Header:MyCustomValue \
Authorization: "Bearer $ACCESS_TOKEN"
HTTP/ 1.1 200 OK
Cache-Control: no-cache, no-store, max-age= 0 , must-revalidate
Content-Type: application/json;charset=UTF- 8
Date: Wed, 15 Jun 2016 22 : 28 : 47 GMT
{
"message" : "Found Custom-Header: MyCustomValue" ,
"status" : "SUCCESS"
}
|
如果我们没有Custom-Header 这个标题呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | http localhost: 8080 /custom-header Authorization: "Bearer $ACCESS_TOKEN"
HTTP/ 1.1 500 Internal Server Error
Cache-Control: no-cache, no-store, max-age= 0 , must-revalidate
Connection: close
Content-Type: application/json;charset=UTF- 8
Date: Wed, 15 Jun 2016 22 : 34 : 13 GMT
{
"error" : "Internal Server Error" ,
"exception" : "com.stormpath.spring.boot.examples.controller.HelloController$MissingCustomHeaderException" ,
"message" : "'Custom-Header' on the request is required." ,
"path" : "/custom-header" ,
"status" : 500 ,
"timestamp" : 1466030053360
}
|
那么,这里有什么问题? 首先,它不符合我们已经建立的响应格式。 此外,它会导致500(内部服务器错误)错误,这样的情况很糟糕。
幸运的是,Spring Boot使这个很容易修复。 所有我们需要做的是添加一个异常处理程序。 不需要更改其他代码。
1 2 3 4 5 | @ResponseStatus (HttpStatus.BAD_REQUEST)
@ExceptionHandler (MissingCustomHeaderException. class )
public ApiResponse exception(MissingCustomHeaderException e) {
return new ApiResponse( "ERROR" , e.getMessage());
}
|
让我们再看看现在的返回值:
1 2 3 4 5 6 7 8 9 10 11 12 | http localhost: 8080 /custom-header Authorization: "Bearer $ACCESS_TOKEN"
HTTP/ 1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age= 0 , must-revalidate
Connection: close
Content-Type: application/json;charset=UTF- 8
Date: Wed, 15 Jun 2016 22 : 59 : 32 GMT
{
"message" : "'Custom-Header' on the request is required." ,
"status" : "ERROR"
}
|
现在我们介绍到正确的响应400(错误的请求)。我们依然可以接收到和成功响应一样格式的响应。
有奖提示:尝试Stormpath
Stormpath提供了一个高级的,以开发人员为中心的定制服务,包括身份验证和授权,可以在几分钟内实现。 Stormpath REST API使开发人员能够快速轻松地构建各种各样的用户管理功能,否则他们必须自己编写代码,包括: