Spring Boot - OAuth2 与 JWT
在本章中,您将详细了解 Spring Boot Security 机制以及使用 JWT 的 OAuth2。
注意 − 此示例使用 Spring Boot 1.5.9 编写。在 Spring Boot 3 中,Spring Security Authorization Server 已被弃用。
授权服务器
Authorization Server 是 Web API Security 的核心架构组件。Authorization Server 充当集中式授权点,允许您的应用和 HTTP 端点识别应用程序的功能。
资源服务器
Resource Server 是一个向客户端提供 access token 以访问 Resource Server HTTP Endpoints 的应用。它是一组包含 HTTP Endpoints、静态资源和动态网页的库集合。
OAuth2
OAuth2 是一个授权框架,使应用 Web Security 能够从客户端访问资源。要构建 OAuth2 应用,我们需要关注 Grant Type(Authorization code)、Client ID 和 Client secret。
JWT Token
JWT Token 是一种 JSON Web Token,用于表示两方之间安全的声明。您可以在 www.jwt.io/ 上了解更多关于 JWT Token 的信息。
现在,我们将构建一个 OAuth2 应用程序,通过 JWT Token 的帮助来启用 Authorization Server 和 Resource Server 的使用。
您可以使用以下步骤,通过访问数据库来实现带有 JWT Token 的 Spring Boot Security。
首先,我们需要在构建配置文件中添加以下依赖。
Maven 用户可以在 pom.xml 文件中添加以下依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
Gradle 用户可以在 build.gradle 文件中添加以下依赖。
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
compile("org.springframework.security.oauth:spring-security-oauth2")
compile('org.springframework.security:spring-security-jwt')
compile("org.springframework.boot:spring-boot-starter-jdbc")
compile("com.h2database:h2:1.4.191")
其中,
Spring Boot Starter Security − 实现 Spring Security
Spring Security OAuth2 − 实现 OAUTH2 结构,以启用 Authorization Server 和 Resource Server。
Spring Security JWT − 为 Web security 生成 JWT Token
Spring Boot Starter JDBC − 访问数据库以确保用户是否存在。
Spring Boot Starter Web − 编写 HTTP endpoints。
H2 Database − 存储用于认证和授权的用户信息。
完整的构建配置文件如下所示。
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.</groupId>
<artifactId>websecurityapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>websecurityapp</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Gradle build.gradle
buildscript {
ext {
springBootVersion = '1.5.9.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
group = 'com.'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
compile("org.springframework.security.oauth:spring-security-oauth2")
compile('org.springframework.security:spring-security-jwt')
compile("org.springframework.boot:spring-boot-starter-jdbc")
compile("com.h2database:h2:1.4.191")
}
现在,在主 Spring Boot 应用程序中,添加 @EnableAuthorizationServer 和 @EnableResourceServer 注解,使同一应用程序充当 Auth server 和 Resource Server。
此外,您可以使用以下代码编写一个简单的 HTTP endpoint,通过使用 JWT Token 和 Spring Security 来访问 API。
WebsecurityappApplication.java
package com..websecurityapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableAuthorizationServer
@EnableResourceServer
@RestController
public class WebsecurityappApplication {
public static void main(String[] args) {
SpringApplication.run(WebsecurityappApplication.class, args);
}
@RequestMapping(value = "/products")
public String getProductName() {
return "Honey";
}
}
使用以下代码定义 POJO class 来存储用于认证的 User 信息。
UserEntity.java
package com..websecurityapp;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public class UserEntity {
private String username;
private String password;
private Collection<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Collection<GrantedAuthority> getGrantedAuthoritiesList() {
return grantedAuthoritiesList;
}
public void setGrantedAuthoritiesList(Collection<GrantedAuthority> grantedAuthoritiesList) {
this.grantedAuthoritiesList = grantedAuthoritiesList;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
现在,使用以下代码定义 CustomUser class,它扩展了 org.springframework.security.core.userdetails.User class,用于 Spring Boot 认证。
CustomUser.java
package com..websecurityapp;
import org.springframework.security.core.userdetails.User;
public class CustomUser extends User {
private static final long serialVersionUID = 1L;
public CustomUser(UserEntity user) {
super(user.getUsername(), user.getPassword(), user.getGrantedAuthoritiesList());
}
}
您可以创建 @Repository class 从数据库读取 User 信息并发送到 Custom user service,同时添加 granted authority ROLE_SYSTEMADMIN。
OAuthDao.java
package com..websecurityapp;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Repository;
@Repository
public class OAuthDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public UserEntity getUserDetails(String username) {
Collection<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
String userSQLQuery = "SELECT * FROM USERS WHERE USERNAME=?";
List<UserEntity> list = jdbcTemplate.query(userSQLQuery, new String[] { username },
(ResultSet rs, int rowNum) -> {
UserEntity user = new UserEntity();
user.setUsername(username);
user.setPassword(rs.getString("PASSWORD"));
return user;
});
if (list.size() > 0) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_SYSTEMADMIN");
grantedAuthoritiesList.add(grantedAuthority);
list.get(0).setGrantedAuthoritiesList(grantedAuthoritiesList);
return list.get(0);
}
return null;
}
}
您可以创建一个 Custom User detail service class,它扩展了 org.springframework.security.core.userdetails.UserDetailsService 来调用 DAO repository class,如下所示。
CustomDetailsService.java
package com..websecurityapp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomDetailsService implements UserDetailsService {
@Autowired
OAuthDao oauthDao;
@Override
public CustomUser loadUserByUsername(final String username) throws UsernameNotFoundException {
UserEntity userEntity = null;
try {
userEntity = oauthDao.getUserDetails(username);
CustomUser customUser = new CustomUser(userEntity);
return customUser;
} catch (Exception e) {
e.printStackTrace();
throw new UsernameNotFoundException("User " + username + " was not found in the database");
}
}
}
接下来,创建一个 @Configuration class 来启用 Web Security,定义 Password encoder (BCryptPasswordEncoder),并定义 AuthenticationManager bean。Security configuration class 应该扩展 WebSecurityConfigurerAdapter class。
SecurityConfiguration.java
package com..websecurityapp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private CustomDetailsService customDetailsService;
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Override
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customDetailsService).passwordEncoder(encoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.NEVER);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
现在,定义 OAuth2 Configuration class 来添加 Client ID、Client Secret,定义 JwtAccessTokenConverter、私钥和公钥用于 token signer key 和 verifier key,并配置 ClientDetailsServiceConfigurer 以设置 Token 的有效期和 scopes。
OAuth2Config.java
package com..websecurityapp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
private String clientid = "";
private String clientSecret = "my-secret-key";
private String privateKey = "private key";
private String publicKey = "public key";
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Bean
public JwtAccessTokenConverter tokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(privateKey);
converter.setVerifierKey(publicKey);
return converter;
}
@Bean
public JwtTokenStore tokenStore() {
return new JwtTokenStore(tokenEnhancer());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore())
.accessTokenConverter(tokenEnhancer());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient(clientid).secret(clientSecret).scopes("read", "write")
.authorizedGrantTypes("password", "refresh_token").accessTokenValiditySeconds(20000)
.refreshTokenValiditySeconds(20000);
}
}
密钥创建
现在,使用 openssl 创建私钥和公钥。
您可以使用以下命令生成私钥。
openssl genrsa -out jwt.pem 2048 openssl rsa -in jwt.pem
对于公钥生成,请使用以下命令。
openssl rsa -in jwt.pem -pubout
对于 Spring Boot 1.5 版本之后的版本,请在 application.properties 文件中添加以下属性,以定义 OAuth2 Resource filter order。
security.oauth2.resource.filter-order=3
YAML 文件用户可以在 YAML 文件中添加以下属性。
security:
oauth2:
resource:
filter-order: 3
现在,在 classpath resources src/main/resources/目录 下创建 schema.sql 和 data.sql 文件,以将应用程序连接到 H2 database。
schema.sql 文件如下所示 −
schema.sql
CREATE TABLE USERS (ID INT PRIMARY KEY, USERNAME VARCHAR(45), PASSWORD VARCHAR(60));
data.sql 文件如下所示 −
data.sql
INSERT INTO USERS (ID, USERNAME,PASSWORD) VALUES ( 1, '@gmail.com','$2a$08$fL7u5xcvsZl78su29x1ti.dxI.9rYO8t0q5wk2ROJ.1cdR53bmaVG'); INSERT INTO USERS (ID, USERNAME,PASSWORD) VALUES ( 2, 'myemail@gmail.com','$2a$08$fL7u5xcvsZl78su29x1ti.dxI.9rYO8t0q5wk2ROJ.1cdR53bmaVG');
注意 − 密码应以 Bcrypt Encoder 的格式存储在数据库表中。
编译和执行
您可以使用以下 Maven 或 Gradle 命令创建可执行 JAR 文件,并运行 Spring Boot 应用程序。
对于 Maven,可以使用以下命令 −
mvn clean install
BUILD SUCCESS 后,您可以在 target 目录下找到 JAR 文件。
对于 Gradle,可以使用以下命令 −
gradle clean build
BUILD SUCCESSFUL 后,您可以在 build/libs 目录下找到 JAR 文件。
现在,使用以下命令运行 JAR 文件 −
java jar <JARFILE>
应用程序在 Tomcat 的 8080 端口启动。
现在,通过 POSTMAN 调用 POST 方法 URL 来获取 OAUTH2 token。
http://localhost:8080/oauth/token
现在,添加以下 Request Headers −
Authorization − 使用您的 Client Id 和 Client secret 进行 Basic Auth。
Content Type − application/x-www-form-urlencoded
现在,添加以下 Request Parameters −
- grant_type = password
- username = 您的用户名
- password = 您的密码
现在,调用 API 并获取 access_token,如下所示 −
现在,使用 Request Header 中的 Bearer access token 调用 Resource Server API,如下所示。
然后,您可以看到如下所示的输出 −
