如果您正在寻找JWT实施,请点击此链接
本指南逐步介绍了使用Spring Boot 2创建集中式身份验证和授权服务器的过程,还将提供演示资源服务器。
如果您不熟悉OAuth2,建议您阅读此书。
先决条件
- JDK 1.8
- 文本编辑器或您喜欢的IDE
- Maven 3.0+
实施概述
对于这个项目,我们将通过Spring Boot使用Spring Security 5 。 如果您熟悉早期版本,那么《 Spring Boot迁移指南》可能会有用。
OAuth2术语
- 资源所有者
- 授权应用程序访问其帐户的用户。
- 资源服务器 :
- 在
client
获取access token
之后处理已认证请求的服务器。
- 在
- 客户
- 代表资源所有者访问受保护资源的应用程序。
- 授权服务器
- 在成功验证
client
和resource owner
并授权请求之后,发出访问令牌的服务器。
- 在成功验证
- 访问令牌
- 用于访问受保护资源的唯一令牌
- 范围
- 许可
- 赠款类型
grant
是一种获取访问令牌的方法。
授权服务器
为了构建我们的Authorization Server
我们将通过Spring Boot 2.0.x使用Spring Security5.x 。
依存关系
您可以转到start.spring.io并生成一个新项目,然后添加以下依赖项:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
数据库
出于本指南的考虑,我们将使用H2数据库 。
在这里,您可以找到Spring Security所需的参考OAuth2 SQL模式。
CREATE TABLE IF NOT EXISTS oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256) NOT NULL,
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4000),
autoapprove VARCHAR(256)
);
CREATE TABLE IF NOT EXISTS oauth_client_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
CREATE TABLE IF NOT EXISTS oauth_access_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256),
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication BLOB,
refresh_token VARCHAR(256)
);
CREATE TABLE IF NOT EXISTS oauth_refresh_token (
token_id VARCHAR(256),
token BLOB,
authentication BLOB
);
CREATE TABLE IF NOT EXISTS oauth_code (
code VARCHAR(256), authentication BLOB
);
然后添加以下条目
-- The encrypted client_secret it `secret`
INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, authorities, access_token_validity)
VALUES ('clientId', '{bcrypt}$2a$10$vCXMWCn7fDZWOcLnIEhmK.74dvK1Eh8ae2WrWlhr2ETPLoxQctN4.', 'read,write', 'password,refresh_token,client_credentials', 'ROLE_CLIENT', 300);
上面的
client_secret
是使用bcrypt生成的。
前缀{bcrypt}
是必需的,因为我们将使用Spring Security 5.x的DelegatingPasswordEncoder的新功能。
在下面的页面中,您可以找到Spring的org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl
使用的User
和Authority
参考SQL模式。
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(256) NOT NULL,
password VARCHAR(256) NOT NULL,
enabled TINYINT(1),
UNIQUE KEY unique_username(username)
);
CREATE TABLE IF NOT EXISTS authorities (
username VARCHAR(256) NOT NULL,
authority VARCHAR(256) NOT NULL,
PRIMARY KEY(username, authority)
);
与之前相同,为用户及其权限添加以下条目。
-- The encrypted password is `pass`
INSERT INTO users (id, username, password, enabled) VALUES (1, 'user', '{bcrypt}$2a$10$cyf5NfobcruKQ8XGjUJkEegr9ZWFqaea6vjpXWEaSqTa2xL9wjgQC', 1);
INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');
Spring安全配置
添加以下Spring配置类。
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.sql.DataSource;
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final DataSource dataSource;
private PasswordEncoder passwordEncoder;
private UserDetailsService userDetailsService;
public WebSecurityConfiguration(final DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
if (passwordEncoder == null) {
passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
return passwordEncoder;
}
@Bean
public UserDetailsService userDetailsService() {
if (userDetailsService == null) {
userDetailsService = new JdbcDaoImpl();
((JdbcDaoImpl) userDetailsService).setDataSource(dataSource);
}
return userDetailsService;
}
}
引用Spring Blog :
@EnableWebSecurity批注和WebSecurityConfigurerAdapter一起提供基于Web的安全性。
如果您使用的是Spring Boot,则将自动配置DataSource
对象,您可以将其注入到类中,而不必自己定义。 需要将其注入到UserDetailsService
中,该服务将使用Spring Security提供的JdbcDaoImpl
,如有必要,您可以将其替换为自己的实现。
由于某些自动配置的Spring @Bean
需要Spring Security的AuthenticationManager
因此有必要重写authenticationManagerBean
方法,并以@Bean
authenticationManagerBean
注释。
PasswordEncoder
将由PasswordEncoderFactories.createDelegatingPasswordEncoder()
处理,其中基于前缀处理一些密码编码器和委托,在我们的示例中,我们使用{bcrypt}
作为密码的前缀。
授权服务器配置
授权服务器验证client
和user
凭证并提供令牌。
添加以下Spring配置类。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private TokenStore tokenStore;
public AuthorizationServerConfiguration(final DataSource dataSource, final PasswordEncoder passwordEncoder,
final AuthenticationManager authenticationManager) {
this.dataSource = dataSource;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
}
@Bean
public TokenStore tokenStore() {
if (tokenStore == null) {
tokenStore = new JdbcTokenStore(dataSource);
}
return tokenStore;
}
@Bean
public DefaultTokenServices tokenServices(final ClientDetailsService clientDetailsService) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore());
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setAuthenticationManager(authenticationManager);
return tokenServices;
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore());
}
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.passwordEncoder(passwordEncoder)
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}
用户信息端点
现在,我们需要定义一个端点,在该端点上可以将授权令牌解码为Authorization
对象,以添加以下类。
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequestMapping("/profile")
public class UserController {
@GetMapping("/me")
public ResponseEntity
get(final Principal principal) {
return ResponseEntity.ok(principal);
}
}
资源服务器配置
资源服务器托管HTTP资源 ,其中的HTTP资源可以是文档,照片或其他内容,在我们的情况下,它将是受OAuth2保护的REST API。
依存关系
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
定义我们受保护的API
下面的代码定义了端点/me
并返回Principal
对象,它要求经过身份验证的用户具有ROLE_USER
的访问权限。
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
@RequestMapping("/me")
public class UserController {
@GetMapping
@PreAuthorize("hasRole('ROLE_USER')")
public ResponseEntity<Principal> get(final Principal principal) {
return ResponseEntity.ok(principal);
}
}
@PreAuthorize
批注会在执行代码之前验证用户是否具有给定角色,以使其正常工作,有必要启用prePost
批注,为此添加以下类:
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration {
}
这里的重要部分是@EnableGlobalMethodSecurity(prePostEnabled = true)
批注, prePostEnabled
标志默认情况下设置为false
,将其设置为true
可使@PreAuthorize
批注起作用。
资源服务器配置
现在,让我们为资源服务器添加Spring的配置。
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
}
来自Javadoc的@EnableResourceServer
批注:
OAuth2资源服务器的便捷注释,可启用Spring Security过滤器,该过滤器通过传入的OAuth2令牌对请求进行身份验证。 用户应添加此批注并提供类型为{@link ResourceServerConfigurer}的
@Bean
(例如,通过{@link ResourceServerConfigurerAdapter}),用于指定资源的详细信息(URL路径和资源ID)。 为了使用此过滤器,您必须在应用程序中的某个位置{@link EnableWebSecurity},使用该注释的位置相同,也可以使用其他位置。
现在我们已经准备好所有必需的代码,我们需要配置RemoteTokenServices ,对我们来说幸运的是,Spring提供了一个配置属性,可以在其中设置可以将令牌转换为Authentication
对象的url。
security:
oauth2:
resource:
user-info-uri: http://localhost:9001/profile/me
一起测试
为了一起测试,我们需要同时旋转Authorization Server
和Resource Server
,在我的设置中,它将相应地在端口9001
和9101
上运行。
生成令牌
$ curl -u clientId:secret -X POST localhost:9001/oauth/token\?grant_type=password\&username=user\&password=pass
{
"access_token" : "e47876b0-9962-41f1-ace3-e3381250ccea",
"token_type" : "bearer",
"refresh_token" : "8e17a71c-cb39-4904-8205-4d9f8c71aeef",
"expires_in" : 299,
"scope" : "read write"
}
访问资源
既然已经生成了令牌,请复制access_token
并将其添加到Authorization
HTTP Header上的请求中,例如:
$ curl -i localhost:9101/me -H "Authorization: Bearer c06a4137-fa07-4d9a-97f9-85d1ba820d3a"
{
"authorities" : [ {
"authority" : "ROLE_USER"
} ],
"details" : {
"remoteAddress" : "127.0.0.1",
"sessionId" : null,
"tokenValue" : "c06a4137-fa07-4d9a-97f9-85d1ba820d3a",
"tokenType" : "Bearer",
"decodedDetails" : null
},
"authenticated" : true,
"userAuthentication" : {
"authorities" : [ {
"authority" : "ROLE_USER"
} ],
"details" : {
"authorities" : [ {
"authority" : "ROLE_USER"
} ],
"details" : {
"remoteAddress" : "127.0.0.1",
"sessionId" : null,
"tokenValue" : "c06a4137-fa07-4d9a-97f9-85d1ba820d3a",
"tokenType" : "Bearer",
"decodedDetails" : null
},
"authenticated" : true,
"userAuthentication" : {
"authorities" : [ {
"authority" : "ROLE_USER"
} ],
"details" : {
"grant_type" : "password",
"username" : "user"
},
"authenticated" : true,
"principal" : {
"password" : null,
"username" : "user",
"authorities" : [ {
"authority" : "ROLE_USER"
} ],
"accountNonExpired" : true,
"accountNonLocked" : true,
"credentialsNonExpired" : true,
"enabled" : true
},
"credentials" : null,
"name" : "user"
},
"clientOnly" : false,
"oauth2Request" : {
"clientId" : "clientId",
"scope" : [ "read", "write" ],
"requestParameters" : {
"grant_type" : "password",
"username" : "user"
},
"resourceIds" : [ ],
"authorities" : [ {
"authority" : "ROLE_CLIENT"
} ],
"approved" : true,
"refresh" : false,
"redirectUri" : null,
"responseTypes" : [ ],
"extensions" : { },
"grantType" : "password",
"refreshTokenRequest" : null
},
"credentials" : "",
"principal" : {
"password" : null,
"username" : "user",
"authorities" : [ {
"authority" : "ROLE_USER"
} ],
"accountNonExpired" : true,
"accountNonLocked" : true,
"credentialsNonExpired" : true,
"enabled" : true
},
"name" : "user"
},
"authenticated" : true,
"principal" : "user",
"credentials" : "N/A",
"name" : "user"
},
"principal" : "user",
"credentials" : "",
"clientOnly" : false,
"oauth2Request" : {
"clientId" : null,
"scope" : [ ],
"requestParameters" : { },
"resourceIds" : [ ],
"authorities" : [ ],
"approved" : true,
"refresh" : false,
"redirectUri" : null,
"responseTypes" : [ ],
"extensions" : { },
"grantType" : null,
"refreshTokenRequest" : null
},
"name" : "user"
}