《深入理解 Spring Cloud 与微服务构建》第十七章 使用 Spring Cloud OAuth2 保护微服务系统

文章目录

  • 《深入理解 Spring Cloud 与微服务构建》第十七章 使用 Spring Cloud OAuth2 保护微服务系统
  • 一、什么是 OAuth2
  • 二、如何使用 Spring OAuth2
    • 1.OAuth2 Provider
    • 2.OAuth2 Client
  • 三、案例分析
    • 1.编写 Eureka Server
    • 2.编写 Uaa 授权服务
    • 3.编写 service-hi 资源服务
  • 四、总结

一、什么是 OAuth2

OAuth2 是一个标准的授权协议。OAuth2 取代了在 2006 年创建的 OAuth1 的工作,OAuth2 对 OAuth1 的工作,OAuth2 对 OAuth1 没有做兼容,即完全废弃了 OAuth1。OAuth2 允许不同的客户端通过认证和授权的形式来访问被其保护起来的资源。在认证和授权的过程中,注意包含以下 3 种角色

  • 服务提供方 Authorization Server
  • 资源持有者 Resource Server
  • 客户端 Client

OAuth2 的认证流程:

  1. 用户(资源持有者)打开客户端,客户端询问用户授权
  2. 用户同意授权
  3. 客户端向授权服务器申请授权
  4. 授权服务器对客户端进行认证,也包括用户信息的认证,认证成功后授权给予令牌
  5. 客户端获取令牌后,携带令牌向资源服务器请求资源
  6. 资源服务器确认令牌正确无误,向客户端释放资源

二、如何使用 Spring OAuth2

OAuth2 协议在 Spring Resource 中的实现为 Spring OAuth2。Spring OAuth2 分为两部分,分别是 OAuth2 Provider 和 OAuth2 Client,下面来对二者逐一讲解

1.OAuth2 Provider

OAuth2 Provider 负责公开被 OAuth2 保护起来的资源。OAuth2 Provider 需要配置代表用户的 OAuth2 客户端信息,被用户允许的客户端就可以访问被 OAuth2 保护的资源。OAuth2 Provider 通过管理和验证 OAuth2 令牌来控制客户端是否有权限访问被其保护的资源。另外,OAuth2 Provider 还必须为用户提供认证 API 接口。根据认证 API 接口,用户提供账号和密码等信息,来确认客户端是否可以被 OAuth2 Provider 授权。这样做的好处就是第三方客户端不需要获取用户的账号和密码,通过授权的方式就可以访问被 OAuth2 保护起来的资源

OAuth2 Provider 的角色被分为 Authorization Service(授权服务)和 Resource Service(资源服务),通常它们不在同一个服务中,可能一个 Authorization Service 对应多个 Resource Service。Spring OAuth2 需配合 Spring Security 一起使用,所有的请求由 Spring MVC 控制器处理,并经过一系列的 Spring Security 过滤器

在 Spring Security 过滤器链种有以下两个节点,这两个节点是向 Authorization Service 获取验证和授权的

  • 授权节点:默认为 /oauth/authorize
  • 获取 Token 节点:默认为 /oauth/token

Authorization Server 配置

在配置 Authorization Server 时,需要考虑客户端(Client)从用户获取访问令牌的授权类型(例如授权代码、用户凭据、刷新令牌)。Authorization Server 需要配置客户端的详细信息和令牌服务的实现

在任何实现了 AuthorizationServerConfigurer 接口的类上加 @EnableAuthorizationServer 注解,开启 Authorization Server 的功能,以 Bean 的形式注入 Spring IoC 容器中,并需要实现以下 3 个配置:

  • ClientDetailsServiceConfigurer:配置客户端信息
  • AuthorizationServerEndpointsConfigurer:配置授权 Token 的节点和 Token 服务
  • AuthorizationServerSecurityConfigurer:配置 Token 节点的安全策略

下面具体讲解这 3 个配置

(1)ClientDetailsServiceConfigurer
客户端的配置信息既可以放在内存中,也可以放在数据库中,需要配置以下信息:

  • clientId:客户端 Id,需要在 Authorization Server 种是唯一的
  • secret:客户端的密码
  • scope:客户端的域
  • authorizedGrantTypes:认证类型
  • authorities:权限信息

客户端信息可以存储在数据库中,这样就可以通过更改数据库来实时更新客户端信息的数据。Spring OAuth2 已经设计好了数据库的表,且不可变。创建数据库的脚本如下:

