前提:在security基础上,不会的可以参考我的另一篇文章
搭建授权服务器
OAuth2 协议一共支持 4 种不同的授权模式:
授权码模式:常见的第三方平台登录功能基本都是使用这种模式。
简化模式:简化模式是不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌(token),一般如果网站是纯静态页面则可以采用这种方式。
密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用说这些信息向授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务提供商就是同一家公司,我们自己做前后端分离登录就可以采用这种模式。
客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权,严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
+--------+ +---------------+ | |--(A)- 授权请求 ->| 资源 | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+
(A) 客户端向资源所有者请求授权。
授权请求可以直接发送给资源所有者(
如图所示),或者最好通过授权
服务器作为中介间接发送。
(B) 客户端接收到授权授权,它是
代表资源所有者授权的凭证,
使用本规范中定义的四种授权类型之一
或使用扩展授权类型表示。
授权类型取决于
客户端请求授权的方式和授权服务器支持的类型
。
(C) 客户端通过与授权服务器进行身份验证并提供授权许可
来请求访问令牌。
(D) 授权服务器对客户端进行身份验证并验证
授权授予,如果有效,则颁发访问令牌。
(E) 客户端从资源
服务器请求受保护的资源,并通过提供访问令牌进行身份验证。
(F) 资源服务器验证访问令牌,如果有效,则为
请求提供服务。
配置
-
可以用 @EnableAuthorizationServer 注解并继承 AuthorizationServerConfifigurerAdapter 来配置 OAuth2.0 授权服务器。
/**
* oauth2
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
略。。。
}
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
public AuthorizationServerConfigurerAdapter() {}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
}
-
配置客户端详细信息
- clientId:(必须的)用来标识客户的Id。
- secret:(需要值得信任的客户端)客户端安全码,如果有的话
- scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
- authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
- authorities:此客户端可以使用的权限(基于Spring Security authorities)。
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 配置客户端认证(谁来申请令牌)使用内存
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//暂时使用内存
clients.inMemory() //使用内存
.withClient("c1") //client_id
.secret(passwordEncoder.encode("secret")) //密钥
.resourceIds("res1","res2") //可使用的资源列表
// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")
.scopes("all")// 允许的授权范围,一个字符串
.autoApprove(false)//false跳转到授权页面
//加上验证回调地址
.redirectUris("http://www.baidu.com");
}
管理令牌
- InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
- JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath当中。
- JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对 于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。
1、定义TokenConfifig
package com.security.oauth2.oauth2service.config.oauth2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
@Configuration
public class TokenConfig {
/**
* 令牌策略-内存方式
* @return
*/
@Bean
public TokenStore tokenStore(){
//内存方式
return new InMemoryTokenStore();
}
}
2、定义AuthorizationServerTokenServices
//注入令牌策略-内存方式
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 配置令牌服务
*/
@Bean
public AuthorizationServerTokenServices tokenService(){
DefaultTokenServices services = new DefaultTokenServices();
//使用客户端配置
services.setClientDetailsService(clientDetailsService);
//是否使用刷新令牌
services.setSupportRefreshToken(true);
//令牌的存储策略
services.setTokenStore(tokenStore);
//令牌默认有效期2小时
services.setAccessTokenValiditySeconds(7200);
//刷新令牌默认有效期3天
services.setRefreshTokenValiditySeconds(259200);
return services;
}
令牌访问端点配置
- authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。
- userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现,或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfifigurer 这个配置对象),当你设置了这个之后,那么 "refresh_token" 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。
- authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 "authorization_code" 授权码类型模式。
- implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
- tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的需求的时候,才会考虑使用这个。
- 第一个参数:String 类型的,这个端点URL的默认链接。
- 第二个参数:String 类型的,你要进行替代的URL链接。
以上的参数都将以 "/" 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的第一个参数:
- /oauth/authorize:授权端点。
- /oauth/token:令牌端点。
- /oauth/confifirm_access:用户确认授权提交端点。
- /oauth/error:授权服务错误信息端点。
- /oauth/check_token:用于资源服务访问的令牌解析端点。
- /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。
//授权码服务 使用内存
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
//使用内存
return new InMemoryAuthorizationCodeServices();
}
//在security配置文件WebSecurityConfig 中加入bean容器
@Autowired
private AuthenticationManager authenticationManager;
/**
* 配置令牌的访问端点(申请令牌的地址)和令牌服务(令牌怎么发放)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//认证管理器,密码模式需要
.authorizationCodeServices(authorizationCodeServices())//授权码服务
.tokenServices(tokenService())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
令牌端点的安全约束
/**
* 令牌的安全约束(验证有没有资格申请令牌)
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //oauth/token_key是公开
.checkTokenAccess("permitAll()") //oauth/check_token公开
.allowFormAuthenticationForClients(); //表单认证(申请令牌)
}
在security配置文件WebSecurityConfig 中加入认证管理器bean容器
//认证管理器,oauth2需要,放入容器
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
package com.security.oauth2.oauth2service.config.oauth2;
import com.security.oauth2.oauth2service.config.security.MD5PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
/**
* oauth2
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 配置客户端认证(谁来申请令牌)使用内存
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//暂时使用内存
clients.inMemory() //使用内存
.withClient("c1") //client_id
.secret(passwordEncoder.encode("secret")) //密钥
.resourceIds("res1","res2") //可使用的资源列表
// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")
.scopes("all")// 允许的授权范围,一个字符串
.autoApprove(false)//false跳转到授权页面
//加上验证回调地址
.redirectUris("http://www.baidu.com");
}
//注入令牌策略-内存方式
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 配置令牌服务
*/
@Bean
public AuthorizationServerTokenServices tokenService(){
DefaultTokenServices services = new DefaultTokenServices();
//使用客户端配置
services.setClientDetailsService(clientDetailsService);
//是否使用刷新令牌
services.setSupportRefreshToken(true);
//令牌的存储策略
services.setTokenStore(tokenStore);
//令牌默认有效期2小时
services.setAccessTokenValiditySeconds(7200);
//刷新令牌默认有效期3天
services.setRefreshTokenValiditySeconds(259200);
return services;
}
//授权码服务 使用内存
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
//使用内存
return new InMemoryAuthorizationCodeServices();
}
//在security配置文件WebSecurityConfig 中加入bean容器
@Autowired
private AuthenticationManager authenticationManager;
/**
* 配置令牌的访问端点(申请令牌的地址)和令牌服务(令牌怎么发放)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//认证管理器,密码模式需要
.authorizationCodeServices(authorizationCodeServices())//授权码服务
.tokenServices(tokenService())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
/**
* 令牌的安全约束(验证有没有资格申请令牌)
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") //oauth/token_key是公开
.checkTokenAccess("permitAll()") //oauth/check_token公开
.allowFormAuthenticationForClients(); //表单认证(申请令牌)
}
}
启动访问
授权码方式:
/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参数(授权码)
/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一致。
测试
http://localhost:8081/demo2/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=lisi&password=123 http://localhost:8081/demo2/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com http://localhost:8081/demo2/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=lisi&password=123
输入zhangsan 123(注意数据库user表,账号密码对应,密码为md5加密串)
点击授权
确认授权后,会自动跳转到百度页面,路径上带着code,这就是申请令牌的code了
然后postman 用post方法访问:
http://localhost:8081/demo2/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=KMi0xa&redirect_uri=http://www.baidu.comhttp://localhost:8081/demo2/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=KMi0xa&redirect_uri=http://www.baidu.com
简化模式:
/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
测试:
输入zhangsan 123
密码模式
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写password表示密码模式
- username:资源拥有者用户名。
- password:资源拥有者密码。
测试:
客户端模式
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
- client_id:客户端准入标识。
- client_secret:客户端秘钥。
- grant_type:授权类型,填写client_credentials表示客户端模式
测试:
整合数据库:
添加表:
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端标 识',
`resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '接入资源列表',
`client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '客户端秘钥',
`scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` longtext CHARACTER SET utf8 COLLATE utf8_general_ci,
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
`archived` tinyint(4) DEFAULT NULL,
`trusted` tinyint(4) DEFAULT NULL,
`autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '接入客户端信息' ROW_FORMAT = Dynamic;INSERT INTO `oauth_client_details` VALUES ('c1', 'res1', '5EBE2294ECD0E0F08EAB7690D2A6EE69', 'ROLE_ADMIN,ROLE_USER,ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', NULL, 7200, 259200, NULL, '2022-04-25 11:18:26', 0, 0, 'false');
INSERT INTO `oauth_client_details` VALUES ('c2', 'res2', '5EBE2294ECD0E0F08EAB7690D2A6EE69', 'ROLE_API', 'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com', NULL, 31536000, 2592000, NULL, '2022-04-25 11:18:28', 0, 0, 'false');
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authentication` blob,
INDEX `code_index`(`code`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'Spring Security OAuth2使用,用来存储授权码' ROW_FORMAT = Compact;
修改AuthorizationServer中的客户认证配置:
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private DataSource dataSource;
@Bean
public ClientDetailsService clientDetails() {
//使用数据库
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService)
//配置密匙加密为md5
.setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
/**
* 配置客户端认证(谁来申请令牌)使用数据库
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
修改授权码配置跟令牌配置:
//注入令牌策略-内存方式
@Autowired
private TokenStore tokenStore;
/**
* 配置令牌服务
*/
@Bean
public AuthorizationServerTokenServices tokenService(){
DefaultTokenServices services = new DefaultTokenServices();
//使用客户端配置
services.setClientDetailsService(clientDetails());
//是否使用刷新令牌
services.setSupportRefreshToken(true);
//令牌的存储策略
services.setTokenStore(tokenStore);
//令牌默认有效期2小时
services.setAccessTokenValiditySeconds(7200);
//刷新令牌默认有效期3天
services.setRefreshTokenValiditySeconds(259200);
return services;
}
//授权码服务 使用数据库
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
//使用数据库
return new JdbcAuthorizationCodeServices(dataSource);
}
//在security配置文件WebSecurityConfig 中加入bean容器
@Autowired
private AuthenticationManager authenticationManager;
/**
* 配置令牌的访问端点(申请令牌的地址)和令牌服务(令牌怎么发放)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//认证管理器,密码模式需要
.authorizationCodeServices(authorizationCodeServices())//授权码服务
.tokenServices(tokenService())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
用密码模式验证: