龙空技术网

在Spring Boot中使用OAuth2保护API

SimpleIsTheBest 683

前言:

眼前兄弟们对“aspnetrequire未定义”都比较珍视,兄弟们都想要剖析一些“aspnetrequire未定义”的相关资讯。那么小编在网摘上汇集了一些有关“aspnetrequire未定义””的相关内容,希望你们能喜欢,看官们一起来学习一下吧!

代码下载

本文章所有代码可从Github下载。地址:GitHub - YuRui1113/spring-security-oauth2-api: 在Spring Boot中使用OAuth2保护API

概述

大多数现代应用中使用OAuth2的场景如下图:

综合起来OAuth2应用场景有:

浏览器与 Web 应用程序通信Web 应用程序与 Web API 通信(有时单独使用,有时代表用户)基于浏览器的应用程序与 Web API 通信原生应用程序与 Web API 通信基于服务器的应用程序与 Web API 通信Web API 与 Web API 通信(有时单独使用,有时代表用户)

设计到的对象模型/术语有:

Security Token Service:安全令牌服务提供者,它主要实现了OpenID Connect和OAuth 2.0 协议。User:用户是使用已注册客户端访问资源的人员。Client:客户端是从Security Token Service请求令牌的软件 - 用于对用户进行身份验证(请求身份令牌)或访问资源(请求访问令牌)。客户端必须先向Security Token Service注册,然后才能请求令牌。Resources:资源是您希望使用Security Token Service保护的东西 - 用户的身份数据或 API。每个资源都有一个唯一的名称 - 客户端使用此名称来指定它们要访问哪些资源。ID Token:身份令牌表示身份验证过程的结果。它至少包含用户的标识符以及有关用户如何以及何时进行身份验证的信息。它可以包含其他标识数据。Access Token:访问令牌允许访问 API 资源。客户端请求访问令牌并将其转发到 API。访问令牌包含有关客户端和用户的信息(如果存在)。API 使用该信息来授权访问其数据。

本文主要描述使用Spring Security 的 OAuth 2.0部分保护API资源。

代码仓库提供了三个应用:

OAuth2Server/ – 用于测试的实现了OpenID Connect和OAuth 2.0 协议的安全令牌服务提供者应用AngularClient/ - 用于访问API的Angular客户端应用spring-security-oauth2-api/ - 基于Spring Boot受OAuth2服务保护的API应用

在OAuth2里使用Scope这种机制来限制对应用程序的访问,它相当于Spring中的Authority(权限)。

为保护spring-security-oauth2-api提供了两种Scope:

apiBook.readapiBook.write开源的安全令牌服务提供者 - IdentityServer4

IdentityServer4 是适用于 ASP.NET Core 的 OpenID Connect 和 OAuth 2.0 框架。最初是开源的,从2022年9月后开源版本不再维护,只能支持使用到.NET Core 3.1。对于创建用于测试目的的OpenID Connect和OAuth 2.0 服务器是很方便的。

代码仓库下目录OAuth2Server即是该测试服务器源码,我们可以安装.NET Core 3.1 SDK来运行它。

使用下面连接下载微软官方.NET Core 3.1 SDK

Download .NET Core 3.1 (Linux, macOS, and Windows)

安装SDK成功后,我们可以配置服务器运行Url,开发环境不需要使用https,修改文件 OAuth2Server\Properties\launchSettings.json内容为:

{  "profiles": {    "SelfHost": {      "commandName": "Project",      "launchBrowser": true,      "environmentVariables": {        "ASPNETCORE_ENVIRONMENT": "Development"      },      "applicationUrl": ";    }  }}

OAuth2资源和客户端配置在OAuth2Server\Config.cs里,可以按照已有代码格式添加和修改:

using IdentityServer4;using IdentityServer4.Models;using System.Collections.Generic;namespace IdentityServerAspNetIdentity{    public static class Config    {        static string SERVICE_NAME_BOOK = "apiBook";        public static IEnumerable<IdentityResource> IdentityResources =>            new List<IdentityResource>            {                new IdentityResources.OpenId(),                new IdentityResources.Profile(),            };        public static IEnumerable<ApiScope> ApiScopes =>            new List<ApiScope>            {                new ApiScope($"{SERVICE_NAME_BOOK}.read", "Book API read"),                new ApiScope($"{SERVICE_NAME_BOOK}.write", "Book API write")            };        // With ApiResource you can now create two logical APIs and their correponding scopes        public static IEnumerable<ApiResource> ApiResources =>            new List<ApiResource>            {                new ApiResource                {                    Name = SERVICE_NAME_BOOK,                    DisplayName = "Book API service",                    Description = "The API for book mangement API",                    Scopes = new List<string> { $"{SERVICE_NAME_BOOK}.read", $"{SERVICE_NAME_BOOK}.write" },                    ApiSecrets = new List<Secret> {new Secret("secret".Sha256())}                }            };        public static IEnumerable<Client> Clients =>            new List<Client>            {                // machine to machine client                new Client                {                    ClientId = "client",                    ClientSecrets = { new Secret("secret".Sha256()) },                    AllowedGrantTypes = GrantTypes.ClientCredentials,                    // scopes that client has access to                    AllowedScopes = { "apiBook" }                },                                // interactive Angular client                new Client                {                    ClientId = "angular",                    ClientSecrets = { new Secret("secret".Sha256()) },                    ClientUri = ";, // public uri of the client                    AllowedCorsOrigins = { "; },                    AllowedGrantTypes = GrantTypes.Code,                                        // where to redirect to after login                    RedirectUris = { "; },                    // where to redirect to after logout                    PostLogoutRedirectUris = { "; },                    AllowedScopes = new List<string>                    {                        IdentityServerConstants.StandardScopes.OpenId,                        IdentityServerConstants.StandardScopes.Profile,                        $"{SERVICE_NAME_BOOK}.read"                    },                    // Default is true                    RequirePkce = false,                    AccessTokenLifetime = 60*60*2, // 2 hours                    IdentityTokenLifetime= 60*60*2 // 2 hours                }            };    }}

上面配置中,我们定义了:

Scope: apiBook.read和apiBook.write,Api:apiBook (包含两种Scope: apiBook.read和apiBook.write)Client:Angular(只包含Scope:apiBook.read,这样Angular客户端只能读不能修改数据)

打开Windows PowerShell,进入工程文件所在目录,然后执行命令即可运行:

dotnet run -f netcoreapp3.1

运行成功界面如下:

在浏览器中打开

使用可以查看提供的元数据和uri:

这样一个可用于测试的OAuth2.0的服务器应用就准备好了。

开发环境

当前项目使用以下开发环境:

操作系统:Windows 11JDK 17数据库:PostgreSQL 15.2IDE:VS Code(版本1.83.1),并安装以下插件:

Extension Pack for Java

Spring Boot Extension Pack

创建测试数据库

使用postgres用户登录PostgreSQL数据库:

psql -h localhost -U postgres

输入postgres用户密码后登录成功,之后使用下面命令创建数据库test。

CREATE DATABASE book_database;

创建Spring Boot项目指定项目语言:Java输入group id:com.taylor输入artifact:spring-security-oauth2-api指定包类型:jar指定java版本:17选择所需依赖:

Lombok

Spring Boot DevTools

Spring Web

OAuth2 Resource Server

Spring Data JPA

PostgreSQL Driver SQL

按回车键后,系统提示选择保存项目文件夹,指定文件后按回车键创建项目完成。还需要另外添加依赖:spring-security-oauth2-jose,它包含 Spring Security 对 JOSE(Javascript 对象签名和加密)框架的支持。 集成JPA

请参考文章:Spring Boot中集成JPA和使用PostgreSQL数据库

实现API基本功能

本API应用实现了基本书籍管理功能:

按页获取所有书籍记录录入一本新书信息按ID取书籍信息修改书籍信息按ID删除书籍

Controller代码如下:

package com.taylor.oauth2.controllers;import java.net.URI;import java.util.Optional;import org.springframework.data.domain.Page;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.DeleteMapping;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.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.support.ServletUriComponentsBuilder;import com.taylor.oauth2.orm.entities.Book;import com.taylor.oauth2.services.BookService;@RestController@RequestMapping("/api/v1/book")public class BookController {    private final BookService service;    public BookController(BookService service) {        this.service = service;    }    // Get books by page    @GetMapping()    public Page<Book> getBooksByPage(@RequestParam Optional<Integer> page,            @RequestParam Optional<Integer> size) {        return service.getBooksByPage(page, size);    }    // Create a new book    @PostMapping()    public ResponseEntity<Void> createBook(@RequestBody Book book) {        Book newBook = service.createBook(book);        if (newBook == null) {            return ResponseEntity.noContent().build();        }        URI uri = ServletUriComponentsBuilder                .fromCurrentRequest()                .path("/{id}")                .buildAndExpand(newBook.getId())                .toUri();        return ResponseEntity.created(uri).build();    }    // Get book by id    @GetMapping("/{id}")    public ResponseEntity<Book> getBookById(@PathVariable Long id) {        return ResponseEntity.ok(service.getBookById(id));    }    // Update a book    @PutMapping("/{id}")    public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book bookDetails) {        Book updatedBook = service.updateBook(id, bookDetails);        return ResponseEntity.ok(updatedBook);    }    // Delete a book    @DeleteMapping("/{id}")    public ResponseEntity<HttpStatus> deleteById(@PathVariable Long id) {        service.deleteStuduent(id);        return new ResponseEntity<HttpStatus>(HttpStatus.ACCEPTED);    }}
启用Spring Security

创建一个安全配置类,并使用@EnableWebSecurity注解来开启Spring Security。

@Configuration@EnableWebSecuritypublic class SecurityConfig {	…}

对于Spring Security来说,每一个OAuth2 Scope将会映射到一个使用前缀SCOPE_的Authority,比如Scope “read”对应Authority为“SCOPE_read”。

我们需要对书籍API提供的REST API添加权限控制,设置权限代码格式如下:

.authorizeHttpRequests(authorize -> authorize                                                    .requestMatchers(HttpMethod.POST, "/api/v1/book/**")    .hasAuthority(SCOPE_BOOK_WRITE)                                                    .anyRequest().authenticated())

对应API功能,它和Scope的映射关系如下:

按页获取所有书籍记录:客户端具有Scope apiBook.read或apiBook.write录入一本新书信息:客户端具有Scope apiBook.write按ID取书籍信息:客户端具有Scope apiBook.write修改书籍信息:客户端具有Scope apiBook.write按ID删除书籍:客户端具有Scope apiBook.write

整个权限控制实现代码如下:

package com.taylor.oauth2.configuration;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.HttpMethod;import org.springframework.security.config.Customizer;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.oauth2.jwt.JwtDecoder;import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;import org.springframework.security.web.SecurityFilterChain;import com.nimbusds.jose.JOSEObjectType;import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;import com.nimbusds.jose.proc.SecurityContext;@Configuration@EnableWebSecuritypublic class SecurityConfig {        private static final String AUTHORITY_PREFIX = "SCOPE_";        private static final String SCOPE_BOOK = AUTHORITY_PREFIX + "apiBook";        private static final String SCOPE_BOOK_READ = SCOPE_BOOK + ".read";        private static final String SCOPE_BOOK_WRITE = SCOPE_BOOK + ".write";        @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")        private String issuerUri;        @Bean        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {                http                                .authorizeHttpRequests(authorize -> authorize                                                .requestMatchers(HttpMethod.GET, "/api/v1/book/**")                                                .hasAnyAuthority(SCOPE_BOOK_READ, SCOPE_BOOK_WRITE)                                                .requestMatchers(HttpMethod.POST, "/api/v1/book/**")                                                .hasAuthority(SCOPE_BOOK_WRITE)                                                .requestMatchers(HttpMethod.PUT, "/api/v1/book/**")                                                .hasAuthority(SCOPE_BOOK_WRITE)                                                .requestMatchers(HttpMethod.DELETE, "/api/v1/book/**")                                                .hasAuthority(SCOPE_BOOK_WRITE)                                                .anyRequest().authenticated())                                .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));                return http.build();        }        @Bean        public JwtDecoder jwtDecoder() {                // By default, Spring don't allow JWT header type 'at+jwt', need to manually                // allow like below                DefaultJOSEObjectTypeVerifier<SecurityContext> verifier = new DefaultJOSEObjectTypeVerifier<>(                                new JOSEObjectType("at+jwt"));                // NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)                NimbusJwtDecoder decoder = NimbusJwtDecoder.withIssuerLocation(this.issuerUri)                                .jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier(verifier))                                .build();                return decoder;        }}
测试

我们使用的Angular客户端只具有apiBook.read Scope,它将只有读取数据功能。

进入Angular客户端代码目录下使用如下命令运行应用:

ng serve

如上表示Angular客户端运行成功。

打开浏览器输入,客户端跳转到安全令牌服务提供者应用的登录窗口

输入上面提示的测试用户名和密码(用户名:alice或bob, 密码:Pass123$),登录成功,系统跳转到Angular客户端并显示从API获取的数据:

再使用添加、修改或删除功能,系统跳转到未授权页面:

我们再直接访问API用来获取单页数据接口:

系统提示401,整个API都是在安全令牌服务提供者应用保护之下。

谢谢观看!

标签: #aspnetrequire未定义