-- ----------------------------
--  Table structure for `clientdetails`
-- ----------------------------
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails` (`appId` varchar(128) NOT NULL,`resourceIds` varchar(256) DEFAULT NULL,`appSecret` varchar(256) DEFAULT NULL,`scope` varchar(256) DEFAULT NULL,`grantTypes` varchar(256) DEFAULT NULL,`redirectUrl` varchar(256) DEFAULT NULL,`authorities` varchar(256) DEFAULT NULL,`access_token_validity` int(11) DEFAULT NULL,`refresh_token_validity` int(11) DEFAULT NULL,`additionalInformation` varchar(4096) DEFAULT NULL,`autoApproveScopes` varchar(256) DEFAULT NULL,PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
--  Table structure for `oauth_access_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (`token_id` varchar(256) DEFAULT NULL,`token` blob,`authentication_id` varchar(128) NOT NULL,`user_name` varchar(256) DEFAULT NULL,`client_id` varchar(256) DEFAULT NULL,`authentication` blob,`refresh_token` varchar(256) DEFAULT NULL,PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
--  Table structure for `oauth_approvals`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (`userId` varchar(256) DEFAULT NULL,`clientId` varchar(256) DEFAULT NULL,`scope` varchar(256) DEFAULT NULL,`status` varchar(10) DEFAULT NULL,`expiresAt` datetime DEFAULT NULL,`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
--  Table structure for `oauth_client_details`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (`client_id` varchar(256) NOT NULL,`resource_ids` varchar(256) DEFAULT NULL,`client_secret` varchar(256) DEFAULT NULL,`scope` varchar(256) DEFAULT NULL,`authorized_grant_types` varchar(256) DEFAULT NULL,`web_server_redirect_uri` varchar(256) DEFAULT NULL,`authorities` varchar(256) DEFAULT NULL,`access_token_validity` int(11) DEFAULT NULL,`refresh_token_validity` int(11) DEFAULT NULL,`additional_information` varchar(4096) DEFAULT NULL,`autoapprove` varchar(256) DEFAULT NULL,PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
--  Table structure for `oauth_client_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (`token_id` varchar(256) DEFAULT NULL,`token` blob,`authentication_id` varchar(128) NOT NULL,`user_name` varchar(256) DEFAULT NULL,`client_id` varchar(256) DEFAULT NULL,PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
--  Table structure for `oauth_code`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (`code` varchar(256) DEFAULT NULL,`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ----------------------------
--  Table structure for `oauth_refresh_token`
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (`token_id` varchar(256) DEFAULT NULL,`token` blob,`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(2)AuthorizationServerEndpointsConfigurer
在默认情况下,AuthorizationServerEndpointsConfigurer 配置开启了所有的验证类型,除了密码类型的验证,密码验证只有配置了 authenticationManager 的配置才会开启。AuthorizationServerEndpointsConfigurer 配置由以下 5 项组成:

  • authenticationManager:只有配置了该选项,密码认证才会开启。在大多数情况下都是密码验证,所以一般都会配置这个选项
  • userDetailsService:配置获取用户认证信息的接口,和上一章实现的 userDetailsService 类似
  • authorizationCodeServices:配置验证码服务
  • implicitGrantService:配置管理 implict 验证的状态
  • tokenGranter:配置 Token Granter

另外,需要设置 Token 的管理策略,目前支持以下 3 种:

  • InMemoryTokenStore:Token 存储在内存中
  • JdbcTokenStore:Token 存储在数据库中。需要引入 spring-jdbc 的依赖包,并配置数据源,以及初始化 Spring OAuth2 的数据库脚本,即上一节的数据库脚本
  • JwtTokenStore:采用 JWT 形式,这种形式没有做任何的存储,因为 JWT 本身包含了用户验证的所有信息,不需要存储。采用这种形式,需要引入 spring-jwt 的依赖

(3)AuthorizationServerSecurityConfigurer
如果资源服务和授权服务是在同一个服务中,用默认的配置即可,不需要做其他任何的配置。但是如果资源服务和授权服务不在同一个服务中,则需要做一些额外配置。如果采用 RemoteTokenServices(远程 Token 校验),资源服务器的每次请求所携带的 Token 都需要从授权服务做校验。这时需要配置 “/oauth/check_token” 校验节点的校验策略

Authorization Server 的配置比较复杂,细节较多。通过在实现了 AuthorizationServerConfigurer 接口的类上加 @EnableAuthorizationServer 注解,开启 Authorization Server 的功能,并注入 IoC 容器中。然后需要配置 ClinetDetailsServiceConfigurer、AuthorizationServerSecurityConfigurer 和 AuthorizationServerEndpointsConfigurer,它们有很多可选的配置,需要读者慢慢理解

Resource Server 的配置

Resource Server(可以是授权服务器,也可以是其他的资源服务)提供了受 OAuth2 保护的资源,这些资源为 API 接口、Html 页面、Js 文件等。Spring OAuth2 提供了实现此保护功能的 Spring Security 认证过滤器。在加了 @Configuration 注解的配置类上加 @EnableResourceServer 注解,开启 Resource Server 的功能,并使用 ResouceServerConfigurer 进行配置(如有必要),需要配置以下的内容:

  • tokenServices:定义 Token Service。例如用 ResourceServerTOkenservices 类,配置 Token 是如何编码和解码的。如果 Resource Server 和 Authorization Server 在同一个工程上,则不需要配置 tokenServices,如果不在同一个程序就需要配置。也可以用 RemoteTokenServices 类,即 Resource Server 采用远程授权服务器进行 Token 解码,这时也不需要配置此选项,本章案例采用此方式
  • resourceId:配置资源 Id

2.OAuth2 Client

OAuth2 Client(客户端)用于访问被 OAuth2 保护起来的资源。客户端需要提供用于存储用户的授权码和访问令牌的机制,需要配置如下两个选项:

  • Protected Resource Configuration(受保护的资源配置)
  • Client Conffiguration(客户端配置)

Protected Resource Configuration

使用 OAuth2ProtectedResourceDetails 类型的 Bean 来定义受保护的资源,受保护的资源具有以下属性:

  • Id:资源的 Id,它在 Spring OAuth2 协议中没有用到,用于客户端寻找资源,不需要做配置,默认即可
  • clientId:OAuth2 Client 的 Id,和之前 OAuth2 Provider 种配置的一一对应
  • clientSecret:客户端密码,和之前 OAuth2 Provider 种配置的一一对应
  • accessTokenUri:获取 Token 的 API 节点
  • scope:客户端的域
  • clientAuthenticationScheme:有两种客户端验证类型,分别为 Http Basic 和 Form,默认为 Http Basic
  • userAuthorizationUri:如果用户需要授权访问资源,则用户将被重定向到认证 Url

Client Configuration

对于 OAuth2 Client 的配置,可以使用 @EnableOAuth2Client 注解来简化配置,另外还需要配置以下两项:

  • 创建一个过滤器 Bean(Bean 的 Id 为 oauth2ClientContextFilter),用来存储当前请求和上下文的请求。在请求期间,如果用户需要进行身份验证,则用户会被重定向到 OAuth2 的认证 Url
  • 在 Request 域内创建 AccessTokenRequest 类型的 Bean

三、案例分析

在这个案例种有 3 个工程,分别是服务注册中心工程 eureka-server、授权中心 Uaa(User Accounts and Authentication)工程 auth-service 和资源工程 service-hi

首先,浏览器向 auth-service服务器提供客户端信息、用户名和密码,请求获取 Token。auth-service 确认这些信息无误后,根据该用户的信息生成 Token 并返回给浏览器。浏览器在以后每次请求都需要携带 Token 给资源服务 service-hi,资源服务器获取到请求携带的 Token 后,通过远程调度将 Token 给授权服务 auth-service 确认。auth-service 确认 Token 正确无误后,将该 Token 对应的用户的权限信息返回给资源服务 service-hi。如果该 Token 对应的用户具有访问该 API 接口的权限,就正常返回请求结果,否则返回权限不足的错误提示

1.编写 Eureka Server

整个项目采用的是 Maven 多 Module 的形式,并制定了项目的 Spring Boot 版本为 2.1.0,Spring Cloud 版本为 Greenwich.RELEASE

在主 Maven 工程下,创建一个 eureka-server 的 moudle 工程,创建完成后,在 eureka-server 工程的 pom 文件中引入 eureka server 的起步以来,代码如下:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

在 eureka-server 工程的配置文件 application.yml 中配置 Eureka Server,包括配置了程序的端口号为 8761,host 为 localhost,并配置了不自注册,具体的配置代码如下:

server:port: 8761eureka:instance:#提交 IP 信息prefer-ip-address: truehostname: localhostclient:#默认情况下,Eureka Server 会向自己注册,这时需要配置 register-with-eureka 和 fetch-registry 为 false,防止自己注册自己register-with-eureka: falsefetch-registry: falseservice-url:defaultZone:http://${eureka.instance.hostname}:${server.port}/eureka/

在程序的启动类 EurekaServerApplication 上加 @EnableEurekaServer 注解,开启 EurekaServer 的功能,代码如下:

package com.sisyphus;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {public static void main(String[] args){SpringApplication.run(EurekaServerApplication.class, args);}
}

2.编写 Uaa 授权服务

依赖管理 pom 文件

在主 Maven 工程下创建一个 Moudle 工程,取名为 auth-service,作为 Uaa 服务(授权服务),在 auth-service 工程的 pom 文件里引入工程所需的依赖,代码如下:

<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId><version>2.0.0.RELEASE</version></dependency><dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId><version>2.0.0.RELEASE</version></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.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency>
</dependencies>

其中需要引入起步以来 spring-cloud-starter-security,以及 spring-security-oauth2-autoconfigure 依赖。在工程中使用了 MySQL 数据库,引入 MySQL 的连接驱动依赖 mysql-connector-java 和 JPA 的起步依赖 spring-boot-starter-data-jpa。在工程中使用了 WEB 功能,引入 WEB 的起步依赖 spring-boot-starter-web。这个工程作为 Eureka Client,引入了 Eureka 的起步依赖 spring-cloud-starter-eureka

配置文件 application.yml

在工程的配置文件 application.yml 做如下配置:

spring:application:name: service-authdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8username: rootpassword: 123456jpa:hibernate:ddl-auto: updateshow-sql: trueserver:servlet:context-path: /uaaport: 5000eureka:client:service-url:defaultZone: http://localhost:8761/eureka/

配置 Spring Security

由于 auth-service 需要对外暴露检查 Token 的 API 接口,所以 auth-service 也是一个资源服务,需要在工程中引入 Spring Security,并做相关的配置,对 auth-service 资源进行保护。配置代码如下:

package com.sisyphus.config;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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserServiceDetail userServiceDetail;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userServiceDetail).passwordEncoder(new BCryptPasswordEncoder());}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}

WebSecurityConfig 类通过 @EnableWebSecurity 注解开启 WEB 保护功能,通过 @EnableGloabalMethodSecurity 注解开启在方法上的保护功能。WebSecurityConfig 类继承了 WebSecurityConfigurerAdapter 类,并复写了以下 3 个方法来做相关的配置:

  • configure(HttpSecurity http):HttpSecurity 中配置了所有的请求都需要安全验证
  • configure(AuthenticationManagerBuilder auth):AuthenticationManagerBuilder 中配置了验证的用户信息源和密码加密的策略,并且向 IoC 容器注入了 AuthenticationManager 对象。这需要在 OAuth2 中配置,因为在 OAuth2 中配置了 AuthenticationManager,密码验证才会开启。在本例中,采用的就是密码验证
  • authenticationManagerBean():配置了验证管理的 Bean

UserServiceDetail 实现了 UserDetailsService 接口,并使用了 BCryPasswordEncoder 对密码进行加密,代码如下:

package com.sisyphus.service;import com.sisyphus.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;@Service
public class UserServiceDetail implements UserDetailsService {@Autowiredprivate UserDao userRepository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return userRepository.findByUsername(username);}
}

UserDao 类继承了 JpaRepository,在 UserDao 类中写一个根据用户名获取用户的方法,代码如下:

package com.sisyphus.dao;import com.sisyphus.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;public interface UserDao extends JpaRepository<User, Long> {User findByUsername(String username);
}

User 类需要实现 UserDetails 接口,Role 类需要实现 GrantedAuthority 接口,代码如下:

package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;@Entity
public class User implements UserDetails, Serializable {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false, unique = true)private String username;@Columnprivate String password;@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)@JoinTable(name = "user_role",joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))private List<Role> authorities;public User() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}public void setAuthorities(List<Role> authorities) {this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;@Entity
public class Role implements GrantedAuthority {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false)private String name;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public void setName(String name) {this.name = name;}@Overridepublic String getAuthority() {return name;}@Overridepublic String toString() {return name;}
}

配置 Authorization Server

首先列出配置代码,然后根据代码对每一个配置做详细说明,代码如下:

package com.sisyphus;import com.sisyphus.service.UserServiceDetail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
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.configuration.EnableResourceServer;
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.JdbcTokenStore;import javax.sql.DataSource;@SpringBootApplication
@EnableResourceServer
@EnableEurekaClient
public class AuthServiceApplication {public static void main(String[] args) {SpringApplication.run(AuthServiceApplication.class, args);}@Autowired@Qualifier("dataSource")private DataSource dataSource;@Configuration@EnableAuthorizationServerprotected class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {JdbcTokenStore tokenStore = new JdbcTokenStore(dataSource);@Autowired@Qualifier("authenticationManagerBean")private AuthenticationManager authenticationManager;@Autowiredprivate UserServiceDetail userServiceDetail;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("browser").authorizedGrantTypes("refresh_token", "password").scopes("ui").and().withClient("service-hi").secret("123456").authorizedGrantTypes("client_credentials", "refresh_token", "password").scopes("server");}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager).userDetailsService(userServiceDetail);}@Overridepublic void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()").allowFormAuthenticationForClients().passwordEncoder(NoOpPasswordEncoder.getInstance());}}
}

在程序启动类 AuthServiceApplication 加上 @EnableEurekaClient 注解,开启 Eureka Client 的功能,加上 @EnableResourceServer 注解,开启 Resource Server。程序需要对外暴露获取 Token 的 API 接口和验证 Token 的API 接口,所以该程序也是一个资源服务

OAuth2AuthorizationConfig 类继承 AuthorizationServerConfigurerAdapter,并在这个类上加上注解 @EnableAuthorizationServer、开启授权服务的功能。作为授权服务需要配置 3 个选项,分别为 ClientDetailsServiceConfigurer、AuthorizationServerEndpointsConfigurer 和 AuthorizationServerSecurityConfigurer

其中,ClientDetailsServiceConfigurer 配置了客户端的一些基本信息,clients.inMemory() 方法配置了将客户端的信息存储在内存中,.withClient(“browser”) 方法创建了一个 clientId 为 browser 的客户端,authorizedGrantTypes(“refresh_token”, “password”) 方法配置了验证类型为 refresh_token 和 password,.scope(“ui”) 方法配置了客户端域为 “ui”。接着创建了另一个 client,它的 Id 为 “service-hi”

AuthorizationServerEndpointsConfigurer 需要配置 tokenStore、authenticationManager 和 userServiceDetail。其中,tokenStore(Token 的存储方式)采用的方式是将 Token 存储在内存中,即使用 InMEmoryTokenStore。如果资源服务和授权服务是同一个服务,用 InMemoryTokenStore 是最好的选择。如果资源服务和授权服务不是同一个服务,则不用 InMemoryTokenStore 进行存储 Token。因为当授权服务出现故障,需要重启服务,之前存在内存中的 Token 全部丢失,导致资源服务的 Token 全部失效。另一种方式是用 JdbcTokenStore,即使用数据库去存储,使用 JdbcTokenStore 存储需要引入连接数据库依赖,如本例中的 MySQL 连接器、JPA,并且需要初始化数据库。authenticationManager 需要配置 AuthenticationManager 这个 Bean,这个 Bean 来源于 WebSecurityConfigurerAdater 中的配置,只有配置了这个 Bean 才会开启密码类型的验证。最后配置了 userDetailsService,用来读取验证用户的信息

