前言
OAuth 2.0
是介于 用户资源 和 第三方应用 之间的一个 中间层,它把 资源 和 第三方应用 隔开,使得 第三方应用 无法直接访问 资源,从而起到 保护资源 的作用。为了访问这种 受限资源,第三方应用(客户端)在访问的时候需要 提供凭证。
正文
1. OAuth 2.0简介
在 认证 与 授权 的过程中,主要包含以下 3
种角色:
服务提供方: Authorization Server
资源持有者: Resource Server
客户端: Client
OAuth 2.0
的 认证流程 如图所示,具体如下:
用户(资源持有者)打开 客户端,客户端 询问 用户授权。
用户 同意授权。
客户端 向 授权服务器 申请授权。
授权服务器 对 客户端 进行认证,也包括 用户信息 的认证,认证成功后授权给予 令牌。
客户端 获取令牌后,携带令牌 向 资源服务器 请求资源。
资源服务器 确认令牌正确无误,向 客户端 发放资源。
OAuth2 Provider
的角色被分为 Authorization Server
(授权服务)和 Resource Service
(资源服务),通常它们不在同一个服务中,可能一个 Authorization Service
对应 多个 Resource Service
。Spring OAuth2.0
需配合 Spring Security
一起使用,所有的请求由 Spring MVC
控制器处理,并经过一系列的 Spring Security
过滤器拦截。
在 Spring Security
过滤器链 中有以下两个 端点,这两个节点用于从 Authorization Service
获取验证 和 授权。
用于 授权 的端点:默认为
/oauth/authorize
。用于获取 令牌 的端点:默认为
/oauth/token
。
2. 新建本地数据库
客户端信息 可以存储在 数据库 中,这样就可以通过更改 数据库 来实时 更新客户端信息 的数据。Spring OAuth2
已经设计好了数据库的表,且不可变。首先将以下 DDL
导入数据库中。
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 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;
-- ----------------------------
-- Table structure for `role`
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`password` varchar(255) DEFAULT NULL,
`username` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_sb8bbouer5wak8vyiiy4pf2bx` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `user_role`
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
KEY `FKa68196081fvovjhkek5m97n3y` (`role_id`),
KEY `FK859n2jvi8ivhui0rl0esws6o` (`user_id`),
CONSTRAINT `FK859n2jvi8ivhui0rl0esws6o` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
CONSTRAINT `FKa68196081fvovjhkek5m97n3y` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
3. 新建Maven项目
采用 Maven
的多 Module
的项目结构,新建一个 空白的 Maven
工程,并在 根目录 的 pom.xml
文件中配置 Spring Boot
的版本 1.5.3.RELEASE
,Spring Cloud
的版本为 Dalston.RELEASE
,完整的代码如下:
4.0.0
org.springframework.boot
spring-boot-starter-parent
1.5.3.RELEASE
eureka-server
service-auth
service-hi
io.github.ostenant.springcloud
spring-cloud-oauth2-example
0.0.1-SNAPSHOT
spring-cloud-oauth2-example
Demo project for Spring Boot
1.8
Dalston.RELEASE
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
4. 创建Eureka Server
4.1. 创建应用模块
新建一个 eureka-server
模块,并添加 Eureka
的相关依赖,并指定 pom.xml
的父节点如下:
4.0.0
io.github.ostenant.springcloud
eureka-server
0.0.1-SNAPSHOT
jar
eureka-server
Demo project for Spring Boot
io.github.ostenant.springcloud
spring-cloud-oauth2-example
0.0.1-SNAPSHOT
1.8
org.springframework.cloud
spring-cloud-starter-eureka-server
org.springframework.boot
spring-boot-maven-plugin
4.2. 配置application.yml
在 eureka-server
模块的配置文件 application.yml
中配置 Eureka Server
的信息:
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
4.3. 配置应用启动类
最后在应用的 启动类 上添加 @EnableEurekaServer
注解开启 Eureka Server
的功能。
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
5. 创建Uaa授权服务
5.1. 创建应用模块
新建一个 service-auth
模块,并添加以下依赖,作为 Uaa
(授权服务),完整的代码如下:
4.0.0
io.github.ostenant.springcloud
service-auth
0.0.1-SNAPSHOT
jar
service-auth
Demo project for Spring Boot
io.github.ostenant.springcloud
spring-cloud-oauth2-example
0.0.1-SNAPSHOT
1.8
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.boot
spring-boot-maven-plugin
打开 spring-cloud-starter-oauth2
依赖包可以看到,它已经整合了以下 3
个 起步依赖:
spring-cloud-starter-security
spring-security-oauth2
spring-security-jwt
5.2. 配置application.yml
在 service-oauth
模块中的 application.yml
完成如下配置:
spring:
application:
name: service-auth
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
server:
context-path: /uaa
port: 5000
security:
oauth2:
resource:
filter-order: 3
# basic:
# enabled: false
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
配置 security.oauth2.resource.filter-order
为 3
,在 Spring Boot 1.5.x
版本之前,可以省略此配置。
5.3. 配置安全认证
由于 auth-service
需要对外暴露检查 Token
的 API
接口,所以 auth-service
其实也是一个 资源服务,需要在 auth-service
中引入 Spring Security
,并完成相关配置,从而对 auth-service
的 资源 进行保护。
WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail userServiceDetail;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
// @formatter:on
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
public @Bean AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
UserServiceDetail.java
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
配置表的关系映射类 User
,需要实现 UserDetails
接口:
@Entity
public class User implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column
private 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 authorities;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
// setter getter
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
配置表的关系映射类 Role
,需要实现 GrantedAuthority
接口:
@Entity
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
// setter getter
@Override
public String getAuthority() {
return name;
}
@Override
public String toString() {
return name;
}
}
UserRepository.java
public interface UserRepository extends JpaRepository {
User findByUsername(String username);
}
5.4. 配置Authentication Server
配置 认证服务器,使用 @EnableAuthorizationServer
注解开启 Authorization Server
,对外提供 认证 和 授权 的功能。
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
// 将Token存储在内存中
// private TokenStore tokenStore = new InMemoryTokenStore();
private TokenStore tokenStore = new JdbcTokenStore(dataSource);
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private UserServiceDetail userServiceDetail;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 将客户端的信息存储在内存中
clients.inMemory()
// 创建了一个client名为browser的客户端
.withClient("browser")
// 配置验证类型
.authorizedGrantTypes("refresh_token", "password")
// 配置客户端域为“ui”
.scopes("ui")
.and()
.withClient("service-hi")
.secret("123456")
.authorizedGrantTypes("client_credentials", "refresh_token","password")
.scopes("server");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置Token的存储方式
endpoints.tokenStore(tokenStore)
// 注入WebSecurityConfig配置的bean
.authenticationManager(authenticationManager)
// 读取用户的验证信息
.userDetailsService(userServiceDetail);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// 对获取Token的请求不再拦截
oauthServer.tokenKeyAccess("permitAll()")
// 验证获取Token的验证信息
.checkTokenAccess("isAuthenticated()");
}
}
5.5. 开启Resource Server
在应用的启动类上,使用 @EnableResourceServer
注解 开启资源服务,应用需要对外暴露获取 token
的 API
接口。
@EnableEurekaClient
@EnableResourceServer
@SpringBootApplication
public class ServiceAuthApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceAuthApplication.class, args);
}
}
本例采用 RemoteTokenService
这种方式对 token
进行 验证。如果 其他资源服务 需要验证 token
,则需要远程调用 授权服务 暴露的 验证 token
的 API
接口。
@RestController
@RequestMapping("/users")
public class UserController {
@RequestMapping(value = "/current", method = RequestMethod.GET)
public Principal getUser(Principal principal) {
return principal;
}
}
6. 编写service-hi资源服务
6.1. 创建应用模块
新建一个 service-hi
模块,这个服务作为 资源服务。在 pom.xml
文件引入如下依赖:
4.0.0
io.github.ostenant.springcloud
service-hi
0.0.1-SNAPSHOT
jar
service-hi
Demo project for Spring Boot
io.github.ostenant.springcloud
spring-cloud-oauth2-example
0.0.1-SNAPSHOT
1.8
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-feign
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
org.springframework.boot
spring-boot-maven-plugin
6.2. 配置application.yml
在 application.yml
中配置 service-hi
在 service-auth
中配置的 OAuth Client
信息:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8762
spring:
application:
name: service-hi
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
security:
oauth2:
resource:
# 获取当前Token的用户信息
user-info-uri: http://localhost:5000/uaa/users/current
client:
clientId: service-hi
clientSecret: 123456
# 获取Token
accessTokenUri: http://localhost:5000/uaa/oauth/token
grant-type: client_credentials,password
scope: server
6.3. 配置Resource Server
server-hi
模块作为 Resource Server
(资源服务),需要进行 Resource Server
的相关配置,配置代码如下:
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 对用户注册的URL地址开放
.antMatchers("/user/registry").permitAll()
.anyRequest().authenticated();
}
}
6.4. 配置OAuth2 Client
@Configuration
@EnableOAuth2Client
@EnableConfigurationProperties
public class OAuth2ClientConfig {
@Bean
@ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
// 配置受保护资源的信息
return new ClientCredentialsResourceDetails();
}
@Bean
public RequestInterceptor oauth2FeignRequestInterceptor(){
// 配置一个拦截器,对于每一个外来的请求,都会在request域内创建一个AccessTokenRequest类型的bean。
return new OAuth2FeignRequestInterceptor(
new DefaultOAuth2ClientContext(),
clientCredentialsResourceDetails());
}
@Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
// 用于向认证服务器服务请求token
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}
6.5. 创建用户注册接口
把 service-auth
模块的 User.java
和 UserRepository.java
拷贝到 service-hi
模块中。创建 UserService
用于 创建用户,并对 用户密码 进行 加密。
UserService.java
@Service
public class UserService {
private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
@Autowired
private UserRepository userRepository;
public User create(String username, String password) {
User user = new User();
user.setUsername(username);
String hash = encoder.encode(password);
user.setPassword(hash);
return userRepository.save(user);
}
}
UserController.java
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/registry", method = RequestMethod.POST)
public User createUser(@RequestParam("username") String username,
@RequestParam("password") String password) {
return userService.create(username,password);
}
}
6.6. 创建资源服务接口
@RestController
public class HiController {
private static final Logger LOGGER = LoggerFactory.getLogger(HiController.class);
@Value("${server.port}")
private String port;
/**
* 不需要任何权限,只要Header中的Token正确即可
*/
@RequestMapping("/hi")
public String hi() {
return "hi : " + ",i am from port: " + port;
}
/**
* 需要ROLE_ADMIN权限
*/
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping("/hello")
public String hello() {
return "hello you!";
}
/**
* 获取当前认证用户的信息
*/
@GetMapping("/getPrinciple")
public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication,
Principal principal,
Authentication authentication){
LOGGER.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
LOGGER.info(oAuth2Authentication.toString());
LOGGER.info("principal.toString()" + principal.toString());
LOGGER.info("principal.getName()" + principal.getName());
LOGGER.info("authentication:" + authentication.getAuthorities().toString());
return oAuth2Authentication;
}
}
6.6. 配置应用的启动类
@EnableEurekaClient
@SpringBootApplication
public class ServiceHiApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceHiApplication.class, args);
}
}
依次启动 eureka-service
,service-auth
和 service-hi
三个服务。
7. 使用PostMan验证
- 注册一个用户,返回注册成功信息
- 基于密码模式从认证服务器获取
token
- 访问资源服务
/hi
,不需要权限,只要token
正确即可
- 访问资源服务
/hello
,提示需要ROLE_ADMIN
权限
- 访问不成功,修改数据库的
role
表,添加 权限信息ROLE_ADMIN
,然后在user_role
表关联下再次访问
总结
本案列架构有仍有改进之处。例如在 资源服务器 加一个 登录接口,该接口不受 Spring Security
保护。登录成功后,service-hi
远程调用 auth-service
获取 token
返回给浏览器,浏览器以后所有的请求都需要携带该 token
。
这个架构的缺陷就是,每次请求 都需要由 资源服务 内部 远程调用 service-auth
服务来 验证 token
的正确性,以及该 token
对应的用户所具有的 权限,多了一次额外的 内部请求开销。如果在 高并发 的情况下,service-auth
需要以 集群 的方式部署,并且需要做 缓存处理。所以最佳方案还是结合 Spring Security OAuth2
和 JWT
一起使用,来实现 Spring Cloud
微服务系统的 认证 和 授权。
参考
- 方志朋《深入理解Spring Cloud与微服务构建》
欢迎关注技术公众号:零壹技术栈
本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。