java rest api oauth,Spring REST API + OAuth2 + AngularJS

本文详细介绍了如何使用OAuth2进行REST API的授权保护,并通过创建一个包含隐式授权和密码授权的AngularJS客户端进行演示。涉及Spring Boot授权服务器配置、资源服务器搭建、数据库连接及JWT Token处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、概述

在本教程中,我们将使用 OAuth 来保护 REST API,并通过一个简单的 AngularJS 客户端进行示范。

我们要建立的应用将包含了四个独立模块:

授权服务器

资源服务器

UI implicit —— 一个使用 Implicit Flow 的前端应用

UI password —— 一个使用 Password Flow 的前端应用

2、授权服务器

首先,让我们先搭建一个简单的 Spring Boot 应用作为授权服务器。

2.1、Maven 配置

添加以下依赖:

org.springframework.boot

spring-boot-starter-web

org.springframework

spring-jdbc

mysql

mysql-connector-java

runtime

org.springframework.security.oauth

spring-security-oauth2

${oauth.version}

上面使用了 spring-jdbc 和 MySQL,因为我们将使用 JDBC 来实现 token 存储。

2.2、@EnableAuthorizationServer

现在,我们来配置负责管理 Access Token(访问令牌)的授权服务器:

@Configuration

@EnableAuthorizationServer

public class AuthServerOAuth2Config

extends AuthorizationServerConfigurerAdapter {

@Autowired

@Qualifier("authenticationManagerBean")

private AuthenticationManager authenticationManager;

@Override

public void configure(

AuthorizationServerSecurityConfigurer oauthServer)

throws Exception {

oauthServer

.tokenKeyAccess("permitAll()")

.checkTokenAccess("isAuthenticated()");

}

@Override

public void configure(ClientDetailsServiceConfigurer clients)

throws Exception {

clients.jdbc(dataSource())

.withClient("sampleClientId")

.authorizedGrantTypes("implicit")

.scopes("read")

.autoApprove(true)

.and()

.withClient("clientIdPassword")

.secret("secret")

.authorizedGrantTypes(

"password","authorization_code", "refresh_token")

.scopes("read");

}

@Override

public void configure(

AuthorizationServerEndpointsConfigurer endpoints)

throws Exception {

endpoints

.tokenStore(tokenStore())

.authenticationManager(authenticationManager);

}

@Bean

public TokenStore tokenStore() {

return new JdbcTokenStore(dataSource());

}

}

注意:

为了持久化 token,我们使用了一个 JdbcTokenStore

我们为 implicit 授权类型注册了一个客户端

我们注册了另一个客户端,授权了 password、authorization_code 和 refresh_token 等授权类型

为了使用 password 授权类型,我们需要装配并使用 AuthenticationManager bean

2.3、数据源配置

接下来,让我们为 JdbcTokenStore 配置数据源:

@Value("classpath:schema.sql")

private Resource schemaScript;

@Bean

public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {

DataSourceInitializer initializer = new DataSourceInitializer();

initializer.setDataSource(dataSource);

initializer.setDatabasePopulator(databasePopulator());

return initializer;

}

private DatabasePopulator databasePopulator() {

ResourceDatabasePopulator populator = new ResourceDatabasePopulator();

populator.addScript(schemaScript);

return populator;

}

@Bean

public DataSource dataSource() {

DriverManagerDataSource dataSource = new DriverManagerDataSource();

dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));

dataSource.setUrl(env.getProperty("jdbc.url"));

dataSource.setUsername(env.getProperty("jdbc.user"));

dataSource.setPassword(env.getProperty("jdbc.pass"));

return dataSource;

}

请注意,由于我们使用了 JdbcTokenStore,需要初始化数据库 schema(模式),因此我们使用了 DataSourceInitializer,和以下 SQL schema:

drop table if exists oauth_client_details;

create table oauth_client_details (

client_id VARCHAR(255) PRIMARY KEY,

resource_ids VARCHAR(255),

client_secret VARCHAR(255),

scope VARCHAR(255),

authorized_grant_types VARCHAR(255),

web_server_redirect_uri VARCHAR(255),

authorities VARCHAR(255),

access_token_validity INTEGER,

refresh_token_validity INTEGER,

additional_information VARCHAR(4096),

autoapprove VARCHAR(255)

);

drop table if exists oauth_client_token;

create table oauth_client_token (

token_id VARCHAR(255),

token LONG VARBINARY,

authentication_id VARCHAR(255) PRIMARY KEY,

user_name VARCHAR(255),

client_id VARCHAR(255)

);

drop table if exists oauth_access_token;

create table oauth_access_token (

token_id VARCHAR(255),

token LONG VARBINARY,

authentication_id VARCHAR(255) PRIMARY KEY,

user_name VARCHAR(255),

client_id VARCHAR(255),

authentication LONG VARBINARY,

refresh_token VARCHAR(255)

);

