一、环境介绍
Spring-Security-OAuth2是对OAuth2的一种实现,并且跟之前学习的Spring Security相辅相成,与Spring Cloud体系的集成也非常便利。
OAuth2.0的服务提供方涵盖两个服务,即授权服务(Authorization Server,也即认证服务)和资源服务(Resource Server),使用Spring Security OAuth2.0的时候,可以选择把它们放在同一个应用中实现,也可以选择建立使用同一个授权服务的多个资源服务。
授权服务(Authorization Server)应包含对接入端以及登录用户的合法性进行验证并颁发token等功能,对令牌的请求端点由Spring MVC控制器进行实现,下面配置一个认证服务必须要实现的endpoints:
- AuthorizationEndpoint 服务于认证请求,默认URL:/login/authorize。
- TokenEndpoint 服务于访问令牌的请求,默认URL:/login/token。
-
OAuth2AuthenticationProcessingFilter
用来对请求给出的身份令牌解析鉴权。
本篇教程分别创建UAA授权服务(也可叫认证服务)和订单资源服务。
认证流程如下:
1、客户端请求UAA授权服务进行认证。
2、认证通过后由UAA颁发令牌。
3、客户端携带令牌Token请求资源服务。
4、资源服务校验令牌的合法性,合法即返回资源信息。
二、环境搭建
2.1 父工程
创建maven父工程distributed-security,pom依赖如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.pengjs.book.admin.distributed.security</groupId>
<artifactId>distributed-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>distributed-security</name>
<packaging>pom</packaging>
<description>distributed-security</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<modules>
<module>distributed-security-uaa</module>
<module>distributed-security-order</module>
<module>distributed-security-discovery</module>
<module>distributed-security-gateway</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!--父模块,编译插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.2 UAA授权服务工程
1、创建distributed-security-uaa作为授权服务工程,依赖如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pengjs.book.admin.distributed.security</groupId>
<artifactId>distributed-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.pengjs.book.admin.distributed.security.uaa</groupId>
<artifactId>distributed-security-uaa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>distributed-security-uaa</name>
<description>distributed-security-uaa</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
工程结构如下:
2、启动类
@SpringBootApplication
@EnableHystrix
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.pengjs.book.admin.distributed.security.uaa"})
public class DistributedSecurityUaaApplication {
public static void main(String[] args) {
SpringApplication.run(DistributedSecurityUaaApplication.class, args);
}
}
3、配置文件application.yml
server:
port: 53020
servlet:
context-path: /uaa
tomcat:
remote-ip-header: x-formarded-for
protocol-header: x-formarded-proto
use-forward-headers: true
spring:
application:
name: uaa-service
http:
encoding:
enabled: true
charset: UTF-8
force: true
main:
allow-bean-definition-overriding: true
freemarker:
enabled: true
suffix: .html
request-context-attribute: rc
content-type: text/html
charset: UTF-8
mvc:
throw-exception-if-no-handler-found: true
resources:
add-mappings: false
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///oauth2?useUnicode=true&characterEncoding=utf8
username: root
password: root
logging:
level:
root: debug
org:
springframework:
web: info
eureka:
client:
serviceUrl:
defaultZone: http://localhost:53000/eureka/
instance:
preferIpAddress: true
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance-id:${server.port}}
management:
endpoints:
web:
exposure:
include: refresh,health,info,env
feign:
hystrix:
enabled: true
compression:
request:
enabled: true
mime-types[0]: text/html
mime-types[1]: application/xml
mime-types[2]: application/json
min-request-size: 2048
response:
enabled: true
2.3 Order订单资源服务工程
本工程为Order订单服务工程,访问工程的资源需要通过认证。
1、创建Order工程,pom依赖如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pengjs.book.admin.distributed.security</groupId>
<artifactId>distributed-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.pengjs.book.admin.distributed.security.order</groupId>
<artifactId>distributed-security-order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>distributed-security-order</name>
<description>distributed-security-order</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<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-actuator</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-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
工程结构如下:
2、配置文件application.yml
server:
port: 53021
servlet:
context-path: /order
tomcat:
remote-ip-header: x-formarded-for
protocol-header: x-formarded-proto
use-forward-headers: true
spring:
application:
name: order-service
http:
encoding:
enabled: true
charset: UTF-8
force: true
main:
allow-bean-definition-overriding: true
freemarker:
enabled: true
suffix: .html
request-context-attribute: rc
content-type: text/html
charset: UTF-8
mvc:
throw-exception-if-no-handler-found: true
resources:
add-mappings: false
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///oauth2?useUnicode=true&characterEncoding=utf8
username: root
password: root
logging:
level:
root: debug
org:
springframework:
web: info
eureka:
client:
serviceUrl:
defaultZone: http://localhost:53000/eureka/
instance:
preferIpAddress: true
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance-id:${server.port}}
management:
endpoints:
web:
exposure:
include: refresh,health,info,env
feign:
hystrix:
enabled: true
compression:
request:
enabled: true
mime-types[0]: text/html
mime-types[1]: application/xml
mime-types[2]: application/json
min-request-size: 2048
response:
enabled: true
三、授权服务配置
3.1 EnableAuthorizationServer
可以使用@EnableAuthorizationServer
注解并继承AuthorizationServerConfigurerAdapter
来配置OAuth2.0
授权服务器。
在config包下创建AuthorizationServer
:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
// 略...
}
AuthorizationServerConfigurerAdapter
要求配置以下几个类,这几个类是由Spring创建的独立的配置对象,他们会被Spring传入AuthorizationServerConfigurer
中进行配置。
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
}
-
ClientDetailsServiceConfigurer:用来配置客户端详情服务(
ClientDetailsService
),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者通过数据库来存储和查询详情信息。 - AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
- AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束。
3.2 配置客户端详情信息
ClientDetailsServiceConfigurer
能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService
),ClientDetailsService
负责找到ClientDetails
,而ClientDetails
有几个重要的如下:
- clientId:(必须的)用来标识客户端的id。
- secret:(需要知得新人的客户端)客户端安全嘛码,如果有的话。
- scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
- authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
- authorities:此客户端可以使用的权限(基于Spring Security authorities)。
客户端详情(Client Details
)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户端详情存储在一个关系型数据库表中,皆可以使用JdbcClientDetailsService
)或者通过自己实现ClientRegistrationService
接口(同时你也可以实现ClientDetailsService
接口)来进行管理。
这里暂时使用内存方式存储客户端详情信息,配置爱如下:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clientDetailsService使用jdbc查询数据库的方式
// clients.withClientDetails(clientDetailsService);
// 使用in-memory存储
clients.inMemory()
// client_id
.withClient("c1")
// 客户端秘钥
.secret(new BCryptPasswordEncoder().encode("secret"))
// 客户端可以访问的资源列表
.resourceIds("res1")
// 该client允许的授权范围(所有支持的5种)
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
// 允许的授权范围(客户端的权限),user-service、read等标识
.scopes("all")
// false:授权码模式,跳转到授权的页面,如果是true,不用跳转页面
.autoApprove(false)
// 加上验证回调地址
.redirectUris("http://www.baidu.com");
}
3.3 管理令牌
AuthorizationServerTokenServices
接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。
自己可以创建AuthorizationServerTokenServices
这个接口的实现,则需要继承DefaultTokenServices
(实现了AuthorizationServerTokenServices
)这个类,里面已经包含了一些有用的实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个TokenStore
接口来实现的以外,这个类几乎帮你做了所有的事情。并且TokenStore
这个接口有一个默认的时间,它就是InMemoryTokenStore
,如其命名,所有的令牌是被保存在内存中。除了使用这个以外,你还可以使用以下其他的预定义实现,下面有几个版本,他们都实现了TokenStore
接口:
- InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为他不会被保存到磁盘中,更便于调试。
- JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,你可以死在不同服务器之间共享领牌子信息,使用这个版本的时候,请注意把“spring-jdbc”这个依赖加入到pom中。
-
JwtTokenStore:这个版本的全称是JSON Web Token(JWT),它把令牌相关的数据惊醒编码(因此对于后端服务来说,他不需要进行存储,这将是一个重大优势),但是它有一个缺点,就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点是这个令牌占用的空间会比较大,如果你加入了比较多的用户凭证信息。
JwtTokenStore
不会保存任何数据,但是它在转换令牌值以及授权信息方面与DefaultTokenServices
所扮演的角色是一样的。
3.3.1 定义TokenConfig
在config包下定义TokenConfig
,这里暂时先使用InMemoryTokenStore
,生成一个普通的令牌。
@Configuration
public class TokenConfig {
/**
* 临牌的存储策略
* @return TokenStore
*/
@Bean
public TokenStore tokenStore() {
// 使用内存方式存储令牌(普通令牌)
return new InMemoryTokenStore();
}
}
3.3.2 定义AuthorizationServerTokenServices
在AuthorizationServer中定义AuthorizationServerTokenServices
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 令牌管理服务
* @return
*/
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
// 客户端详情服务
service.setClientDetailsService(clientDetailsService);
// 是否产生支持刷新令牌
service.setSupportRefreshToken(true);
// 令牌存储策略
service.setTokenStore(tokenStore);
// 设置令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
// 令牌桶默认有效期2小时
service.setAccessTokenValiditySeconds(7200);
// 刷新令牌默认有效期3天
service.setRefreshTokenValiditySeconds(259200);
return service;
}
3.4 令牌访问端点配置
AuthorizationServerEndpointsConfigurer
这个对象的实例可以完成令牌服务以及令牌endpoint配置。
3.4.1 配置授权类型(Grant Types)
AuthorizationServerEndpointsConfigurer
通过设定以下属性决定支持的授权类型(Grant Types):
-
authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个
AuthenticationManager
对象。 -
userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的
UserDetailsService
接口的实现,或者你可以把这个东西设置到全局域上面去(例如GlobalAuthenticationManagerConfigurer
这个配置对象),当你设置了这个之后,那么“refresh_token”即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账号的话。 -
authorizationCodeServices:这个属性是用来设置授权码服务的(即
AuthorizationCodeServices
的实例对象),主要用于“authorization_code”授权码类型模式。 - implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
-
tokenGranter:当你设置了这个属性(即
TokenGranter
接口实现),那么授权将交由你来完全掌控,并且会忽略掉上面的几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。
3.4.2 配置授权端点URL(Endpoint URLs)
AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做pathMapping()
的方法用来配置端点URL链接,它有两个参数:
- 第一个参数:String类型,这个端点URL的默认链接。
- 第二个参数:String类型,你要进行替换的URL链接。
以上的参数都将以“/”字符开始的字符串,框架的默认URL链接如下列表,可以作为这个pathMapping()
方法的第一个参数:
- /oauth/authorize:授权端点。
- /oauth/token:令牌端点。
- /oauth/confirm_access:用户确认授权提交端点。
- /oauth/error:授权服务错误信息端点。
- /oauth/check_token:用于资源访问的令牌解析端点。
- /oauth/token_key:提供公有秘钥的端点,如果你使用JWT令牌的话。
需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问。
在AuthorizationServer
中配置爱令牌访问端点:
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
/**
* 令牌访问端点
* tokenService():令牌管理服务
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// authenticationManager:认证管理器,密码模式
// authorizationCodeServices:授权码服务
endpoints.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices)
// 令牌管理服务,都需要
.tokenServices(tokenService())
// 允许的POST提交
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
3.5 令牌访问端点的安全约束
AuthorizationServerSecurityConfigurer 用来配置令牌端点(Token Endpoint)的安全约束,在AuthorizationServer
中配置如下:
/**
* 令牌端点的安全配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// /oauth/token_key: 这个endpoint当使用jwttoken且使用非对称加密时,资源服务器用于获取公钥而开放的,这里指这个endpoint完全公开
security.tokenKeyAccess("permitAll()")
// oauth/check_token: checkToken这个endpoint完全公开
.checkTokenAccess("permitAll()")
// 允许表单认证
.allowFormAuthenticationForClients();
}
- tokenKeyAccess("permitAll()"):“/oauth/token_key”这个endpoint当使用jwttoken且使用非对称加密时,资源服务器用于获取公钥而开放的,这里指这个endpoint完全公开。
- checkTokenAccess("permitAll()"):“auth/check_token“checkToken这个endpoint完全公开。
- allowFormAuthenticationForClients():允许表单认证。
授权服务配置总结:授权服务配置分成三大块,可以关联记忆。
既然要完成认证,它首先得先知道客户端信息从哪里读取,因此要进行客户端详情配置。
既然要颁发token,那必须得定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的token。
既然暴露了一些endpoint,那对这些endpoint可以定义一些安全上的约束等等。
3.6 web安全配置
/**
* @EnableGlobalMethodSecurity(securedEnabled = true) 启用基于注解的安全性,可以使用@Secured注解
*/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码编码器(采用什么方式比对)
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证管理器
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* 安全拦截机制(最重要)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 屏蔽CSRF控制,即spring security不再限制CSRF(跨站请求伪造)
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
四、授权码模式
4.1 授权码模式介绍
如下图是授权码模式交互图:
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,浏览器重定向到授权服务器,重定向时会附加客户端的身份信息,如:/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
参数列表如下:
- client_id:客户端准入标识。
- response_type:授权码模式固定为code。
- scope:客户端授权。
- redirect_uri:跳转URI,当授权码申请成功后跳转到此地址,并在后边带上code参数(授权码)。
(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。
(3)授权浏览器将授权码(AuthorizationCode)经浏览器发送给client(通过redirect_uri)。
(4)客户端拿到授权码向授权服务器索要访问access_token,请求如下:
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
参数列表如下:
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写authorization_code,表示授权码模式。
- code:授权码,就是刚刚获取到的授权码,注意:授权码只是用一次就无效了,需要重新申请。
- redirect_uri:申请授权码时的跳转url,一定要和申请授权码时用的redirect_uri一致。
4.2 授权码模式演示
4.2.1 授权登录获取授权码访问
浏览器访问如下URL:http://127.0.0.1:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
自动跳转到授权登录页面:
4.2.2 使用授权码去申请token
curl --location --request POST 'http://127.0.0.1:53020/uaa/oauth/token' \
--form 'client_id=c1' \
--form 'client_secret=secret' \
--form 'grant_type=authorization_code' \
--form 'code=6Q2nz7' \
--form 'redirect_uri=http://www.baidu.com'
响应结果:
{
"access_token": "04255fc1-2e95-4419-a819-1bab1bfed73f",
"token_type": "bearer",
"refresh_token": "d983f5f3-78df-4ee6-9088-edc6098ffbfe",
"expires_in": 6925,
"scope": "all"
}
一个授权码只能使用一次:
五、简化模式
5.1 简化模式介绍
下图是简化模式交互图:
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器重定向到授权服务器,重定向时会附加客户端的身份信息,如:/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
,参数描述同授权码模式,注意:response_type=token
表示简化模式。
(2)浏览器出现授权服务器授权页面,之后将用户信息同意授权。
(3)授权服务器将授权令牌以hash的形式存放在重定向的URI的fargument参数中发送给浏览器。
注:fargument
主要用来标识URI所标识资源里的某个资源,在URI的末尾通过(#)作为fargument
的开头,其中#
不属于fargument
的值,如https://domain/index#L18
这个URI中L18
就是fargument
的值。大家只需要知道js通过响应浏览器地址栏变化的方式来获取到fargument就行了。
一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。
5.2 简化模式演示
浏览器中输入URI:http://127.0.0.1:53020/uaa/oauth/authorize?client_id=c1&response_type=token&scpoe=all&redirect_uri=http://www.baidu.com
(注意:response_type=token 简化模式)
https://www.baidu.com/#access_token=c22bc67a-e4f8-45e9-81ec-c8022a4a4d74&token_type=bearer&expires_in=5979&scope=all
六、密码模式
6.1 密码模式介绍
下图是密码模式交互图:
(1)资源拥有者将用户名、密码发送给客户端
(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123
参数列表如下:
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写password表示密码模式。
- username:支援拥有者的用户名。
- password:支援拥有者密码。
(3)授权服务器将令牌(access_token)发送给client
这种模式十分简单,但是却意味着直接将用户敏感信息邪路给了client,因此这就说明这种模式只能用于client使我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。
注意:如果要让密码模式生效时需要在授权模式中配置的
6.2 密码模式演示
使用POST方式:http://127.0.0.1:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123
七、客户端模式
7.1 客户端模式介绍
(1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)
(2)确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
参数列表如下:
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写client_credentials表示客户端模式。
这种模式是最方便但最不安全的模式,因此者及要求我们对client完全的信任,而client本省也是安全的,因此这种模式一般用来提供给我们完全信任的服务器端服务,比如,合作方系统对接,拉取一组用户信息。
7.2 客户端模式演示
POST方式:http://127.0.0.1:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
八、资源服务测试
8.1 资源服务配置
@EnableResourceServer
注解到一个@Configuration
配置类上,并且必须使用ResourceServerConfig
这个配置对象来进行配置(可以继承自ResourceServerConfigurerAdapter
然后覆写其中的方法,参数就是这个对象的实例),下面是一些可以配置的属性。
ResourceServerSecurityConfigurer
中主要包括:
-
tokenServices:
ResourceServerTokenServices
类的实例,用来实现令牌服务。 -
tokenStore:
TokenStore
类的实例,指定令牌如何访问,与tokenServices配置可选。 - resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务器中进行验证。
- 其他的拓展属性例如tokenExtractor令牌提取器,用来提取请求中的令牌。
HttpSecurity
配置这个与Spring Security类似:
- 请求匹配器,用来设置需要惊醒保护的资源路径,默认的情况下是保护资源服务的全部路径。
- 通过http.authorizeRequests()来设置受保护资源的访问规则。
- 其他的自定义权限保护规则通过HttpSecurity来进行配置。
@EnableResourceServer
注解自动增加了一个类型为OAuth2AuthenticationProcessingFilter
的过滤器链
编写ResourceServerConfig
:
@Configuration
@EnableResourceServer
// 或者是单独配置WebSecurityConfig也可以
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
/**
* 资源列表,与服务端的resourceIds一致
*/
private static final String RESOURCE_ID = "res1";
/**
* 从TokenConfig中注入tokenStore
*/
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 资源id
resources.resourceId(RESOURCE_ID)
// 验证令牌的服务
.tokenServices(tokenService())
.stateless(true);
}
/**
* 资源访问策略
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.access("#oauth2.hasScope('all')") // 所有的请求都必须有scope=all,跟服务端一致
.and().csrf().disable() // 关闭CSRF
// 基于token的方式,session就不用再记录了
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 资源服务令牌解析服务
* @return
*/
@Bean
public ResourceServerTokenServices tokenService() {
// 使用远程服务请求授权服务校验token,必须制定校验token的URL、client_id、client_secret
RemoteTokenServices service = new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
service.setClientId("c1");
service.setClientSecret("secret");
return service;
}
}
8.2 验证token
ResourceServerTokenServices
是组成授权服务的另一半,如果你的授权服务和资源服务在同一个应用程序中的话,你可以使用DefaultTokenServices(ResourceServerTokenServices的实现类),这样的话,你就不用考虑关于实现所有必要的接口的一致性问题。如果你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的ResourceServerTokenServices
,它知道如何对令牌进行解码。
令牌解析方法:
- 使用
DefaultTokenServices
在资源服务器本地配置令牌存储、解码、解析方式。 - 使用
RemoteTokenServices
资源服务器通过Http请求来解码令牌,每次都请求授权服务器端点/oauth/check_token
使用授权服务的/oauth/check_token
端点,你需要在石泉服务中将这个端点暴露出去,一遍资源服务可以进行访问,这在咱们的授权服务配置中已经提到了,下面是一个例子,我们在授权服务中配置了/oauth/check_token
和/oauth/check_key
两个端点:
/**
* 令牌端点的安全配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// /oauth/token_key: 这个endpoint当使用jwttoken且使用非对称加密时,资源服务器用于获取公钥而开放的,这里指这个endpoint完全公开
security.tokenKeyAccess("permitAll()")
// oauth/check_token: checkToken这个endpoint完全公开
.checkTokenAccess("permitAll()")
// 允许表单认证
.allowFormAuthenticationForClients();
}
在资源服务中配置RemoteTokenServices,在ResourceServerConfig中配置:
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 资源id
resources.resourceId(RESOURCE_ID)
// 验证令牌的服务
.tokenServices(tokenService())
.stateless(true);
}
/**
* 资源服务令牌解析服务
* @return
*/
@Bean
public ResourceServerTokenServices tokenService() {
// 使用远程服务请求授权服务校验token,必须制定校验token的URL、client_id、client_secret
RemoteTokenServices service = new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
service.setClientId("c1");
service.setClientSecret("secret");
return service;
}
调用授权服务check_token示例:
POST方式:http://127.0.0.1:53020/uaa/oauth/check_token?token=92f32994-c429-4314-a96d-3a85078f4328
8.3 编写资源
在controller包下编写OrderController,此controller表示订单资源的访问类:
@RestController
public class OrderController {
/**
* 流程:当携带token访问这个资源的时候,会通过远程的service去请求地址
* http://localhost:53020/uaa/oauth/check_token校验token是否合法
* @return
*/
@GetMapping(value = "/r1")
// 拥有p1权限方可访问此URL
@PreAuthorize("hasAnyAuthority('p1')")
public String r1() {
// 通过spring security api获取当前登录用户
// UserDto userDto = (UserDto) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return username + "访问资源1";
// return userDto.getUsername() + "访问资源1";
// return JSON.toJSONString(userDto) + "访问资源1";
}
@GetMapping(value = "/r2")
// 拥有p2权限方可访问此URL
@PreAuthorize("hasAnyAuthority('p2')")
public String r2() {
UserDto userDto = (UserDto) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// return userDto.getUsername() + "访问资源2";
return JSON.toJSONString(userDto) + "访问资源2";
}
}
8.4 测试
- 申请令牌,这里使用密码方式
http://127.0.0.1:53020/uaa/oauth/token
响应结果:
{
"access_token": "e6f8f518-8ec6-4b79-94cd-fee1ee689030",
"token_type": "bearer",
"refresh_token": "629e37b4-7115-4ccd-9d78-c91f047bc646",
"expires_in": 6616,
"scope": "all"
}
- 请求资源
按照OAuth2.0协议要求,请求资源需要携带token,参数名称为Authorization,值为Bearer token值,POST方式http://localhost:53021/order/r1
8.5 添加安全访问控制
/**
* 安全访问控制
*/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 安全拦截机制(最重要)
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
// 先在是基友方法的授权,基于web的授权可以屏蔽掉
.authorizeRequests()
// .antMatchers("/r/r1").hasAnyAuthority("p1")
// .antMatchers("/r/r2").hasAnyAuthority("p2")
.antMatchers("/r/**").authenticated() // 所有/r/**的请求都必须认证通过
.anyRequest().permitAll(); // 除了/r/**,其他的请求都可以访问
}
}