使用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();
实现/实施 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');
抑制会话生成
不允许在未认证状态下生成令牌以防止会话生成。
生成未认证状态的会话的原因是为了能够将请求的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