Spring Security 与 OAuth2(授权服务器)

xiaoxiao2021-03-01  14

authrization-server(授权服务器)

授权服务配置

配置一个授权服务,需要考虑 授权类型(GrantType)、不同授权类型为客户端(Client)提供了不同的获取令牌(Token)方式,每一个客户端(Client)都能够通过明确的配置以及权限来实现不同的授权访问机制,也就是说如果你提供了一个 “client_credentials” 授权方式,并不意味着其它客户端就要采用这种方式来授权

使用 @EnableAuthorizationServer 来配置授权服务机制,并继承 AuthorizationServerConfigurerAdapter 该类重写 configure 方法定义授权服务器策略

配置客户端详情(Client Details)

ClientDetailsServiceConfigurer 能够使用内存或 JDBC 方式实现获取已注册的客户端详情,有几个重要的属性: clientId:客户端标识 ID(账号)secret:客户端安全码(密码)scope:客户端访问范围,默认为空则拥有全部范围authorizedGrantTypes:客户端使用的授权类型,默认为空authorities:客户端可使用的权限

管理令牌(Managing Token)

       读和写令牌所用的tokenService不同

ResourceServerTokenServices 接口定义了令牌加载、读取方法AuthorizationServerTokenServices 接口定义了令牌的创建、获取、刷新方法ConsumerTokenServices 定义了令牌的撤销方法(删除)DefaultTokenServices 实现了上述三个接口,它包含了一些令牌业务的实现,如创建令牌、读取令牌、刷新令牌、获取客户端ID。默认的当尝试创建一个令牌时,是使用 UUID 随机值进行填充的,除了持久化令牌是委托一个 TokenStore 接口实现以外,这个类几乎帮你做了所有事情而 TokenStore 接口也有一些实现: InMemoryTokenStore:默认采用该实现,将令牌信息保存在内存中,易于调试JdbcTokenStore:令牌会被保存近关系型数据库,可以在不同服务器之间共享令牌JwtTokenStore:使用 JWT 方式保存令牌,它不需要进行存储,但是它撤销一个已经授权令牌会非常困难,所以通常用来处理一个生命周期较短的令牌以及撤销刷新令牌

JWT 令牌(JWT Tokens)

使用 JWT 令牌需要在授权服务中使用 JWTTokenStore,资源服务器也需要一个解码 Token 令牌的类 JwtAccessTokenConverter,JwtTokenStore 依赖这个类进行编码以及解码,因此授权服务以及资源服务都需要配置这个转换类Token 令牌默认是有签名的,并且资源服务器中需要验证这个签名,因此需要一个对称的 Key 值,用来参与签名计算这个 Key 值存在于授权服务和资源服务之中,或者使用非对称加密算法加密 Token 进行签名,Public Key 公布在 /oauth/token_key 这个 URL 中默认 /oauth/token_key 的访问安全规则是 "denyAll()" 即关闭的,可以注入一个标准的 SpingEL 表达式到 AuthorizationServerSecurityConfigurer 配置类中将它开启,例如 permitAll()需要引入 spring-security-jwt 库

配置授权类型(Grant Types)

授权是使用 AuthorizationEndpoint 这个端点来进行控制的,使用 AuthorizationServerEndpointsConfigurer 这个对象实例来进行配置,默认是支持除了密码授权外所有标准授权类型,它可配置以下属性: authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象userDetailsService:可定义自己的 UserDetailsService 接口实现authorizationCodeServices:用来设置收取码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 "authorization_code" 授权码类型模式implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态tokenGranter:完全自定义授权服务实现(TokenGranter 接口实现),只有当标准的四种授权模式已无法满足需求时

配置授权端点 URL(Endpoint URLs)

AuthorizationServerEndpointsConfigurer 配置对象有一个 pathMapping() 方法用来配置端点的 URL,它有两个参数: 参数一:端点 URL 默认链接参数二:替代的 URL 链接下面是一些默认的端点 URL: /oauth/authorize:授权端点/oauth/token:令牌端点/oauth/confirm_access:用户确认授权提交端点/oauth/error:授权服务错误信息端点/oauth/check_token:用于资源服务访问的令牌解析端点/oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话授权端点的 URL 应该被 Spring Security 保护起来只供授权用户访问(加入spring security对这些端点进行身份验证)