AuthorizationServerSecurityConfigurer 配置了获取 Token 的策略,在本案例中对获取 Token 请求不进行拦截,只需要验证获取 Token 的验证信息,这些信息准确无误,就返回 Token。另外配置了检查 Token 的策略

暴露 Remote Token Services 接口

本案例采用 RemoteTokenServices 这种方式对 Token 进行验证。如果其他资源服务需要验证 Token,则需要远程调用授权服务暴露的验证 Token 的 API 接口。本案例中验证 Token 的 API 接口的代码如下:

package com.sisyphus.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import java.security.Principal;@RestController
@RequestMapping("/users")
public class UserController {@RequestMapping(value = "/current", method = RequestMethod.GET)public Principal getUser(Principal principal){return principal;}
}

3.编写 service-hi 资源服务

项目依赖

在主 Maven 工程下,创建一个 Moudle 工程,取名为 service-hi,这个工程作为资源服务。在 service-hi 工程的 pom 文件引入项目所需的依赖,代码如下:

<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId><version>2.0.0.RELEASE</version></dependency><dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId><version>2.0.0.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>
</dependencies>

在工程中用到了 MySQL 数据库,采用了 JPA 的 ORM 框架来操作数据库,所以需要在工程的 pom 文件引入 JPA 的起步依赖 spring-boot-starter-data-jpa 和 MySQL 数据库连接器依赖 mysql-connector-java。作为 Eureka Client,需要在工程的 pom 文件引入 Eureka 的起步依赖 spring-cloud-starter-netflix-eureka-client。作为 WEB 服务器,需要在工程的 pom 文件引入 WEB 的起步依赖 spring-boot-starter-web。另外使用 Feign 作为远程调度框架,需要在工程的 pom 文件引入 Feign 的起步依赖 spring-cloud-starter-security 和依赖 spring-security-oauth2-autoconfigure