drop table if exists oauth_refresh_token;

create table oauth_refresh_token (

token_id VARCHAR(255),

token LONG VARBINARY,

authentication LONG VARBINARY

);

drop table if exists oauth_code;

create table oauth_code (

code VARCHAR(255), authentication LONG VARBINARY

);

drop table if exists oauth_approvals;

create table oauth_approvals (

userId VARCHAR(255),

clientId VARCHAR(255),

scope VARCHAR(255),

status VARCHAR(10),

expiresAt TIMESTAMP,

lastModifiedAt TIMESTAMP

);

drop table if exists ClientDetails;

create table ClientDetails (

appId VARCHAR(255) PRIMARY KEY,

resourceIds VARCHAR(255),

appSecret VARCHAR(255),

scope VARCHAR(255),

grantTypes VARCHAR(255),

redirectUrl VARCHAR(255),

authorities VARCHAR(255),

access_token_validity INTEGER,

refresh_token_validity INTEGER,

additionalInformation VARCHAR(4096),

autoApproveScopes VARCHAR(255)

);

需要注意的是,我们不一定需要显式声明 DatabasePopulator bean —— 我们可以简单地使用一个 schema.sql —— Spring Boot 默认。

2.4、安全配置

最后,让我们将授权服务器变得更加安全。

当客户端应用需要获取一个 Access Token 时,在一个简单的表单登录驱动验证处理之后,它将执行此操作:

@Configuration

public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {

@Override

protected void configure(AuthenticationManagerBuilder auth)

throws Exception {

auth.inMemoryAuthentication()

.withUser("john").password("123").roles("USER");

}

@Override

@Bean

public AuthenticationManager authenticationManagerBean()

throws Exception {

return super.authenticationManagerBean();

}

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests()

.antMatchers("/login").permitAll()

.anyRequest().authenticated()

.and()

.formLogin().permitAll();

}

}

这里的需要提及的是,Password flow 不需要表单登录配置 —— 仅限于 Implicit flow,因此你可以根据你使用的 OAuth2 flow 跳过它。

3、资源服务器

现在,我们来讨论一下资源服务器;本质上就是我们想要消费的 REST API。

3.1、Maven 配置

我们的资源服务器配置与之前的授权服务器应用配置相同。

3.2、Token 存储配置

接下来,我们将配置 TokenStore 来访问与授权服务器用于存储 Access Token 相同的数据库:

@Autowired

private Environment env;

@Bean

public DataSource dataSource() {

DriverManagerDataSource dataSource = new DriverManagerDataSource();

dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));

dataSource.setUrl(env.getProperty("jdbc.url"));

dataSource.setUsername(env.getProperty("jdbc.user"));

dataSource.setPassword(env.getProperty("jdbc.pass"));

return dataSource;

}

@Bean

public TokenStore tokenStore() {

return new JdbcTokenStore(dataSource());

}

请注意,针对这个简单的实现,即使授权服务器与资源服务器是单独的应用,我们也共享着 token 存储的 SQL。

原因当然是资源服务器需要能够验证授权服务器发出的 Access Token 的有效性。

3.3、远程 Token 服务

我们需要使用 RemoteTokeServices,而不是 TokenStore:

@Primary

@Bean

public RemoteTokenServices tokenService() {

RemoteTokenServices tokenService = new RemoteTokenServices();

tokenService.setCheckTokenEndpointUrl(

"http://localhost:8080/spring-security-oauth-server/oauth/check_token");

tokenService.setClientId("fooClientIdPassword");

tokenService.setClientSecret("secret");

return tokenService;

}

注意:

该 RemoteTokenService 将使用授权服务器上的 CheckTokenEndPoint 来验证 AccessToken 并从中获取 Authentication 对象。

可以在 AuthorizationServerBaseURL + /oauth/check_token 找到

授权服务器可以使用任何 TokenStore 类型 [JdbcTokenStore、JwtTokenStore、……] —— 这不会影响到 RemoteTokenService 或者资源服务器。

3.4、一个简单的控制器

接下来实现一个简单控制器以暴露一个 Foo 资源:

@Controller

public class FooController {

@PreAuthorize("#oauth2.hasScope('read')")

@RequestMapping(method = RequestMethod.GET, value = "/foos/{id}")

@ResponseBody

public Foo findById(@PathVariable long id) {

return

new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));

}

}

请注意客户端需要 read scope(范围、作用域或权限)访问此资源。

我们还需要开启全局方法保护并配置 MethodSecurityExpressionHandler:

@Configuration

@EnableResourceServer

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class OAuth2ResourceServerConfig

extends GlobalMethodSecurityConfiguration {

@Override

protected MethodSecurityExpressionHandler createExpressionHandler() {

return new OAuth2MethodSecurityExpressionHandler();

}

}

以下是基础的 Foo 资源:

public class Foo {

private long id;

private String name;

}