引入依赖

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>

基于内存存储令牌

配置授权服务类,创建一个类继承 AuthorizationServerConfigurerAdapter 并添加 @EnableAuthorizationServer 注解,添加客户端信息

@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //添加客户端信息 clients.inMemory() // 使用in-memory存储客户端信息 .withClient("client") // client_id .secret("secret") // client_secret .authorizedGrantTypes("authorization_code") // 该client允许的授权类型 .scopes("app"); // 允许的授权范围 } }

修改配置文件,设置 Security 密码为 password,用户名为 root,相当于一个资源拥有者(用户)的账号密码

security: user: name: root password: 1234 server: port: 8081

测试

通过浏览器模拟客户端访问授权端点 /oauth/authorize

#(该步骤为**授权码模式中的A**),需要附上客户端申请认证的参数(**A步骤中所包含的参数**) localhost:8081/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com

进入用户登陆页面(该步骤为授权码模式中的B),第三方验证页面,一般为表单验证

 

输入 root 1234 登陆后会进入下面页面,询问用户是否授权客户端(该步骤为授权码模式中的C),confirm_access,一般可以跳过此验证

勾选授权后点击按钮会跳转到百度

#(**A步骤中包含的参数定义了重定向URL**),并在 URL 中包含一个授权码 https://www.baidu.com/?code=mhlA24

客户端拿到授权码后,附上先前设置的重定向 URL 向服务器申请令牌

# (该步骤为**授权码模式中的D**),通过令牌端点 /oauth/token # 使用 CURL 工具发送 POST 命令,授权码模式不需要 client_sercet,因此该值可以为任意值 curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=Li4NZo&redirect_uri=http://www.baidu.com' "http://client:secret@localhost:8081/oauth/token"

返回令牌如下

{"access_token":"d0e2f362-3bfd-43bb-a6ca-b6cb1b8ea9ee","token_type":"bearer","expires_in":43199,"scope":"app"}

基于JDBC存储令牌

Spring Cloud Security OAuth 已经为我们设计好了一套 Schema 和对应的 DAO 对象Spring Cloud Security OAuth2 通过 DefaultTokenServices 类来完成 token 生成、过期等 OAuth2 标准规定的业务逻辑,而 DefaultTokenServices 又是通过 TokenStore 接口完成对生成数据的持久化在上面的 Demo 中,TokenStore 的默认实现为 InMemoryTokenStore 即内存存储,对于 Client 信息,ClientDetailsService 接口负责从存储仓库中读取数据,在上面的 Demo 中默认使用的也是 InMemoryClientDetailsService 实现类要想使用数据库存储,只要提供这些接口的实现类即可,而框架已经为我们写好 JdbcTokenStore 和 JdbcClientDetailsService

建表

框架已提前为我们设计好了数据库表,但对于 MYSQL 来说,默认建表语句中主键为 Varchar(256),这超过了最大的主键长度,可改成 128,并用 BLOB 替换语句中的 LONGVARBINARY 类型 https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

编写 @Configuration 类继承 AuthorizationServerConfigurerAdapter

@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; @Autowired private TokenStore tokenStore; private ClientDetailsService clientDetailsService; @Bean // 声明TokenStore实现 public TokenStore tokenStore() { return new JdbcTokenStore(dataSource); } @Bean // 声明 ClientDetails实现 public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } @Override // 配置框架应用上述实现 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager); endpoints.tokenStore(tokenStore); // 配置TokenServices参数 DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(endpoints.getTokenStore()); tokenServices.setSupportRefreshToken(false); tokenServices.setClientDetailsService(endpoints.getClientDetailsService()); tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer()); tokenServices.setAccessTokenValiditySeconds( (int) TimeUnit.DAYS.toSeconds(30)); // 30天 endpoints.tokenServices(tokenServices); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); } }

修改配置文件,并引入 MYSQL 和 JDBC 依赖库

spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/client?useUnicode=yes&characterEncoding=UTF-8 username: root password: 123456ly <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>

往数据库 oauth_client_details 表添加客户端信息

 

