mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): add api guide for spring boot (#4517)
This commit is contained in:
parent
a449a0a21f
commit
0995b4257c
4 changed files with 336 additions and 0 deletions
|
@ -0,0 +1,309 @@
|
|||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
import { appendPath } from '@silverhand/essentials';
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Start a Spring Boot project">
|
||||
|
||||
With [Spring Initializr](https://start.spring.io/), you can quickly start a Spring Boot project.
|
||||
Use the following options:
|
||||
|
||||
- Gradle Project
|
||||
- Language: Java
|
||||
- Spring Boot: 2.7.2 or above
|
||||
|
||||
Generate and open the project.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add dependencies">
|
||||
|
||||
Add the dependencies to your Gradle project build file `build.gradle`:
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
|
||||
}
|
||||
```
|
||||
|
||||
<InlineNotification>
|
||||
Since Spring Boot and Spring Security have built-in support for both OAuth2 resource server and JWT validation,
|
||||
you DO NOT need to add additional libraries from Logto to integrate.
|
||||
|
||||
See [Spring Security OAuth 2.0 Resource Server](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html)
|
||||
and [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture)
|
||||
for more details.
|
||||
</InlineNotification>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Get issuer and JWKS URI">
|
||||
|
||||
All tokens are issued by the [issuer](https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier),
|
||||
and signed with [JWK](https://datatracker.ietf.org/doc/html/rfc7517)
|
||||
(See [JWS](https://datatracker.ietf.org/doc/html/rfc7515) for more details).
|
||||
|
||||
Before moving on, you will need to get an issuer and a JWKS URI to verify the issuer and the signature of the Bearer Token (`access_token`).
|
||||
|
||||
<p>
|
||||
All the Logto Authorization server configurations can be found by requesting <code>{appendPath(props.endpoint, '/oidc/.well-known/openid-configuration')}</code>, including the <strong>issuer</strong>, <strong>jwks_uri</strong> and other authorization configs.
|
||||
</p>
|
||||
|
||||
An example of the response:
|
||||
|
||||
<pre>
|
||||
<code className="language-json">
|
||||
{`{
|
||||
// ...
|
||||
"issuer": "${appendPath(props.endpoint, '/oidc')}",
|
||||
"jwks_uri": "${appendPath(props.endpoint, '/oidc/jwks')}"
|
||||
// ...
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Configure application">
|
||||
|
||||
Use an `application.yml` file (instead of the default `application.properties`) to configure the server port, audience, and OAuth2 resource server.
|
||||
|
||||
<pre>
|
||||
<code className="language-yaml">
|
||||
{`# path/to/project/src/main/resources/application.yaml
|
||||
server:
|
||||
port: 3000
|
||||
|
||||
logto:
|
||||
audience: ${props.audience}
|
||||
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: ${appendPath(props.endpoint, '/oidc')}
|
||||
jwk-set-uri: ${appendPath(props.endpoint, '/oidc/jwks')}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
- `audience`: The unique API identifier of your protected API resource.
|
||||
- `spring.security.oauth2.resourceserver.jwt.issuer-uri`: The iss claim value and the issuer URI in the JWT issued by Logto.
|
||||
- `spring.security.oauth2.resourceserver.jwt.jwk-set-uri`: Spring Security uses this URI to get the authorization server's public keys to validate JWT signatures.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Provide audience validator">
|
||||
|
||||
Provide your own `AudienceValidator` class that implements the `OAuth2TokenValidator` interface to validate whether the required audience is present in the JWT.
|
||||
|
||||
```java
|
||||
// path/to/project/src/main/java/io/logto/springboot/sample/validator/AudienceValidator.java
|
||||
package io.logto.springboot.sample.validator;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
|
||||
|
||||
private final OAuth2Error oAuth2Error = new OAuth2Error("invalid_token", "Required audience not found", null);
|
||||
|
||||
private final String audience;
|
||||
|
||||
public AudienceValidator(String audience) {
|
||||
this.audience = audience;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(Jwt jwt) {
|
||||
if (!jwt.getAudience().contains(audience)) {
|
||||
return OAuth2TokenValidatorResult.failure(oAuth2Error);
|
||||
}
|
||||
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<InlineNotification>
|
||||
For <a href="https://docs.logto.io/docs/recipes/rbac/" target="_blank" rel="noopener">🔐 RBAC</a>, scope validation is also required.
|
||||
</InlineNotification>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Configure Spring Security">
|
||||
|
||||
Spring Security makes it easy to configure your application as a resource server and validate the JWT from the Bearer Token in the request header.
|
||||
|
||||
You need to provide instances of `JwtDecoder` and `SecurityFilterChain` (as Spring beans), and add the `@EnableWebSecurity` annotation.
|
||||
|
||||
```java
|
||||
// path/to/project/src/main/java/io/logto/springboot/sample/configuration/SecurityConfiguration.java
|
||||
package io.logto.springboot.sample.configuration;
|
||||
|
||||
import com.nimbusds.jose.JOSEObjectType;
|
||||
import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import io.logto.springboot.sample.validator.AudienceValidator;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Value("${logto.audience}")
|
||||
private String audience;
|
||||
|
||||
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
|
||||
private String issuer;
|
||||
|
||||
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
|
||||
private String jwksUri;
|
||||
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwksUri)
|
||||
// The decoder should support the token type: Access Token + JWT.
|
||||
.jwtProcessorCustomizer(customizer -> customizer.setJWSTypeVerifier(
|
||||
new DefaultJOSEObjectTypeVerifier<SecurityContext>(new JOSEObjectType("at+jwt"))))
|
||||
.build();
|
||||
|
||||
jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
|
||||
new AudienceValidator(audience),
|
||||
new JwtIssuerValidator(issuer),
|
||||
new JwtTimestampValidator()));
|
||||
|
||||
return jwtDecoder;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt).cors().and()
|
||||
.authorizeRequests(customizer -> customizer
|
||||
// Only authenticated requests can access your protected APIs
|
||||
.mvcMatchers("/", "/secret").authenticated()
|
||||
// Anyone can access the public profile.
|
||||
.mvcMatchers("/profile").permitAll()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add APIs">
|
||||
|
||||
Add a controller to provide the protected and public APIs:
|
||||
|
||||
```java
|
||||
// path/to/project/src/main/java/io/logto/springboot/sample/controller/ProtectedController.java
|
||||
package io.logto.springboot.sample.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
// Only allow all origins for the sample.
|
||||
// (Production applications should configure CORS carefully.)
|
||||
@CrossOrigin(origins = "*")
|
||||
@RestController
|
||||
public class ProtectedController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String protectedRoot() {
|
||||
return "Protected root.";
|
||||
}
|
||||
|
||||
@GetMapping("/secret")
|
||||
public String protectedSecret() {
|
||||
return "Protected secret.";
|
||||
}
|
||||
|
||||
@GetMapping("/profile")
|
||||
public String publicProfile() {
|
||||
return "Public profile.";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Access protected API">
|
||||
|
||||
Build and run your Spring Boot web application, e.g. execute the bootRun Gradle task.
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="linux" label="Linux or macOS">
|
||||
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="windows" label="Windows">
|
||||
|
||||
```bash
|
||||
gradlew.bat bootRun
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
Request your protected API with the Access Token as the Bearer token in the Authorization header, e.g. execute the `curl` command.
|
||||
|
||||
<pre>
|
||||
<code className="language-bash">
|
||||
{`curl --include '${appendPath(props.endpoint, '/secret')}' \\
|
||||
--header 'Authorization: Bearer <your-access-token>'`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
If successful, you will get a response with 200 status:
|
||||
|
||||
```bash
|
||||
HTTP/1.1 200
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
Otherwise, you will get a response with 401 status like this:
|
||||
|
||||
```bash
|
||||
HTTP/1.1 401
|
||||
...
|
||||
WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
|
||||
...
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Further readings">
|
||||
|
||||
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api/)
|
||||
- [Spring Security OAuth 2.0 Resource Server](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html)
|
||||
- [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture)
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
|
@ -0,0 +1,9 @@
|
|||
import { type GuideMetadata } from '../types';
|
||||
|
||||
const metadata: Readonly<GuideMetadata> = Object.freeze({
|
||||
name: 'Spring Boot',
|
||||
description: 'Spring Boot is an open source Java-based framework used to create a micro Service.',
|
||||
target: 'API',
|
||||
});
|
||||
|
||||
export default metadata;
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="49" height="48" viewBox="0 0 49 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_48_60544)">
|
||||
<path d="M37.7667 9.68245C37.3185 10.757 36.7447 11.7748 36.0572 12.7144C34.5564 11.2074 32.771 10.0137 30.8047 9.20278C28.8385 8.39188 26.7306 7.97995 24.6037 7.99094C22.4769 8.00193 20.3734 8.43562 18.4156 9.26679C16.4579 10.098 14.6848 11.31 13.1997 12.8325C11.7145 14.3551 10.5469 16.1576 9.76455 18.1354C8.98225 20.1132 8.60092 22.2268 8.64275 24.3533C8.68458 26.4798 9.14872 28.5768 10.0082 30.5223C10.8677 32.4678 12.1053 34.223 13.6492 35.6859L14.2422 36.2094C16.5051 38.1162 19.2515 39.3598 22.1773 39.8027C25.1031 40.2455 28.0945 39.8704 30.8203 38.7187C33.5462 37.567 35.9003 35.6837 37.6223 33.2772C39.3443 30.8706 40.367 28.0346 40.5772 25.0829C41.0147 20.9979 39.8152 15.8279 37.7672 9.68295L37.7667 9.68245ZM15.9317 35.7789C15.761 35.9899 15.5314 36.1455 15.2722 36.2261C15.013 36.3066 14.7358 36.3084 14.4755 36.2313C14.2153 36.1542 13.9837 36.0017 13.8102 35.7929C13.6367 35.5842 13.529 35.3287 13.5007 35.0588C13.4724 34.7888 13.5248 34.5165 13.6513 34.2764C13.7778 34.0362 13.9727 33.839 14.2114 33.7096C14.45 33.5803 14.7216 33.5246 14.9919 33.5497C15.2622 33.5748 15.5189 33.6794 15.7297 33.8504C16.012 34.0795 16.1919 34.4113 16.2298 34.7729C16.2676 35.1345 16.1604 35.4963 15.9317 35.7789ZM37.7007 30.9739C33.7407 36.2489 25.2857 34.4689 19.8647 34.7249C19.8647 34.7249 18.9037 34.7814 17.9362 34.9399C17.9362 34.9399 18.3012 34.7849 18.7677 34.6084C22.5747 33.2834 24.3742 33.0284 26.6867 31.8384C31.0407 29.6249 35.3477 24.7774 36.2427 19.7384C34.5862 24.5859 29.5562 28.7544 24.9777 30.4474C21.8397 31.6039 16.1707 32.7304 16.1707 32.7304L15.9422 32.6079C12.0852 30.7329 11.9662 22.3794 18.9807 19.6854C22.0522 18.5024 24.9907 19.1519 28.3077 18.3604C31.8497 17.5189 35.9477 14.8654 37.6152 11.4024C39.4802 16.9424 41.7272 25.6134 37.6982 30.9774L37.7007 30.9739Z" fill="#68BD45"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_48_60544">
|
||||
<rect width="32" height="32" fill="white" transform="translate(8.6665 8)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
|
@ -3,6 +3,7 @@
|
|||
import { lazy } from 'react';
|
||||
|
||||
import apiExpress from './api-express/index';
|
||||
import apiSpringBoot from './api-spring-boot/index';
|
||||
import m2mGeneral from './m2m-general/index';
|
||||
import nativeAndroidJava from './native-android-java/index';
|
||||
import nativeAndroidKt from './native-android-kt/index';
|
||||
|
@ -173,6 +174,13 @@ const guides: Readonly<Guide[]> = Object.freeze([
|
|||
Component: lazy(async () => import('./api-express/README.mdx')),
|
||||
metadata: apiExpress,
|
||||
},
|
||||
{
|
||||
order: Number.POSITIVE_INFINITY,
|
||||
id: 'api-spring-boot',
|
||||
Logo: lazy(async () => import('./api-spring-boot/logo.svg')),
|
||||
Component: lazy(async () => import('./api-spring-boot/README.mdx')),
|
||||
metadata: apiSpringBoot,
|
||||
},
|
||||
]);
|
||||
|
||||
export default guides;
|
||||
|
|
Loading…
Reference in a new issue