This time, in the second part of "Experience the authorization code grant flow with Spring Security OAuth", for the time being, create an application that authenticates and authorizes the REST API with the authorization code grant flow using Spring Security OAuth + Spring Boot. I will try it.
The functional requirements of the application created this time are ...
Service Provider
The Service Provider provides a REST API that manages (registers / updates / deletes / references) task information (title, details, deadline date, completion flag, registration date / time, update date / time) for each user, and accesses the REST API. Authenticates and authorizes using the access token issued by the OAuth 2.0 authorization code grant flow.
I will make it a requirement. Originally, I would like to prepare a Web UI for managing task information on the Service Provider side as well, but I will omit the explanation because the Web UI on the Service Provider side is out of the focus of this entry.
Resource Server
Create the following API as an API for managing the task information of the resource owner.
API name | API overview | Scope to allow access |
---|---|---|
GET /api/tasks | API to get a list of task information | read |
POST /api/tasks | API to register task information | write |
GET /api/tasks/{id} | API to get task information | read |
PUT /api/tasks/{id} | API to update task information | write |
DELETE /api/tasks/{id} | API to delete task information | write |
Authorization Server
The following endpoints (hereinafter referred to as "authorization endpoints") are provided in order to allow the resource owner to access the task information. This endpoint is provided by Spring Security OAuth and does not need to be created by the developer.
Endpoint name | Endpoint overview | Conditions to allow access |
---|---|---|
GET /oauth/authorize | Endpoint for displaying the screen for obtaining approval from the resource owner (hereinafter referred to as "authorization screen") | Authenticated resource owner |
POST /oauth/authorize?user_oauth_approval | Authorization instruction from the resource owner(Allow / Deny)Received approval grant(Authorization code)Endpoint for issuing | Authenticated resource owner |
Create the following endpoint (hereinafter referred to as "token endpoint") in order for the client to issue an access token based on the authorization grant (authorization code) obtained from the resource owner. This endpoint is provided by Spring Security OAuth and does not need to be created by the developer.
Endpoint name | Endpoint overview | Conditions to allow access |
---|---|---|
POST /oauth/token | Authorized grant obtained from resource owner(Authorization code etc.)Endpoint for issuing access tokens based on | Authenticated Client |
** Note: Authentication / authorization for various endpoints **
In this entry ... We will authenticate the resource owner and client to these endpoints using the basic authentication provided by Spring Security.
Client(Web UI)
The Client uses the REST API provided by the Service Provider to provide a Web UI (task list screen and task detail screen) for managing the task information of the resource owner.
Endpoint name | Endpoint overview | Conditions to allow access |
---|---|---|
GET /tasks | Display the task information acquired from the resource server on the task list screen. | Authenticated user |
POST /tasks | Create task information on the resource server | Authenticated user |
GET /tasks/{id} | Display the task information acquired from the resource server on the task details screen | Authenticated user |
POST /tasks/{id}?update | Update the task information managed by the resource server | Authenticated user |
POST /tasks/{id}?delete | Delete the task information managed by the resource server | Authenticated user |
** Note: Task management UI authentication / authorization **
The task management screen prepared on the Client side requires user authentication of the application on the Client side, and the user authentication is performed using Basic authentication provided by Spring Security.
The screen image and screen transition are as follows.
This time, we will create two Spring Boot applications, "client" and "service provider (authorization server + resource server)", and build an application that authenticates and authorizes the API with the authorization code grant flow. The access token and the authentication information associated with the access token between the authorization server and the resource server are linked in-memory on the Web application.
Warning:
The application created in this entry runs using HTTP communication, but ... In the OAuth 2.0 protocol flow, ** Resource owners and endpoints that handle Client authentication and access tokens must use HTTPS communication. **.
Note:
Since the roles of the authorization server and the resource server are different, I think that there are many cases where the application is created as an independent Web application with the following feeling, but this time, prioritizing the simplicity of the application configuration, the authorization server and the resource We decided to implement the server with one Spring Boot application.
When creating an authorization server and a resource server as separate Web applications, selecting the access token and the authentication information associated with the access token between the servers is one point. In addition, we plan to introduce the implementation method when the authorization server and the resource server are separated from the next time.
The application created in this entry is verified using the following version of the library.
Now, let's actually create an application and experience REST API authentication / authorization by authorization code grant flow. First, let's create a development project for the Spring Boot application. Here is an example of creating a project from the command line, but even if you generate it with SPRING INITIALIZR Web UI or the function of your IDE (of course) OK! !!
Since we will create two applications this time, we will create a parent directory to store those applications.
$ mkdir spring-security-oauth-demo
$ cd spring-security-oauth-demo
After navigating to the directory you created, create a Spring Boot application development project for the Service Provider and Client.
$ curl -s https://start.spring.io/starter.tgz\
-d name=service-provider\
-d artifactId=service-provider\
-d dependencies=web,jdbc,h2\
-d baseDir=service-provider\
| tar -xzvf -
$ curl -s https://start.spring.io/starter.tgz\
-d name=client\
-d artifactId=client\
-d dependencies=thymeleaf\
-d baseDir=client\
| tar -xzvf -
Copy the required files, such as the Maven wrapper, from your Service Provider or Client development project.
$ cp -r client/.mvn .mvn
$ cp client/mvnw* .
$ cp client/.gitignore .
$ cp client/pom.xml .
Modify the copied pom.xml
to the build settings. Here, service-provider and client are set to be managed as submodules. This will allow you to build Maven together with service-provider and client.
$ vi pom.xml
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-security-oauth-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-seucirty-oauth-demo</name>
<description>Spring Security OAuth Demo project for Spring Boot</description>
<modules>
<module>service-provider</module>
<module>client</module>
</modules>
</project>
Add "Spring Security OAuth", "Jackson's extension module for JSR 310" and "JTS Topology Suite" to the service provider. ("JTS Topology Suite" is directly related to this entry, but I added it because it is used by H2 Database and an error occurs at runtime.)
$ vi service-provider/pom.xml
service-provider/pom.xml
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.vividsolutions</groupId>
<artifactId>jts</artifactId>
<version>1.13</version>
<scope>runtime</scope>
</dependency>
Add "Spring Security OAuth", "JSR 310 extension module for Jackson", "Thymeleaf extension module for JSR 310", "Webjars Locator", and "WebJar for Bootstrap" to the Client.
$ vi client/pom.xml
client/pom.xml
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7-1</version>
</dependency>
Note:
For "Webjars Locator" and "WebJar for Bootstrap", see "Understanding Access to Static Resources on Spring MVC (+ Spring Boot)" (http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37) ) ”...
- [Accessing static resources on Spring Boot-Using WebJar](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37#webjar%E3%81%AE%E5%88%A9%E7% 94% A8-1)
- [Access to static resources using Spring MVC's unique features-Using WebJar](http://qiita.com/kazuki43zoo/items/e12a72d4ac4de418ee37#webjar%E3%81%AE%E5%88%A9 % E7% 94% A8)
Is briefly explained, so please have a look if you are interested.
Perform a Mavne build (Maven Package) in the directory that contains the pom.xml
for the build, and check the validity of the pom.xml
settings. If you see the following log, the Mavne build is successful.
$ ./mvnw package
...
[INFO] ------------------------------------------------------------------------
[INFO] Building spring-seucirty-oauth-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] service-provider ................................... SUCCESS [ 3.899 s]
[INFO] client ............................................. SUCCESS [ 2.697 s]
[INFO] spring-seucirty-oauth-demo ......................... SUCCESS [ 0.000 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.088 s
[INFO] Finished at: 2017-02-25T09:05:35+09:00
[INFO] Final Memory: 29M/395M
[INFO] ------------------------------------------------------------------------
First, create a Service Provider.
As introduced in the application configuration at the beginning, set the Service Provider port to 18081
and the context path to / provider
. Also, use the H2 file-based database so that the task information handled by the REST API will not be lost when the application is restarted.
service-provider/src/main/resources/application.properties
server.port=18081
server.context-path=/provider
spring.datasource.url=jdbc:h2:~/.h2/service-provider
Note:
When the Service Provider and Client are run on the same host (localhost, etc.), if the
context-path
(default/
) is the same, the cookie that manages the session ID conflicts and HTTP sessions cannot be handled correctly. Therefore, if you want to run it in the local environment, you also need to setcontext-path
.
Create a table to store task information.
service-provider/src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS tasks (
id IDENTITY PRIMARY KEY
, username VARCHAR(255) NOT NULL
, title TEXT NOT NULL
, detail TEXT
, deadline DATE
, finished BOOLEAN NOT NULL DEFAULT FALSE
, created_at DATETIME DEFAULT SYSTIMESTAMP
, updated_at DATETIME DEFAULT SYSTIMESTAMP
, version BIGINT DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_tasks_username ON tasks(username);
Create a domain object that holds the task information handled by the REST API.
service-provider/src/main/java/com/example/Task.java
package com.example;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class Task {
private long id;
private String username;
private String title;
private String detail;
private LocalDate deadline;
private boolean finished;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private long version;
// getter/setter
}
Create a Repository class for the domain object that holds the task information. In this entry, we will implement using the data access function (JdbcOperations
) provided by Spring Framework without using O / R Mapper such as JPA. (Actually ... use NamedParameterJdbcOperations
which can handle name-based parameters)
Note:
Beans of
JdbcOperations
andNamedParameterJdbcOperations
are defined by the Spring Boot AutoConfigure mechanism, so there is no need for the developer to explicitly define the beans.
service-provider/src/main/java/com/example/TaskRepository.java
package com.example;
import java.util.List;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@Repository
public class TaskRepository {
private final NamedParameterJdbcOperations jdbcOperations;
public TaskRepository(NamedParameterJdbcOperations jdbcOperations) {
this.jdbcOperations = jdbcOperations;
}
public List<Task> findAll(String username) {
return jdbcOperations.query(
"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE username = :username ORDER BY deadline DESC, id DESC",
new MapSqlParameterSource("username", username), new BeanPropertyRowMapper<>(Task.class));
}
public Task findOne(long id) {
return jdbcOperations.queryForObject(
"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id",
new MapSqlParameterSource("id", id), new BeanPropertyRowMapper<>(Task.class));
}
public void save(Task task) {
if (task.getId() == null) {
GeneratedKeyHolder holder = new GeneratedKeyHolder();
jdbcOperations.update(
"INSERT INTO tasks (username, title, detail, deadline, finished) VALUES(:username, :title, :detail, :deadline, :finished)",
new BeanPropertySqlParameterSource(task), holder);
task.setId(holder.getKey().longValue());
} else {
jdbcOperations.update(
"UPDATE tasks SET title = :title, detail = :detail, deadline = :deadline, finished = :finished, updated_at = SYSTIMESTAMP, version = version + 1 WHERE id = :id",
new BeanPropertySqlParameterSource(task));
}
}
public void remove(long id) {
jdbcOperations.update("DELETE FROM tasks WHERE id = :id", new MapSqlParameterSource("id", id));
}
}
Create a Controller class that provides CRUD operations (REST API) for task information. Since ʻEmptyResultDataAccessException occurs when the target data is not found when using
JdbcTemplate, in addition to the Handler method for REST API, implement an exception handling method for handling ʻEmptyResultDataAccessException
and returning 404 Not Found. ..
service-provider/src/main/java/com/example/TaskRestController.java
package com.example;
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.on;
import static org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.relativeTo;
import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.Optional;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {
private final TaskRepository repository;
TaskRestController(TaskRepository repository) {
this.repository = repository;
}
@GetMapping
List<Task> getTasks(Principal principal) {
return repository.findAll(extractUsername(principal));
}
@PostMapping
ResponseEntity<Void> postTask(@RequestBody Task task, Principal principal, UriComponentsBuilder uriBuilder) {
task.setUsername(extractUsername(principal));
repository.save(task);
URI createdTaskUri = relativeTo(uriBuilder).withMethodCall(on(TaskRestController.class).getTask(task.getId()))
.build().encode().toUri();
return ResponseEntity.created(createdTaskUri).build();
}
@GetMapping("{id}")
Task getTask(@PathVariable long id) {
return repository.findOne(id);
}
@PutMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void putTask(@PathVariable long id, @RequestBody Task task) {
task.setId(id);
repository.save(task);
}
@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void deleteTask(@PathVariable long id) {
repository.remove(id);
}
private String extractUsername(Principal principal) {
return Optional.ofNullable(principal).map(Principal::getName).orElse("none");
}
@ExceptionHandler(EmptyResultDataAccessException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
void handleEmptyResultDataAccessException() {
// NOP
}
}
Let's check if the REST API you created works properly.
Launch the Spring Boot application using the Maven Plugin provided by Spring Boot.
$ ./mvnw -pl service-provider spring-boot:run
Then, let's innocently call the "task information list acquisition API"! !!
$ $ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 401
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
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
WWW-Authenticate: Basic realm="Spring"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 01:53:04 GMT
{"timestamp":1487987584305,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/provider/api/tasks"}
Apparently Basic authentication is required.
This is because when AutoConfigure of Spring Boot finds a class of Spring Security ... By default, it requires Basic authentication for all request paths (/ **
).
Now that I want to check the operation of the REST API, let's disable Basic authentication once. Basic authentication required by Spring Boot can be disabled by adding the setting security.basic.enabled = false
.
service-provider/src/main/resources/application.properties
security.basic.enabled=false
Warning:
** Please enable Basic authentication after checking the operation of REST API! !! ** **
If you access the REST API after disabling Basic authentication, you can now get an empty list of tasks.
$ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:01:57 GMT
[]
Immediately after starting, no task information is registered, so let's call the "task information creation API" to create task information.
$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Content-Type: application/json" -X POST -d '{"title":"Test Title","detail":"Test Detail","deadline":"2017-02-28"}'
HTTP/1.1 201
Location: http://localhost:18081/provider/api/tasks/1
Content-Length: 0
Date: Sat, 25 Feb 2017 02:17:37 GMT
If the task information is created successfully, the URL for accessing the created task information will be set in the Location
header.
Let's call the "task information acquisition API" (access the URL set in the Location
header) to acquire the created task information.
$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:29:12 GMT
{"id":1,"username":"none","title":"Test Title","detail":"Test Detail","deadline":[2017,2,28],"finished":false,"createdAt":[2017,2,25,11,17,37,671000000],"updatedAt":[2017,2,25,11,17,37,671000000],"version":1}
I was able to get some task information, but ... The format of the date and time is a bit disappointing, so let's try to output the formatted value. If you want to output the formatted date / date / time, set spring.jackson.serialization.write-dates-as-timestamps = false
.
service-provider/src/main/resources/application.properties
spring.jackson.serialization.write-dates-as-timestamps=false
If you access it again after setting spring.jackson.serialization.write-dates-as-timestamps = false
, you can see that it is formatted like that.
$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:31:13 GMT
{"id":1,"username":"none","title":"Test Title","detail":"Test Detail","deadline":"2017-02-28","finished":false,"createdAt":"2017-02-25T11:17:37.671","updatedAt":"2017-02-25T11:17:37.671","version":1}
Since it's a big deal ... I'll call the "task information update API" to update the task information.
$ curl -D - -s http://localhost:18081/provider/api/tasks/1 -H "Content-Type: application/json" -X PUT -d '{"title":"Test Title(Edit)","detail":"Test Detail(Edit)","deadline":"2017-03-31"}'
HTTP/1.1 204
Date: Sat, 25 Feb 2017 02:33:58 GMT
If you get the updated task information, you can confirm that it has been updated correctly.
$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 02:34:21 GMT
{"id":1,"username":"none","title":"Test Title(Edit)","detail":"Test Detail(Edit)","deadline":"2017-03-31","finished":false,"createdAt":"2017-02-25T11:17:37.671","updatedAt":"2017-02-25T11:33:58.51","version":2}
Call the last "Delete task information API" and try to delete the created task information.
$ curl -D - -s http://localhost:18081/provider/api/tasks/1 -X DELETE
HTTP/1.1 204
Date: Sat, 25 Feb 2017 02:35:20 GMT
If you try to get the deleted task information, you will get a client error (404: Not Found) notifying you that there is no target data.
$ curl -D - -s http://localhost:18081/provider/api/tasks/1
HTTP/1.1 404
Content-Length: 0
Date: Sat, 25 Feb 2017 02:35:37 GMT
Assign @EnableAuthorizationServer
to the configuration class, define the beans required for OAuth authentication / authorization, and publish the" authorization endpoint "and" token endpoint "on the authorization server.
service-provider/src/main/java/com/example/AuthorizationServerConfiguration.java
package com.example;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration {
}
Also, by default in Spring Boot, the following values are randomly assigned at startup, so set a fixed value.
service-provider/src/main/resources/application.properties
security.user.password=password
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
In addition, specify the scope and grant type that the default client can handle. "Passowrd: Resource owner password credential" is not required normally, but it is specified in order to check the operation of the authorization server and resource server using CUI (cURL command).
service-provider/src/main/resources/application.properties
security.oauth2.client.scope=read,write
security.oauth2.client.authorized-grant-types=authorization_code,password
If you start the service provider in this state, the following log will be output, and you can see that the endpoint for OAuth has been published to the authorization server.
...
2017-02-25 13:41:48.269 INFO 75893 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/authorize]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
2017-02-25 13:41:48.270 INFO 75893 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/authorize],methods=[POST],params=[user_oauth_approval]}" onto public org.springframework.web.servlet.View org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny(java.util.Map<java.lang.String, java.lang.String>,java.util.Map<java.lang.String, ?>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
2017-02-25 13:41:48.271 INFO 75893 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
2017-02-25 13:41:48.271 INFO 75893 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
2017-02-25 13:41:48.272 INFO 75893 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/check_token]}" onto public java.util.Map<java.lang.String, ?> org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.checkToken(java.lang.String)
2017-02-25 13:41:48.273 INFO 75893 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/confirm_access]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map<java.lang.String, java.lang.Object>,javax.servlet.http.HttpServletRequest) throws java.lang.Exception
2017-02-25 13:41:48.290 INFO 75893 --- [ main] .s.o.p.e.FrameworkEndpointHandlerMapping : Mapped "{[/oauth/error]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)
...
Assign @EnableAuthorizationServer
to the configuration class, define the beans required for OAuth authentication / authorization, and overrideconfigure (HttpSecurity)
of ResourceServerConfigurerAdapter
to configure the authorization settings for the REST API.
service-provider/src/main/java/com/example/ResourceServerConfiguration.java
package com.example;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@EnableResourceServer
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')");
}
}
By making the above settings, OAuth authentication / authorization can be applied to requests under / api
.
Now that you have set up the authorization server and resource server, let's access the resource server.
$ curl -D - -s http://localhost:18081/provider/api/tasks
HTTP/1.1 401
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
Cache-Control: no-store
Pragma: no-cache
WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 09:39:55 GMT
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
Somehow an error occurred. Looking at the details of the error ... An authentication error (401 Unauthorized) has occurred, and it has been notified that authentication with the OAuth Bearer token is required.
The ultimate purpose of this entry is to explain how to use the "authorization code grant" to obtain an access token and access the REST API, but first of all ... "Resources" that allow you to easily obtain an access token. Let's get an access token to access the task information using "Owner Password Credentials".
$ curl -D - -s -u client:secret http://localhost:18081/provider/oauth/token -X POST -d grant_type=password -d username=user -d password=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
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 14:08:39 GMT
{"access_token":"d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097","token_type":"bearer","expires_in":43138,"scope":"read write"}
Specify the obtained access token in the "Authorization" header to access the resource server again.
$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097"
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
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 14:11:17 GMT
[]
Since it's a big deal ... Let's create a new task and get the created task information.
$ curl -D - -s http://localhost:18081/provider/api/tasks -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097" -H "Content-Type: application/json" -X POST -d '{"title":"Test Title","detail":"Test Detail","deadline":"2017-02-28"}'
HTTP/1.1 201
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
Location: http://localhost:18081/provider/api/tasks/6
Content-Length: 0
Date: Sat, 25 Feb 2017 15:24:40 GMT
$ curl -D - -s http://localhost:18081/provider/api/tasks/6 -H "Authorization: Bearer d2fbded0-b8f6-4c2d-bee5-6dd3cafc0097"
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
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 25 Feb 2017 15:26:13 GMT
{"id":6,"username":"user","title":"Test Title","detail":"Test Detail","deadline":"2017-02-28","finished":false,"createdAt":"2017-02-26T00:24:40.843","updatedAt":"2017-02-26T00:24:40.843","version":1}
Now that the Service Provider (authorization server and resource server) has been created, create a Web UI for operating the task information managed by the resource server.
As introduced in the application configuration at the beginning, set the Cleint port to 18080
and the context path to / client
.
client/src/main/resources/application.properties
server.port=18080
server.context-path=/client
In this application, the user authentication on the Client side uses the Basic authentication set up by Spring Boot. If it is the default operation, the password of the default user changes every time it is started, so fix the password.
client/src/main/resources/application.properties
security.user.password=password
By assigning @ EnableOAuth2Client
to the configuration class, the component that manages the access token (ʻOAuth2ClientContext) and the component that guides the user agent (browser) to the authorization server in order to obtain authorization from the resource owner (ʻOAuth2ClientContext
) ʻOAuth2ClientContextFilter) etc. is defined as a bean. In addition, make a Bean definition of
RestTemplate (ʻOAuth2RestTemplate
) extended for OAuth. ʻOAuth2RestTemplate` eliminates the need for the application to be aware of OAuth-related processes (such as the process of acquiring an access token from the authorization server), and calls the REST API in the same way as when authentication / authorization by OAuth is not performed. You will be able to.
client/src/main/java/com/example/ClientConfiguration.java
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
@EnableOAuth2Client
@Configuration
public class ClientConfiguration {
@Bean
OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext context, OAuth2ProtectedResourceDetails details) {
return new OAuth2RestTemplate(details, context);
}
}
In addition, add OAuth related settings (API URL, authorization server URL, client information).
client/src/main/resources/application.properties
#API URL
api.url=http://localhost:18081/provider/api
#Authorization server endpoint URL
auth.url=http://localhost:18081/provider/oauth
security.oauth2.client.access-token-uri=${auth.url}/token
security.oauth2.client.user-authorization-uri=${auth.url}/authorize
#Client information settings
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=secret
security.oauth2.client.scope=read,write
Note:
By default in Spring Boot AutoConfigure, beans are defined to access the resource server using "authorization code grant flow".
Create a domain object that holds task information that you work with via the REST API. (Copy the Task
class created when you created the Service Provider to the Client side)
client/src/main/java/com/example/Task.java
package com.example;
import java.time.LocalDate;
import java.time.LocalDateTime;
public class Task {
private long id;
private String username;
private String title;
private String detail;
private LocalDate deadline;
private boolean finished;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private long version;
// getter/setter
}
Create a Repository class for the domain object that holds the task information.
This class uses the method of the implementation class (ʻOAuth2RestTemplate) of the
RestOperations` interface provided by Spring Security OAuth to access the task information managed on the resource server.
client/src/main/java/com/example/TaskRepository.java
package com.example;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import org.springframework.web.client.RestOperations;
@Repository
public class TaskRepository {
private final RestOperations restOperations;
private final String resourcesUrl;
private final String resourceUrlTemplate;
TaskRepository(RestOperations restOperations,
@Value("${api.url}/tasks") String resourcesUrl) {
this.restOperations = restOperations;
this.resourcesUrl = resourcesUrl;
this.resourceUrlTemplate = resourcesUrl + "/{id}";
}
public List<Task> findAll() {
return Arrays.asList(restOperations.getForObject(resourcesUrl, Task[].class));
}
public Task findOne(long id) {
return restOperations.getForObject(resourceUrlTemplate, Task.class, id);
}
public void save(Task task) {
if (task.getId() == null) {
restOperations.postForLocation(resourcesUrl, task);
} else {
restOperations.put(resourceUrlTemplate, task, task.getId());
}
}
public void remove(long id) {
restOperations.delete(resourceUrlTemplate, id);
}
}
Create a Controller class that provides a Web UI for performing CRUD operations on task information.
client/src/main/java/com/example/TaskController.java
package com.example;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.beans.BeanUtils;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@RequestMapping("/tasks")
@Controller
public class TaskController {
private final TaskRepository repository;
TaskController(TaskRepository repository) {
this.repository = repository;
}
@ModelAttribute
TaskForm setUpForm() {
return new TaskForm();
}
@GetMapping
String list(Model model) {
List<Task> taskList = repository.findAll();
model.addAttribute(taskList);
return "task/list";
}
@PostMapping
String create(@Validated TaskForm form, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
return list(model);
}
Task task = new Task();
BeanUtils.copyProperties(form, task);
repository.save(task);
return "redirect:/tasks";
}
@GetMapping("{id}")
String detail(@PathVariable long id, TaskForm form, Model model) {
Task task = repository.findOne(id);
BeanUtils.copyProperties(task, form);
model.addAttribute(task);
return "task/detail";
}
@PostMapping(path = "{id}", params = "update")
String update(@PathVariable long id, @Validated TaskForm form, BindingResult bindingResult,
Model model, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "task/detail";
}
Task task = new Task();
BeanUtils.copyProperties(form, task);
repository.save(task);
redirectAttributes.addAttribute("id", id);
return "redirect:/tasks/{id}";
}
@PostMapping(path = "{id}", params = "delete")
String delete(@PathVariable long id) {
repository.remove(id);
return "redirect:/tasks";
}
static class TaskForm {
private static final String DATE_TIME_FORMAT = "uuuu-MM-dd HH:mm:ss";
private Long id;
@NotEmpty private String title;
private String detail;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate deadline;
private boolean finished;
@DateTimeFormat(pattern = DATE_TIME_FORMAT) private LocalDateTime createdAt;
@DateTimeFormat(pattern = DATE_TIME_FORMAT) private LocalDateTime updatedAt;
private long version;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
public LocalDate getDeadline() {
return deadline;
}
public void setDeadline(LocalDate deadline) {
this.deadline = deadline;
}
public boolean isFinished() {
return finished;
}
public void setFinished(boolean finished) {
this.finished = finished;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
}
}
Note:
The registration date (
createdAt
) and update date (ʻupdatedAt`) are not input items and should not be retained as form items, but it is also possible to get task information from the resource server every time an input check error occurs. It's subtle, so it's a bit rough, but this time I'll include it in the form item.
It displays the task list obtained from the resource server and provides a Web UI for creating new task information.
client/src/main/resources/templates/task/list.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Task List</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" type="text/css"
th:href="@{/webjars/bootstrap/css/bootstrap.css}"/>
<style type="text/css">
.strike {
text-decoration: line-through;
}
</style>
</head>
<body>
<div class="container">
<h1>Task List</h1>
<div id="taskForm">
<form action="list.html" method="post" class="form-horizontal"
th:action="@{/tasks}" th:object="${taskForm}">
<div class="form-group">
<label for="title" class="col-sm-1 control-label">Title</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="title" th:field="*{title}"/>
<span class="text-error" th:errors="*{title}">error message</span>
</div>
</div>
<div class="form-group">
<label for="detail" class="col-sm-1 control-label">Detail</label>
<div class="col-sm-10">
<textarea class="form-control" id="detail" th:field="*{detail}">
</textarea>
<span class="text-error" th:errors="*{detail}">error message</span>
</div>
</div>
<div class="form-group">
<label for="detail" class="col-sm-1 control-label">Deadline</label>
<div class="col-sm-4">
<input type="date" class="form-control" id="detail" th:field="*{deadline}"/>
<span class="text-error" th:errors="*{deadline}">error message</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-10">
<button type="submit" class="btn btn-default">Create</button>
</div>
</div>
</form>
</div>
<table id="todoList" class="table table-hover" th:if="${not #lists.isEmpty(taskList)}">
<tr>
<th>#</th>
<th>Title</th>
<th>Deadline</th>
<th>Created Datetime</th>
</tr>
<tr th:each="task : ${taskList}">
<td th:text="${taskStat.count}">1</td>
<td>
<span th:class="${task.finished} ? 'strike'">
<a href="detail.html"
th:text="${task.title}" th:href="@{/tasks/{id}(id=${task.id})}">
Create Sample Application
</a>
</span>
</td>
<td>
<span th:text="${#temporals.format(task.deadline,'uuuu-MM-dd')}" th:if="${task.deadline != null}">2017-02-28</span>
</td>
<td>
<span th:text="${#temporals.format(task.createdAt,'uuuu-MM-dd HH:mm.ss')}">2017-02-27 15:17:02</span>
</td>
</tr>
</table>
</div>
</body>
</html>
Provides a Web UI for displaying, updating and deleting task information acquired from the resource server.
client/src/main/resources/templates/task/detail.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Task Detail</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" type="text/css"
th:href="@{/webjars/bootstrap/css/bootstrap.css}"/>
</head>
<body>
<div class="container">
<h1>Task Detail</h1>
<div id="taskForm">
<form action="detail.html" method="post" class="form-horizontal"
th:action="@{/tasks/{id}(id=*{id})}" th:object="${taskForm}">
<div class="form-group">
<label for="title" class="col-sm-2 control-label">Title</label>
<div class="col-sm-10">
<input class="form-control" id="title" value="Create Sample Application" th:field="*{title}"/>
<span class="text-error" th:errors="*{title}">error message</span>
</div>
</div>
<div class="form-group">
<label for="detail" class="col-sm-2 control-label">Detail</label>
<div class="col-sm-10">
<textarea class="form-control" id="detail" th:field="*{detail}">
</textarea>
<span class="text-error" th:errors="*{detail}">error message</span>
</div>
</div>
<div class="form-group">
<label for="detail" class="col-sm-2 control-label">Deadline</label>
<div class="col-sm-4">
<input type="date" class="form-control" id="detail" value="2017-03-10" th:field="*{deadline}"/>
<span class="text-error" th:errors="*{deadline}">error message</span>
</div>
</div>
<div class="form-group">
<label for="finished" class="col-sm-2 control-label">Finished ?</label>
<div class="col-sm-2">
<input type="checkbox" id="finished" th:field="*{finished}"/>
</div>
</div>
<div class="form-group">
<label for="createdAt" class="col-sm-2 control-label">Created Datetime</label>
<div class="col-sm-4">
<input id="createdAt" class="form-control" value="2017-02-28 15:00:01" th:field="*{createdAt}" readonly="readonly"/>
</div>
</div>
<div class="form-group">
<label for="updatedAt" class="col-sm-2 control-label">Updated Datetime</label>
<div class="col-sm-4">
<input id="updatedAt" class="form-control" value="2017-02-28 15:00:01" th:field="*{updatedAt}" readonly="readonly"/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input type="hidden" th:field="*{version}"/>
<button type="submit" class="btn btn-default" name="update">Update</button>
<button type="submit" class="btn btn-default" name="delete">Delete</button>
</div>
</div>
</form>
</div>
<hr/>
<a href="list.html" class="btn btn-default" th:href="@{/tasks}">Task List</a>
</div>
</body>
</html>
Now that you have created the Service Provider and Client applications, let's actually use the applications and experience the authorization code grant flow! !!
First, launch the Service Provider and Client applications.
$ ./mvnw -pl service-provider spring-boot:run
...
2017-02-27 00:29:12.820 INFO 78931 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2017-02-27 00:29:12.867 INFO 78931 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 18081 (http)
2017-02-27 00:29:12.872 INFO 78931 --- [ main] com.example.ServiceProviderApplication : Started ServiceProviderApplication in 3.247 seconds (JVM running for 5.825)
$ ./mvnw -pl client spring-boot:run
...
2017-02-27 00:29:49.282 INFO 78940 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2017-02-27 00:29:49.337 INFO 78940 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 18080 (http)
2017-02-27 00:29:49.344 INFO 78940 --- [ main] com.example.ClientApplication : Started ClientApplication in 3.033 seconds (JVM running for 6.17)
Enter http: // localhost: 18080 / client / tasks in the address bar of your browser to display the task list screen.
When accessing for the first time, user authentication (Basic authentication) on the Client side is required first, so enter the user name (ʻuser) and password (
password`) in the dialog and press the" Login "button.
If the user authentication on the client side is successful, you will be redirected to the authorization endpoint (/ oauth / authorize
) provided by the service provider to obtain the authorization grant (authorization code) from the resource owner. At the first access, resource owner authentication (Basic authentication) is required on the Service Provider side, so enter the user name (ʻuser) and password (
password`) in the dialog and press the" Login "button. please.
If the authentication of the resource owner on the service provider side is successful, the screen (authorization screen) for obtaining the authorization of the resource owner for the scope requested by the client will be displayed, so select allow / deny for each scope and select " Click the "Authorize" button. (Please allow all scopes here)
Note:
In this entry, the authorization screen provided by Spring Security OAuth is used, but I think that it is common to customize in actual application development, so I would like to introduce the customization method from the next time onwards.
After authorization from the resource owner, the task list screen will be displayed: laughing:
However ... I don't know if the task information can be properly obtained from the resource server because the task is not registered ...: sweat_smile: So ... Next, let's register the task using the Web UI. Enter the task information in the input form on the task list screen and click the "Create" button.
Since the task list does not display anything other than the title, deadline, and creation date, let's display the task details screen and check the detailed information of the task. Since the title of the task list is a link, click the title (link) of the task you want to display.
To update or delete a task, click the "Update" or "Delete" button on the task details screen. Here, the deadline is updated to "2017/03/15". (In addition, deletion is omitted)
To destroy the access token and authorization information, restart the Service Provider ("Ctrl + C" + "./mvnw spring-boot: run") and display the task list screen again. Then ... The Service Provider authorization screen will be displayed, and access to the write scope will be denied.
When I try to create a new task on the task list screen ... The error message "Insufficient scope for this resource" is displayed and the call to "Task Creation API" is rejected.
To destroy the access token and authorization information, restart the Service Provider ("Ctrl + C" + "./mvnw spring-boot: run") and display the task list screen again. Then ... The Service Provider authorization screen will be displayed, and access to all scopes will be denied. Then ... Instead of the "authorization code", a parameter that notifies that the resource owner has denied access is added and redirected to the page on the Client side.
Actually, the application (REST API) created so far has a serious security problem. Do you know what the problem is? It is ... how ... ** You can refer to and update the task information of others **: scream:: scream:: scream:
There are two possible ways to solve this problem.
Which method you choose depends on your security requirements. The former behaves the same as when operating on a non-existent task (404 Not Found), while the latter behaves as access denied (403 Forbidden), so it is necessary to record that there was an unauthorized access. In some cases, I think it's better to use the latter method.
If you want to add the user name to the condition (SQL condition) when accessing the task information, add the user name to the argument of the Repository method and add the user name to the SQL condition. Here, for the update and delete process, calling the findOne
method to check whether it is the process for its own task information before performing the process.
@Transactional
@Repository
public class TaskRepository {
// ...
public Task findOne(long id, String username) {
return jdbcOperations.queryForObject(
"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id AND username = :username",
new MapSqlParameterSource("id", id).addValue("username", username), new BeanPropertyRowMapper<>(Task.class)); //★★★ Correction
}
public void save(Task task, String username) {
if (task.getId() == null) {
GeneratedKeyHolder holder = new GeneratedKeyHolder();
jdbcOperations.update(
"INSERT INTO tasks (username, title, detail, deadline, finished) VALUES(:username, :title, :detail, :deadline, :finished)",
new BeanPropertySqlParameterSource(task), holder);
task.setId(holder.getKey().longValue());
} else {
findOne(task.getId(), username); //★★★ Added
jdbcOperations.update(
"UPDATE tasks SET title = :title, detail = :detail, deadline = :deadline, finished = :finished, updated_at = SYSTIMESTAMP, version = version + 1 WHERE id = :id",
new BeanPropertySqlParameterSource(task));
}
}
public void remove(long id, String username) {
findOne(id, username); //★★★ Added
jdbcOperations.update("DELETE FROM tasks WHERE id = :id", new MapSqlParameterSource("id", id));
}
}
The Controller class is also modified according to the argument change of the Repository method.
@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {
// ...
@PostMapping
ResponseEntity<Void> postTask(@RequestBody Task task, Principal principal, UriComponentsBuilder uriBuilder) {
task.setUsername(extractUsername(principal));
repository.save(task, task.getUsername()); //★★★ Correction
URI createdTaskUri = relativeTo(uriBuilder)
.withMethodCall(on(TaskRestController.class).getTask(task.getId(), principal)).build().encode().toUri(); //★★★ Correction
return ResponseEntity.created(createdTaskUri).build();
}
@GetMapping("{id}")
Task getTask(@PathVariable long id, Principal principal) {
return repository.findOne(id, extractUsername(principal)); //★★★ Correction
}
@PutMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void putTask(@PathVariable long id, @RequestBody Task task, Principal principal) {
task.setId(id);
repository.save(task, extractUsername(principal)); //★★★ Correction
}
@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void deleteTask(@PathVariable long id, Principal principal) {
repository.remove(id, extractUsername(principal)); //★★★ Correction
}
// ...
}
If you access the task information of another person, you will get an error (404 Not Found) that occurs when the resource does not exist as shown below.
If you want to check if the owner of the task information and the user of the authentication information match, it is quick and easy to use the method security mechanism provided by Spring Security.
First, grant @EnableGlobalMethodSecurity
to the configuration class to enable the method security mechanism.
service-provider/src/main/java/com/example/ResourceServerConfiguration.java
@EnableGlobalMethodSecurity(prePostEnabled = true) //★★★ Added
@EnableResourceServer
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
// ...
}
Next, set the authorization so that only the resource owner can access the method that acquires task information.
service-provider/src/main/java/com/example/TaskRepository.java
@Transactional
@Repository
public class TaskRepository {
// ...
@PostAuthorize("returnObject.username == authentication.name") //★★★ Added
public Task findOne(long id) {
return jdbcOperations.queryForObject(
"SELECT id, username, title, detail, deadline, finished, created_at, updated_at, version FROM tasks WHERE id = :id",
new MapSqlParameterSource("id", id), new BeanPropertyRowMapper<>(Task.class));
}
// ...
}
Finally, modify it to call the findOne
method when updating or deleting.
service-provider/src/main/java/com/example/TaskRestController.java
@RequestMapping("/api/tasks")
@RestController
public class TaskRestController {
// ...
@PutMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void putTask(@PathVariable long id, @RequestBody Task task) {
repository.findOne(id); //★★★ Added
task.setId(id);
repository.save(task);
}
@DeleteMapping("{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
void deleteTask(@PathVariable long id) {
repository.findOne(id); //★★★ Added
repository.remove(id);
}
// ...
}
If you access the task information of another person, you will get an authorization error (403 Forbidden) as shown below.
The completed version of the application created in this entry is published in the following GitHub repository.
The explanation is a little long, but ... I made an application that authenticates and authorizes the REST API using the authorization code grant flow. This time, the purpose was to let you experience (touch) the authorization code grant flow with Spring Security OAuth + Spring Boot, so make the most of Spring Boot's AutoConfigure mechanism (default operation) and apply it. I tried to make. However ... In actual application development, user (resource owner) and client information is generally managed in a database, etc., and access tokens and authorization information are also managed in a database instead of being managed in the memory of the application. In many cases, it is required to make it persistent. Furthermore, since it is assumed that the service provider handles not one type of resource but multiple resources, or the client also accesses the resources of multiple service providers, the application that can withstand actual operation with only the contents introduced this time. The reality is that it is difficult to develop. So ... From the next time onward, I would like to explain the architecture of Spring Security OAuth and introduce how to develop applications using the extension points of Spring Boot, Spring Security, and Spring Security OAuth.
See you next time! !!
Recommended Posts