基于JWT存储令牌

对称加密,对称加密表示认证服务端和客户端的共用一个密钥

@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore tokenStore; //告诉Spring Security Token的生成方式 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore) .accessTokenConverter(jwtAccessTokenConverter()) .authenticationManager(authenticationManager); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } //使用同一个密钥来编码 JWT 中的 OAuth2 令牌 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("123"); return converter; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // 使用in-memory存储客户端信息 .withClient("client") // client_id .secret("secret") // client_secret .authorizedGrantTypes("authorization_code") // 该client允许的授权类型 .scopes("app") // 允许的授权范围 .autoApprove(true); //登录后绕过批准询问(/oauth/confirm_access) } }

使用不对称的密钥来签署令牌

生成 JKS Java KeyStore 文件

keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass

导出公钥

keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

将公钥保存为 pubkey.txt,将 mytest.jks()授权服务器) 和 pubkey.txt(资源服务器) 放到 resource 目录下

-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhAF1qpL+8On3rF2M77lR +l3WXKpGXIc2SwIXHwQvml/4SG7fJcupYVOkiaXj4f8g1e7qQCU4VJGvC/gGJ7sW fn+L+QKVaRhs9HuLsTzHcTVl2h5BeawzZoOi+bzQncLclhoMYXQJJ5fULnadRbKN HO7WyvrvYCANhCmdDKsDMDKxHTV9ViCIDpbyvdtjgT1fYLu66xZhubSHPowXXO15 LGDkROF0onqc8j4V29qy5iSnx8I9UIMEgrRpd6raJftlAeLXFa7BYlE2hf7cL+oG hY+q4S8CjHRuiDfebKFC1FJA3v3G9p9K4slrHlovxoVfe6QdduD8repoH07jWULu qQIDAQAB -----END PUBLIC KEY-----

验证服务器配置

@Configuration @EnableAuthorizationServer public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private TokenStore tokenStore; //告诉Spring Security Token的生成方式 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore) .accessTokenConverter(jwtAccessTokenConverter()) .authenticationManager(authenticationManager); } @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer //允许所有资源服务器访问公钥端点(/oauth/token_key) //只允许验证用户访问令牌解析端点(/oauth/check_token) .tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()") // 允许客户端发送表单来进行权限认证来获取令牌 .allowFormAuthenticationForClients(); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean //使用私钥编码 JWT 中的 OAuth2 令牌 public JwtAccessTokenConverter jwtAccessTokenConverter() { final JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray()); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest")); return converter; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // 测试用,将客户端信息存储在内存中 .withClient("client") // client_id .secret("secret") // client_secret .authorizedGrantTypes("authorization_code") // 该client允许的授权类型 .scopes("app") // 允许的授权范围 .autoApprove(true); //登录后绕过批准询问(/oauth/confirm_access) } }

自定义令牌声明,添加额外的属性

添加一个额外的字段 "组织" 到令牌中

public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance( OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> additionalInfo = new HashMap<>(); additionalInfo.put("organization", authentication.getName() + randomAlphabetic(4)); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; } }

将把它连接到我们的授权服务器配置

@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers( Arrays.asList(tokenEnhancer(), accessTokenConverter())); endpoints.tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager); } @Bean public TokenEnhancer tokenEnhancer() { return new CustomTokenEnhancer(); }

此时令牌如下

{ "user_name": "john", "scope": [ "foo", "read", "write" ], "organization": "johnIiCh", "exp": 1458126622, "authorities": [ "ROLE_USER" ], "jti": "e0ad1ef3-a8a5-4eef-998d-00b26bc2c53f", "client_id": "fooClientIdPassword" }

测试

启动授权服务器、启动资源服务器

访问授权服务器 /oauth/authorize 端点获取授权码 code=vT4fY0

localhost:8081/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com

访问授权服务器 /oauth/token 端点获取访问令牌

WX20180112-111436@2x.png

访问资源服务器受保护的资源,附上令牌在请求头,**需加上 Bearer **

 

 

 

  作者:林塬 链接:https://www.jianshu.com/p/227f7e7503cb 來源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

转载请注明原文地址: https://www.6miu.com/read-3850272.html

最新回复(0)