OAuth 2.0 是目前最流行的授权标准(协议),用来授权第三方应用,获取用户数据。
OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
SSO :单点登录(Single sign-on)是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
CAS :中央认证服务(Central Authentication Service),一个基于Kerberos票据方式实现SSO单点登录的框架,为Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。
通常微服务的认证和授权思路有两种:
所有的认证授权都由一个独立的用户认证授权服务器负责,它只负责发放Token,然后网关只负责转发请求到各个微服务模块,由微服务各个模块自行对Token进行校验处理。
另一种是网关不但承担了流量转发作用,还承担认证授权流程,当前请求的认证信息由网关中继给下游服务器。
第二种结合了OAuth2体系,网关不仅仅承担流量转发功能,认证授权也是在网关层处理的,令牌会中继给下游服务。这种模式下需要搭建一个UAA(User Account And Authentication)服务。它非常灵活,它可以管理用户,也可以让受信任的客户端自己管理用户,它只负责对客户端进行认证(区别于用户认证)和对客户端进行授权。目前使用OAuth2对微服务进行安全体系建设的都使用这种方式。
oauth2包含以下角色:
客户端:本身不存储资源,需要通过资源拥有者授权去请求资源服务器的资源。
资源拥有者:通常为用户
服务类型:
授权(认证)服务器:用于服务提供商对资源拥有者的身份进行认证、对访问资源进行授权,认证成功颁发令牌(access_token),作为客户端访问资源服务器的凭据,配置认证服务器必须实现的endpoints:
/oauth/authorize
/oauth/token
资源服务器:存储资源的服务器,该资源服务需要实现OAuth2的过滤器(如果使用了GateWay作为服务统一出口,那么这个过滤器再gateway上声明就可以):
<!-- springboot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 服务注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 公共模块 -->
<dependency>
<groupId>top.ygang</groupId>
<artifactId>grady-young-common</artifactId>
<version>1.0</version>
</dependency>
<!-- 服务配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- spring-security依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<!-- mybatis-plus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 连接池,用于创建多个redis-template实例 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
/**
* 用来配置支持的客户端详情,在这里初始化客户端详情配置
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
/**
* 用来配置令牌访问端点和令牌服务,用来生成、颁发令牌
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
/**
* 对于令牌端点的安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
}
客户端详情配置(ClientDetailsServiceConfigurer),可以使用内存存储也可以使用JDBC数据库存储,ClientDetails重要的属性:
clientId:标识客户端ID
secret:客户端安全码(受信任的客户端才有)
scope:限制客户端访问范围,如果为空(默认),客户端可以访问所有资源
authorizedGrantTypes:客户端可以使用的授权类型
authorities:客户端的权限(基于spring security authorities)
/**
* 用来配置支持的客户端详情,在这里初始化客户端详情配置
* @param clients
* @throws Exception
*/
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用内存方式配置
clients.inMemory()
.withClient("test") //客户端id
.secret("abcdef-abcdef")//客户端密钥
.resourceIds("r1") //客户端可访问的资源列表
.authorizedGrantTypes( //客户端可用来申请令牌的方式
"authorization_code",
"password",
"client_credentials",
"implicit",
"refresh_token")
.scopes("all") //允许访问的范围
.autoApprove(false) //跳转到授权页面
.redirectUris("https://www.baidu.com"); //验证回调地址
}
管理令牌服务(AuthorizationServerTokenServices),接口定义了一些操作可以对令牌进行管理
定义TokenConfig
@Configuration
public class TokenConfig {
/**
* 配置令牌存储策略
* @return
*/
@Bean
public TokenStore tokenStore(){
// 内存方式,生成普通令牌
return new InMemoryTokenStore();
}
}
在上面写的授权服务配置AuthorizationServer中,配置令牌服务
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private TokenStore tokenStore;
/**
* 配置令牌服务
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices(){
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService); // 配置客户端信息服务
services.setSupportRefreshToken(true); //是否产生刷新令牌
services.setTokenStore(tokenStore); //配置令牌存储策略
services.setAccessTokenValiditySeconds(7200); //令牌有效期
services.setRefreshTokenValiditySeconds(259200); //刷新令牌有效期
return services;
}
配置令牌访问端点(AuthorizationServerEndpointsConfigurer),这个配置对象有方法pathMapping()
可以替换默认端点,第一个参数是要替换的默认端点,第二个参数是自定义端点。默认端点有:
/oauth/authorize
:授权端点
/oauth/token
:令牌端点
/oauth/confirm_access
:用户确认授权提交端点
/oauth/error
:授权服务错误信息端点
/oauth/check_token
:用于资源服务进行令牌解析、校验的端点
/oauth/token_key
:提供公有密钥的端点
/**
* 暂时使用采用内存方式作为授权码存储
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
/**
* 用来配置令牌访问端点和令牌服务,用来生成、颁发令牌
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) // 用于密码模式或其他自定义的认证模式
.authorizationCodeServices(authorizationCodeServices) // 授权码模式
.tokenServices(authorizationServerTokenServices()) // 令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST); // 允许post方式
}
令牌端点安全约束(AuthorizationServerSecurityConfigurer)
/**
* 对于令牌端点的安全约束
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.checkTokenAccess("permitAll()") //允许任何校验令牌的请求
.tokenKeyAccess("permitAll()") //允许提供公钥
.allowFormAuthenticationForClients(); //允许表单认证
}
3、配置SecurityConfig、userDetailsService,具体配置参考前面单体服务
授权码由第三方(认证服务器)颁发,可以使用授权码获取令牌,具体流程:
1、资源拥有者打开客户端,客户端要求资源拥有者授权,他将浏览器重定向到授权服务器,附带客户端信息
GET /oauth/authorize?client_id=test&response_type=code&scope=all&redirect_url=https://www.baidu.com
client_id
:客户端id
response_type
:授权模式,固定为code
scope
:客户端权限
redirect_url
:跳转url,申请授权码成功会跳转到此地址,后面会加上授权码code
2、认证账号密码
3、登录成功,跳转授权页面,选择第一个Approve,进行授权
4、跳转到之前的redirect_url,并且附带了code
5、使用授权码申请令牌,一个授权码只能使用一次
POST /oauth/token
application/x-www-form-urlencoded
client_id 客户端id
client_secret 客户端密钥
grant_type 申请模式(授权码模式固定为authorization_code)
code 授权码
redirect_url 跳转地址,和申请授权码时一致
和授权码模式第一步比较相似,只不过grant_type改为token,请求验证成功后,会直接将token以hash模式附带在redirect_url后面,请求地址:
GET /oauth/authorize?client_id=test&response_type=token&scope=all&redirect_url=https://www.baidu.com
得到结果:
密码模式可能会泄漏账号密码给客户端,所以一般用在自己写的客户端
POST /oauth/token
application/x-www-form-urlencoded
client_id 客户端id
client_secret 客户端密钥
grant_type 申请模式(密码模式固定为password)
username 用户名
password 密码
redirect_url 跳转地址,和申请授权码时一致
客户端模式比密码模式还要简单,只需要客户端id、客户端密钥就可以申请token
POST /oauth/token
application/x-www-form-urlencoded
client_id 客户端id
client_secret 客户端密钥
grant_type 申请模式(客户端模式固定为client_credentials)
redirect_url 跳转地址,和申请授权码时一致
1、在TokenConfig中配置JWT相关,使用对称加密,配置盐值
@Configuration
public class TokenConfig {
/**
* jwt令牌加密盐值
*/
public static final String SIGNING_KEY = "sojff#OJSDF-9IOFH*124";
/**
* jwt令牌存储策略
* @return
*/
@Bean
public JwtTokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* jwt令牌生成策略
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
2、在AuthorizationServer中配置令牌增强,使用jwt令牌,使用新的JwtTokenStore,替换之前的TokenStore
/**
* 配置令牌服务
* @return
*/
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices(){
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService); // 配置客户端信息服务
services.setSupportRefreshToken(true); //是否产生刷新令牌
services.setTokenStore(jwtTokenStore); //配置令牌存储策略
// 配置令牌增强,使用jwt
TokenEnhancerChain chain = new TokenEnhancerChain();
chain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
services.setTokenEnhancer(chain);
services.setAccessTokenValiditySeconds(7200); //令牌有效期
services.setRefreshTokenValiditySeconds(259200); //刷新令牌有效期
return services;
}
3、重新请求令牌,发现令牌已经成功使用jwt进行生成
4、校验令牌,发现用户信息已经存储
OAuth2.0的服务端和资源端都不是我们自己写的,都是springsecurity框架给我们写的,既然是springsecurity框架的,那么客户端的信息保存在数据库里面的时候,这个数据库的表结构就需要使用springsecurity框架定义的。
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL COMMENT '客户端id',
`resource_ids` varchar(255) DEFAULT NULL COMMENT '客户端所能访问的资源id集合',
`client_secret` varchar(255) DEFAULT NULL COMMENT '用于指定客户端(client)的访问密匙',
`scope` varchar(255) DEFAULT NULL COMMENT '指定客户端申请的权限范围',
`authorized_grant_types` varchar(255) DEFAULT NULL COMMENT '指定客户端支持的grant_type',
`web_server_redirect_uri` varchar(255) DEFAULT NULL COMMENT '客户端的重定向URI,',
`authorities` varchar(255) DEFAULT NULL COMMENT '指定客户端所拥有的Spring Security的权限值',
`access_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的access_token的有效时间值(单位:秒),可选,默认的有效时间值12小时',
`refresh_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的refresh_token的有效时间值',
`additional_information` varchar(255) DEFAULT NULL COMMENT '预留的字段',
`autoapprove` varchar(255) DEFAULT NULL COMMENT '设置用户是否自动Approval操作'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL,
`authentication` longblob,
`refresh_token` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(255) DEFAULT NULL,
`clientId` varchar(255) DEFAULT NULL,
`scope` varchar(255) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` datetime DEFAULT NULL,
`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication_id` varchar(255) DEFAULT NULL,
`user_name` varchar(255) DEFAULT NULL,
`client_id` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) DEFAULT NULL,
`authentication` varbinary(2550) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(255) DEFAULT NULL,
`token` longblob,
`authentication` longblob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
对于之前存在内存中的信息改为存储到数据库
@Autowired
private DataSource dataSource;
/**
* 配置使用数据库作为授权码存储方式
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new JdbcAuthorizationCodeServices(dataSource);
}
/**
* 配置从数据库获得客户端详情
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(){
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}