3.5、Web 配置

最后,为 API 设置一个非常基本的 Web 配置:

@Configuration

@EnableWebMvc

@ComponentScan({ "org.baeldung.web.controller" })

public class ResourceWebConfig extends WebMvcConfigurerAdapter {}

4、前端 - Password Flow

来看看一个简单的前端 AngularJS 客户端实现。

我们将在这里使用 OAuth2 Password flow —— 这就是为什么这只是一个示例,而不是一个可用于生产的应用。你会注意到,客户端凭据被暴露在前端 —— 这也是我们将来在以后的文章中要讨论的。

我们从两个简单的页面开始 - “index” 和 “login”;一旦用户提供凭据,前端 JS 客户端将使用它们从授权服务器获取的一个 Access Token。

4.1、登录页面

以下是一个简单的登录页面:

Login

Username

Password

Login

4.2、获取 Access Token

现在,让我们来看看如何获取 Access Token:

var app = angular.module('myApp', ["ngResource","ngRoute","ngCookies"]);

app.controller('mainCtrl',

function($scope, $resource, $http, $httpParamSerializer, $cookies) {

$scope.data = {

grant_type:"password",

username: "",

password: "",

client_id: "clientIdPassword"

};

$scope.encoded = btoa("clientIdPassword:secret");

$scope.login = function() {

var req = {

method: 'POST',

url: "http://localhost:8080/spring-security-oauth-server/oauth/token",

headers: {

"Authorization": "Basic " + $scope.encoded,

"Content-type": "application/x-www-form-urlencoded; charset=utf-8"

},

data: $httpParamSerializer($scope.data)

}

$http(req).then(function(data){

$http.defaults.headers.common.Authorization =

'Bearer ' + data.data.access_token;

$cookies.put("access_token", data.data.access_token);

window.location.href="index";

});

}

});

注意:

我们发送一个 POST 到 /oauth/token 端点以获取一个 Access Token

我们使用客户端凭据和 Basic Auth 验证来访问此端点

之后我们发送用户凭证以及客户端 id 和授权类型参数的 URL 编码

获取 Access Token 后,我们将其存储在一个 cookie 中

cookie 存储在这里特别重要,因为我们只使用 cookie 作为存储目标,而不是直接发动身份验证过程。这有助于防止跨站点请求伪造(CSRF)类型的攻击和漏洞。

4.3、主页面

以下是一个简单的主页面:

Foo Details

ID{{foo.id}}

Name{{foo.name}}

New Foo

4.4、授权客户端请求

由于我们需要 Access Token 为对资源的请求进行授权,我们将追加一个带有 Access Token 的简单授权头:

var isLoginPage = window.location.href.indexOf("login") != -1;

if(isLoginPage){

if($cookies.get("access_token")){

window.location.href = "index";

}

} else{

if($cookies.get("access_token")){

$http.defaults.headers.common.Authorization =

'Bearer ' + $cookies.get("access_token");

} else{

window.location.href = "login";

}

}

没有找到 cookie,用户将跳转到登录页面。

5.前端 —— 隐式授权(Implicit Grant)

现在,我们来看看使用了隐式授权的客户端应用。

我们的客户端应用是一个独立的模块,尝试使用隐式授权流程从授权服务器获取 Access Token 后访问资源服务器。

5.1、Maven 配置

这里是 pom.xml 依赖:

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-thymeleaf

注意:我们不需要 OAuth 依赖,因为我们将使用 AngularJS 的 OAuth-ng 指令来处理,其可以使用隐式授权流程连接到 OAuth2 服务器。

5.2、Web 配置

以下是我们的一个简单的 Web 配置:

@Configuration

@EnableWebMvc

public class UiWebConfig extends WebMvcConfigurerAdapter {

@Bean

public static PropertySourcesPlaceholderConfigurer

propertySourcesPlaceholderConfigurer() {

return new PropertySourcesPlaceholderConfigurer();

}

@Override

public void configureDefaultServletHandling(

DefaultServletHandlerConfigurer configurer) {

configurer.enable();

}

@Override

public void addViewControllers(ViewControllerRegistry registry) {

super.addViewControllers(registry);

registry.addViewController("/index");

registry.addViewController("/oauthTemplate");

}

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler("/resources/**")

.addResourceLocations("/resources/");

}

}

5.3、主页

接下来,这里是我们的主页:

OAuth-ng 指令需要:

site:授权服务器 URL

client-id:应用客户端 id

redirect-uri:从授权服务器获 Access Token 后,要重定向到的 URI

scope:从授权服务器请求的权限

template:渲染自定义 HTML 模板

site="http://localhost:8080/spring-security-oauth-server"

client-id="clientId"

redirect-uri="http://localhost:8080/spring-security-oauth-ui-implicit/index"

scope="read"

template="oauthTemplate">

Foo Details

ID{{foo.id}}

Name{{foo.name}}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值