使用Spring Boot 2.0和Kotlin来实现一个简单的令牌认证的REST API服务器

我用Spring Boot 2.0和Kotlin实现了一个简单的令牌认证的REST API服务器,记录下来。

GitHub: takuya0301/token-auth-rest-api-server

GitHub:takuya0301/token-auth-rest-api-server

创建Demo项目

使用 curl 命令创建 Demo 项目。

$ curl https://start.spring.io/starter.tgz \
    -d type=gradle-project \
    -d language=kotlin \
    -d baseDir=demo \
    -d dependencies=web \
| tar xz

如果使用curl命令时需要使用start.spring.io的帮助,请使用以下命令进行确认。

$ curl start.spring.io

实现/安装/hello 接口

实现一个GET /hello的端点,以回应字符串”Hello, world!”。

@SpringBootApplication
@RestController
class DemoApplication {
    @GetMapping("/hello")
    fun hello() = "Hello, world!"
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

当启动并使用curl命令访问时,将显示“Hello, world!”。

$ curl -X GET localhost:8080/hello
Hello, world!

添加基本身份验证

引入Spring Security并添加基本身份验证。

这次我们将使用命令来覆盖 build.gradle 文件并引入 Spring Security。

$ cd demo
$ curl start.spring.io/build.gradle \
    -d language=kotlin \
    -d dependencies=web,security \
> build.gradle

与通过手工操作将以下差异添加到build.gradle文件的过程相同。

--- a/build.gradle
+++ b/build.gradle
@@ -41,9 +41,11 @@ repositories {


 dependencies {
+       compile('org.springframework.boot:spring-boot-starter-security')
        compile('org.springframework.boot:spring-boot-starter-web')
        compile('com.fasterxml.jackson.module:jackson-module-kotlin')
        compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        compile("org.jetbrains.kotlin:kotlin-reflect")
        testCompile('org.springframework.boot:spring-boot-starter-test')
+       testCompile('org.springframework.security:spring-security-test')
 }

当系统启动时,将显示生成的用于基本身份验证的密码。每次启动时,都会生成一个不同的密码。

$ ./gradlew bootRun
...
2018-07-01 13:28:26.977  INFO 6754 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: e846f86a-00fd-417a-ad8a-5f91270b6ffb

...

使用curl命令访问时,将显示“你好,世界!”添加认证信息是关键。

$ curl -X GET localhost:8080/hello -u user:e846f86a-00fd-417a-ad8a-5f91270b6ffb
Hello, world!

添加令牌认证

使用Spring Session和Redis进行配置,以添加令牌验证。

同样地,通过命令来覆盖build.gradle文件并引入Spring Session和Redis。因为要将Redis用作会话存储,所以需要在macOS上使用Homebrew来安装Redis,并参考使用brew services命令来启动Redis服务器并在本地保持其运行。

$ curl start.spring.io/build.gradle \
    -d language=kotlin \
    -d dependencies=web,security,session,data-redis \
> build.gradle

使用手工方式将下方的差异附加到build.gradle中,类似于以下方法。

--- a/build.gradle
+++ b/build.gradle
@@ -41,9 +41,11 @@ repositories {


 dependencies {
+       compile('org.springframework.boot:spring-boot-starter-data-redis')
        compile('org.springframework.boot:spring-boot-starter-security')
        compile('org.springframework.boot:spring-boot-starter-web')
        compile('com.fasterxml.jackson.module:jackson-module-kotlin')
+       compile('org.springframework.session:spring-session-data-redis')
        compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        compile("org.jetbrains.kotlin:kotlin-reflect")
        testCompile('org.springframework.boot:spring-boot-starter-test')

在application.properties中添加Redis作为会话存储,以启用Spring Session。

spring.session.store-type=redis

定义一个用于令牌认证的Bean。参考了HttpSession和RESTful API。

@Configuration
class SessionConfig {
    @Bean
    fun httpSessionIdResolver(): HttpSessionIdResolver =
            HeaderHttpSessionIdResolver.xAuthToken()
}

当启动并用curl访问时,将显示”Hello, World!”。由于身份验证密码在每次启动时都会发生变化,所以务必记得进行修正。本次我们将检查响应头中的X-Auth-Token头的内容,因此在curl命令中添加了”-i”选项。

$ curl -X GET localhost:8080/hello -i -u user:190eb4a2-1a11-4baa-8b39-cab0b557b9cd
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Auth-Token: bf03b658-88c9-4de4-9682-f0520a4d12f0
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Sun, 01 Jul 2018 08:18:27 GMT

Hello, world!

因为成功进行了基本身份验证,所以可以使用 X-Auth-Token 头部的令牌进行认证。尝试使用 curl 进行令牌认证。

$ curl -X GET localhost:8080/hello -H "X-Auth-Token: bf03b658-88c9-4de4-9682-f0520a4d12f0"
Hello, world!

使用数据库中保存的认证信息进行认证。

使用Spring JDBC和H2数据库,根据存储在数据库中的认证信息进行身份验证。

使用命令将build.gradle覆盖,并引入Spring JDBC和H2数据库。

$ curl start.spring.io/build.gradle \
    -d language=kotlin \
    -d dependencies=web,security,session,data-redis,jdbc,h2 \
> build.gradle

用手工操作将下面的差异添加到 build.gradle 中,与下述操作相同。

--- a/build.gradle
+++ b/build.gradle
@@ -42,12 +42,15 @@ repositories {

 dependencies {
        compile('org.springframework.boot:spring-boot-starter-data-redis')
+       compile('org.springframework.boot:spring-boot-starter-jdbc')
        compile('org.springframework.boot:spring-boot-starter-security')
        compile('org.springframework.boot:spring-boot-starter-web')
        compile('com.fasterxml.jackson.module:jackson-module-kotlin')
        compile('org.springframework.session:spring-session-data-redis')
+       compile('org.springframework.session:spring-session-jdbc')
        compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        compile("org.jetbrains.kotlin:kotlin-reflect")
+       runtime('com.h2database:h2')
        testCompile('org.springframework.boot:spring-boot-starter-test')
        testCompile('org.springframework.security:spring-security-test')
 }

定义一个用于从数据库获取认证信息的 Bean。参考了 JdbcDaoImpl。JdbcUserDetailsManager 是 JdbcDaoImpl 的子类。可以参考 Spring Boot 的示例 spring-boot-sample-web-secure-jdbc。

@Configuration
class SecurityConfig {
    @Bean
    fun jdbcUserDetailsManager(dataSource: DataSource): JdbcUserDetailsManager {
        val jdbcUserDetailsManager = JdbcUserDetailsManager()
        jdbcUserDetailsManager.setDataSource(dataSource)
        return jdbcUserDetailsManager
    }
}

在启动时添加 schema.sql 以创建数据库模式。参考 User Schema。

create table users(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(50) 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);

另外,添加 data.sql 以在启动时将用户注册到数据库中。加上{noop}的密码表示是参考 DelegatingPasswordEncoder 实现的。DelegatingPasswordEncoder 是默认使用的 PasswordEncoder。

insert into users values ('user', '{noop}password', true);
insert into authorities values ('user', 'ROLE_USER');

当您启动并使用curl访问时,将显示“Hello, world!”。认证信息将使用已在数据库中注册的用户的信息。

$ curl -i -X GET localhost:8080/hello -u user:password
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Auth-Token: f2c57a09-7ff6-4dd1-b322-bfb0759821cb
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Tue, 03 Jul 2018 10:17:54 GMT

Hello, world!

由于使用保存在数据库中的认证信息进行了认证,因此可以使用X-Auth-Token头部的令牌进行认证。让我们尝试使用curl进行令牌认证。

$ curl -X GET localhost:8080/hello -H "X-Auth-Token: f2c57a09-7ff6-4dd1-b322-bfb0759821cb"
Hello, world!

适用于CORS的功能。

使用Spring Security功能支持CORS。

为了支持CORS,需要配置HttpSecurity并定义Bean。参考CORS进行设置。

在这里,我们将响应来自 Example Domain 的请求。我们使用通配符指定允许所有 HTTP 方法和 HTTP 请求头。我们将 X-Auth-Token 响应头指定为暴露头部,以便在 JavaScript 中可以获取。

@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.authorizeRequests()
                .anyRequest().authenticated()
        http.cors()
        http.httpBasic()
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.allowedOrigins = Arrays.asList("http://example.com")
        configuration.allowedMethods = Arrays.asList("*")
        configuration.allowedHeaders = Arrays.asList("*")
        configuration.exposedHeaders = Arrays.asList("X-Auth-Token")
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", configuration)
        return source
    }

    @Bean
    fun jdbcUserDetailsManager(dataSource: DataSource): JdbcUserDetailsManager {
        val jdbcUserDetailsManager = JdbcUserDetailsManager()
        jdbcUserDetailsManager.setDataSource(dataSource)
        return jdbcUserDetailsManager
    }
}

访问示例域名,并通过JavaScript控制台发送Basic认证请求。

var credentials = 'Basic ' + btoa('user:password');
var xhr1 = new XMLHttpRequest();
xhr1.open('GET', 'http://localhost:8080/hello');
xhr1.setRequestHeader('Authorization', credentials);
xhr1.send();

使用 X-Auth-Token 响应头的值发送令牌认证请求。

var token = xhr1.getResponseHeader('X-Auth-Token');
var xhr2 = new XMLHttpRequest();
xhr2.open('GET', 'http://localhost:8080/hello');
xhr2.setRequestHeader('X-Auth-Token', token);
xhr2.send();
ajax-get-request-to-cors-supported-rest-api-server.png

实现/实施 POST /hello 的端点

你好,!实现一个响应 POST /hello 端点的程序。

@SpringBootApplication
@RestController
class DemoApplication {
    @GetMapping("/hello")
    fun hello() = "Hello, world!"

    @PostMapping("/hello")
    fun helloWithName(@RequestBody name: String) = "Hello, $name!"
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

春季安全默认启用 CSRF 保护。对于 POST 请求,会要求提供 CSRF 令牌,而通过 CORS 来检查 Origin 来实施 CSRF 保护。因此,建议参考配置 CSRF 保护和 CSRF 防护备忘单,将默认的 CSRF 保护设置为无效。

@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.authorizeRequests()
                .anyRequest().authenticated()
        http.cors()
        http.csrf().disable()
        http.httpBasic()
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.allowedOrigins = Arrays.asList("http://example.com")
        configuration.allowedMethods = Arrays.asList("*")
        configuration.allowedHeaders = Arrays.asList("*")
        configuration.exposedHeaders = Arrays.asList("X-Auth-Token")
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", configuration)
        return source
    }

    @Bean
    fun jdbcUserDetailsManager(dataSource: DataSource): JdbcUserDetailsManager {
        val jdbcUserDetailsManager = JdbcUserDetailsManager()
        jdbcUserDetailsManager.setDataSource(dataSource)
        return jdbcUserDetailsManager
    }
}

访问示例域并从JavaScript控制台发送基本身份验证请求。

var credentials = 'Basic ' + btoa('user:password');
var xhr1 = new XMLHttpRequest();
xhr1.open('POST', 'http://localhost:8080/hello');
xhr1.setRequestHeader('Authorization', credentials);
xhr1.send('takuya');

使用X-Auth-Token响应头的值发送令牌认证请求。

var token = xhr1.getResponseHeader('X-Auth-Token');
var xhr2 = new XMLHttpRequest();
xhr2.open('POST', 'http://localhost:8080/hello');
xhr2.setRequestHeader('X-Auth-Token', token);
xhr2.send('takuya');
ajax-post-request-to-cors-supported-rest-api-server.png

抑制会话生成

不允许在未认证状态下生成令牌以防止会话生成。

生成未认证状态的会话的原因是为了能够将请求的URL保存在会话中,在认证后进行重定向。

你可以通过设置 NullRequestCache 来禁用该功能,因为它是由 HttpSessionRequestCache 实现的。

@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.authorizeRequests()
                .anyRequest().authenticated()
        http.cors()
        http.csrf().disable()
        http.httpBasic()
        http.requestCache().requestCache(NullRequestCache())
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.allowedOrigins = Arrays.asList("http://example.com")
        configuration.allowedMethods = Arrays.asList("*")
        configuration.allowedHeaders = Arrays.asList("*")
        configuration.exposedHeaders = Arrays.asList("X-Auth-Token")
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", configuration)
        return source
    }

    @Bean
    fun jdbcUserDetailsManager(dataSource: DataSource): JdbcUserDetailsManager {
        val jdbcUserDetailsManager = JdbcUserDetailsManager()
        jdbcUserDetailsManager.setDataSource(dataSource)
        return jdbcUserDetailsManager
    }
}

用 curl 发送失败的认证请求后,确认 X-Auth-Token 响应头不再存在。

$ curl -i -X GET localhost:8080/hello
HTTP/1.1 401 
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 14 Jul 2018 10:05:29 GMT

{"timestamp":"2018-07-14T10:05:29.720+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/hello"}

我在本次中讲解了使用Spring Boot 2.0和Kotlin实现简单令牌认证的REST API服务器的步骤。下一次我计划尝试实现用户注册。

请提供参考资料。
请提供文献参考。

以下是您要求的原文的中文释义:
https://docs.spring.io/spring-boot/docs/2.0.3.RELEASE/reference/htmlsingle/
https://docs.spring.io/spring-security/site/docs/5.0.6.RELEASE/reference/htmlsingle/
https://docs.spring.io/spring-session/docs/2.0.4.RELEASE/reference/htmlsingle/
https://www.owasp.org/index.php/CSRF_Prevention_Cheat_Sheet

广告
将在 10 秒后关闭
bannerAds