配置文件 application.yml

在工程的配置文件 application.yml 做程序的相关配置,具体配置如下:

eureka:client:service-url:defaultZone: http://localhost:8761/eureka/server:port: 8762spring:application:name: service-hidatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8username: rootpassword: 123456jpa:hibernate:ddl-auto: updateshow-sql: truemain:allow-bean-definition-overriding: truesecurity:oauth2:resource:user-info-uri: http://localhost:5000/uaa/users/currentclient:clientId: service-hiclientSecret: 123456accessTokenUri: http://localhost:5000/uaa/oauth/tokengrant-type: client_credentials,passwordscope: server

在上面的配置文件中,配置了程序名为 service-hi,端口为 8762,服务注册中心的地址为 http://localhost:8761/eureka/,配置了数据库的连接驱动、数据库连接地址、数据库用户名和密码,以及 JPA 的相关配置。然后配置了 security.oauth2.resource,指定了 user-info-uri 的地址,用于获取当前 Token 的用户信息,配置了 security.oauth2.client 的相关信息,以及 clientId、clientSecret 等信息,这些配置需要和在 Uaa 服务中配置的一一对应

配置 Resource Server

service-hi 工程作为 Resource Server(资源服务),需要配置 Resource Server 的相关配置,配置代码如下:

package com.sisyphus.config;import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/user/registry").permitAll().anyRequest().authenticated();}
}

在 ResourceServerConfigurer 类上加 @EnableResourceServer 注解,开启 Resource Server 的功能,加 @EnableGlobalMethodSecurity 注解,开启方法级别的保护。ResourceServerConfigurer 类继承 ResourceServerConfigurerAdapter 类,并重写 configure(HttpSecurity http) 方法,通过 ant 表达式,配置哪些请求需要验证,哪些请求不需要验证。如本案例中 “/user/register” 的接口不需要验证,其他所有的请求都需要验证

配置 OAuth2 Client

OAuth2 Client 用来访问被 OAuth2 保护的资源。service-hi 作为 OAuth2 Client,它的配置代码如下:

package com.sisyphus.config;import feign.RequestInterceptor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;@Configuration
@EnableConfigurationProperties
@EnableOAuth2Client
public class OAuth2ClientConfig {@Bean@ConfigurationProperties(prefix = "security.oauth2.client")public ClientCredentialsResourceDetails clientCredentialsResourceDetails(){return new ClientCredentialsResourceDetails();}@Beanpublic RequestInterceptor oauth2FeignRequestInterceptor(){return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());}@Beanpublic OAuth2RestTemplate clientCredentialsRestTemplate(){return new OAuth2RestTemplate(clientCredentialsResourceDetails());}
}

简单来说,需要配置 3 个选项:一是配置受保护的资源的信息,即 ClientCredentialsResourceDetails;二是配置一个过滤器,存储当前请求和上下文;三是在 Request 域内创建 AccessTokenRequest 类型的 Bean

现在通过上述代码来具体说明,在 OAuth2ClientConfig 类上加 @EnableOAuth2Client 注解,开启 OAuth2 Client 的功能;并配置了一个 ClientCredentialsResourceDetails 类型的 Bean,该 Bean 是通过读取配置文件中前缀为 security.oauth2.client 的配置来获取 Bean 的配置属性的;注入一个 OAuth2FeignRequestInterceptor 类型过滤器的 Bean;最后注入了一个用于向 Uaa 服务请求的 OAuth2RestTemplate 类型的 Bean

到目前为止,授权服务、资源服务和 OAuth2 客户端都已经搭建完毕,现在写一个注册 API 接口来做测试

编写用户注册接口

首先编写一个 User 类,在本案例中采用了 JPA 作为 ORM 框架,需要在 User 类加上 JPA 的注解,同 Uaa 服务的 User 类一样,代码如下:

package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;@Entity
public class User implements UserDetails, Serializable {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false, unique = true)private String username;@Columnprivate String password;@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)@JoinTable(name = "user_role",joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))private List<Role> authorities;public User() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}public void setAuthorities(List<Role> authorities) {this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

数据操作类 UserDao 继承了 JpaRepository,UserDao 具备了基本的操作数据库单表的基本方法,代码如下:

package com.sisyphus.dao;import com.sisyphus.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;public interface UserDao extends JpaRepository<User, Long> {User findByUsername(String username);
}

Service 层的 UserServiceImpl 类包含一个创建用户逻辑的方法,其中用到 BCryptPasswordEncoder 类来加密密码,代码如下:

package com.sisyphus.service;import com.sisyphus.dao.UserDao;
import com.sisyphus.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Service
public class UserServiceImpl{private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();@Autowiredprivate UserDao userDao;public User create(String username, String password){User user = new User();user.setUsername(username);String hash = encoder.encode(password);user.setPassword(hash);User u = userDao.save(user);return u;}
}

编写 UserController 类,在类中有一个注册的 API 接口,代码如下:

package com.sisyphus.controller;import com.sisyphus.entity.User;
import com.sisyphus.service.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserServiceImpl userService;@RequestMapping(value = "/registry", method = RequestMethod.POST)public User createUser(@RequestParam("username") String username, @RequestParam("password") String password){return userService.create(username, password);}
}

编写一个测试类 HiController,其中有 3 个接口:第一个 API 接口 “/hi”,不需要任何权限,只需要验证 Header 中的 Token 正确与否,Token 正确即可访问;第二个 API 接口 “/hello”,需要 “ROLE_ADMIN” 权限;第三个接口 “/getPrinciple”,用户获取当前 Token 用户信息。代码如下:

package com.sisyphus.controller;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
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
public class HiController {Logger logger = LoggerFactory.getLogger(HiController.class);@Value("${server.port}")String port;@RequestMapping("/hi")public String home(){return "hi" + ",i am from port:" + port;}@PreAuthorize("hasAuthority('ROLE_ADMIN')")@RequestMapping("/hello")public String hello(){return "hello!";}@GetMapping("/getPrinciple")public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, Principal principal, Authentication authentication){logger.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());logger.info("principal.toString()" + principal.toString());logger.info("principal.getName" + principal.getName());logger.info("authentication:" + authentication.getAuthorities().toString());return oAuth2Authentication;}
}

下面来演示整个流程
(1)通过 Curl 命令模拟请求,调用注册的 API 接口,注册一个用户,Curl 命令如下:

curl -d "username=sisyphus&password=123456" "localhost:8762/user/registry"

注册成功,返回结果如下:

(2)通过 Curl 命令模拟请求,调用获取 Token 的 API 接口,Curl 命令如下:

curl -i -X POST -d "username=sisyphus&password=123456&grant_type=password&client_id=service-hi&client_secret=123456" http://localhost:5000/uaa/oauth/token

获取 Token 成功,返回结果如下:

(3)通过 Curl 命令模拟请求,访问不需要权限点的接口 “/hi”,Curl 命令如下:

curl -l -H "Authorization:Bearer 66e27729-3a68-4d41-98f0-9247a9e90515" -X GET "localhost:8762/hi"

返回结果如下:

(4)通过 Curl 命令模拟请求,访问需要有 “ROLE_ADMIN” 权限点的 API 接口 “/hello”,Curl 命令如下:

curl -l -H "Authorization:Bearer 66e27729-3a68-4d41-98f0-9247a9e90515" -X GET "localhost:8762/hello"

由于该用户没有 “ROLE_ADMIN” 权限点,所以没有权限访问该 API 接口,返回结果如下:

(5)在数据库中给予该用户 “ROLE_ADMIN” 权限,在数据库中执行以下脚本:

INSERT INTO role VALUES ('1','ROLE_USER'),('2','ROLE_ADMIN');
INSERT INTO user_role VALUES ('1', '2');

(6)给予该用户 “ROLE_ADMIN” 权限后,重新访问 API 接口 “/hello”,Curl 命令如下:

curl -l -H "Authorization:Bearer 66e27729-3a68-4d41-98f0-9247a9e90515" -X GET "localhost:8762/hello"

获取的返回结果如下:

从上面的请求返回的结果可知,给该用户加上 “ROLE_ADMIN” 权限后,该请求能够获取正常的返回结果。由此可见,被 Spring Cloud OAuth2 保护的资源服务,是需要验证请求的用户信息和该用户所具有的权限的,验证通过,则返回正确结果,否则返回 “不允许访问” 的结果

四、总结

这个架构存在的缺陷就是每次请求都需要资源服务内部远程调度 auth-service 服务来验证 Token 的正确性,以及该 Token 对应的用户所具有的权限,额外多了一次内部请求。如果在高并发的情况下,auth-service 需要集群部署,并且需要做缓存处理

《深入理解 Spring Cloud 与微服务构建》第十七章 使用 Spring Cloud OAuth2 保护微服务系统相关推荐

  1. 《深入理解 Spring Cloud 与微服务构建》第九章 熔断器 Hystrix

    <深入理解 Spring Cloud 与微服务构建>第九章 熔断器 Hystrix 文章目录 <深入理解 Spring Cloud 与微服务构建>第九章 熔断器 Hystrix ...

  2. 《深入理解 Spring Cloud 与微服务构建》第二章 微服务应该具备的功能

    <深入理解 Spring Cloud 与微服务构建>第二章 微服务应该具备的功能 文章目录 <深入理解 Spring Cloud 与微服务构建>第二章 微服务应该具备的功能 一 ...

  3. 《深入理解 Spring Cloud 与微服务构建》第一章 微服务简介

    <深入理解 Spring Cloud 与微服务构建>第一章 微服务简介 文章目录 <深入理解 Spring Cloud 与微服务构建>第一章 微服务简介 一.单体架构及其存在的 ...

  4. 《深入理解Spring Cloud与微服务构建》出版啦!

    作者简介 方志朋,毕业于武汉理工大学,CSDN博客专家,专注于微服务.大数据等领域,乐于分享,爱好开源,活跃于各大开源社区.著有<史上最简单的Spring Cloud教程>,累计访问量超过 ...

  5. 《深入理解Spring Cloud与微服务构建》书籍目录

    转载请标明出处: https://blog.csdn.net/forezp/article/details/79735542 本文出自方志朋的博客 作者简介 方志朋,毕业于武汉理工大学,CSDN博客专 ...

  6. Spring Boot 中使用 Swagger2 构建强大的 RESTful API 文档

    项目现状:由于前后端分离,没有很好的前后端合作工具. 由于接口众多,并且细节复杂(需要考虑不同的HTTP请求类型.HTTP头部信息.HTTP请求内容等),高质量地创建这份文档本身就是件非常吃力的事,下 ...

  7. java Spring Boot中使用Swagger2构建API文档

    1.添加Swagger2的依赖 在pom.xml中加入Swagger2的依赖 <dependency><groupId>io.springfox</groupId> ...

  8. 《Spring Boot极简教程》第8章 Spring Boot集成Groovy,Grails开发

    第8章 Spring Boot集成Groovy,Grails开发 本章介绍Spring Boot集成Groovy,Grails开发.我们将开发一个极简版的pms(项目管理系统). Groovy和Gra ...

  9. 【JAVA SE】第十七章 反射、注解与Spring事务底层原理

    第十七章 反射.注解与Spring事务底层原理 文章目录 第十七章 反射.注解与Spring事务底层原理 一.反射 1.简介 2.意义 3.缺点 4.应用场景 5.反射技术的使用 二.注解 1.概念 ...

最新文章

  1. Redis 笔记(09)— 过期时间 expire(设置、查询、取消过期时间)
  2. 哈尔滨工业大学计算机改专业课,哈尔滨工业大学计算机专业课 复试 2013HITCS
  3. 河南上oracle客户,解决Oracle监听服务报错
  4. python网站框架下载_最受欢迎的7款Python开源框架总结,忍不住收藏了~
  5. python列表推导式生成随机数_python 【迭代器 生成器 列表推导式】
  6. 【Elasticsearch】关于 Analyzers 的一切,第一部分
  7. lisp 河道水面线计算_鹤岗河道用铸铁轻型闸门厂家
  8. Angular进阶教程一
  9. bash:pip:找不到命令
  10. android nexus 刷机工具包,Android Nexus 6p刷机及root
  11. HC32F460 SPI DMA 驱动 TFT显示屏
  12. 计算机内存分配、管理
  13. c++串口配置及DCB结构体
  14. 信息安全软考——关于DES初始置换表解答题
  15. 安化云台山第二届星空帐篷音乐节盛大启动
  16. 吉林大学珠海学院论坛 http://j.bnubbs.cn
  17. 大小非解禁(大小非解禁对股票的影响)
  18. Guns V3.0简介
  19. 2021杭州恒生电子实习一面面经
  20. 解决Mac能上微信聊QQ但是打不开网页的问题

热门文章

  1. java工商银行项目_ChaosBlade 在工商银行混沌工程体系中的应用实践
  2. linux getline函数用法,get()与getline()
  3. idea创建管理项目
  4. 【Teradata SQL】禁用和启用数据库用户登录
  5. Crazypony四轴飞行器代码框架
  6. hdu 2873 Bomb Game 博弈论
  7. 成功演示的关键步骤(三)
  8. linux上ftp和lftp冲突,Linux FTP客户端 Lftp 使用方法,该如何解决
  9. stm32经典笔试题_嵌入式面试经典30问
  10. 转换文档参数_1分钟教会你将Excel转换成Word,简单高效,办公人士必备神技