Spring Authorization Server的使用
一、背景
在 Spring Security 5
中,现在已经不提供了 授权服务器
的配置,但是 授权服务器 在我们平时的开发过程中用的还是比较多的。不过 Spring 官方提供了一个 由Spring官方主导,社区驱动的授权服务 spring-authorization-server
,目前已经到了 0.1.2 的版本,不过该项目还是一个实验性的项目,不可在生产环境中使用,此处来使用项目搭建一个简单的授权服务器。
二、前置知识
1、了解 oauth2 协议、流程。可以参考阮一峰的这篇文章
2、JWT、JWS、JWK的概念
JWT:
指的是 JSON Web Token,由 header.payload.signture 组成。不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的。JWS:
指的是签过名的JWT,即拥有签名的JWT。JWK:
既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。
三、需求
1、 完成授权码(authorization-code
)流程。
最安全的流程,需要用户的参与。
2、 完成客户端(client credentials
)流程。
没有用户的参与,一般可以用于内部系统之间的访问,或者系统间不需要用户的参与。
3、简化模式在新的 spring-authorization-server 项目中已经被弃用了。
4、刷新令牌。
5、撤销令牌。
6、查看颁发的某个token信息。
7、查看JWK信息。
8、个性化JWT token,即给JWT token中增加额外信息。
完成案例:张三
通过QQ登录
的方式来登录优快云
网站。
登录后,优快云就可以获取到QQ颁发的token
,优快云网站拿着token就可以获取张三在QQ资源服务器上的 个人信息
了。
角色分析张三:
用户即资源拥有者优快云:
客户端QQ:
授权服务器个人信息:
即用户的资源,保存在资源服务器中
四、核心代码编写
1、引入授权服务器依赖
<dependency> | |
<groupId>org.springframework.security.experimental</groupId> | |
<artifactId>spring-security-oauth2-authorization-server</artifactId> | |
<version>0.1.2</version> | |
</dependency> |
2、创建授权服务器用户
张三
通过QQ登录
的方式来登录优快云
网站。
此处完成用户张三
的创建,这个张三是授权服务器的用户,此处即QQ服务器的用户。
@EnableWebSecurity | |
public class DefaultSecurityConfig { | |
@Bean | |
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { | |
http | |
.authorizeRequests(authorizeRequests -> | |
authorizeRequests.anyRequest().authenticated() | |
) | |
.formLogin(); | |
return http.build(); | |
} | |
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return new BCryptPasswordEncoder(); | |
} | |
// 此处创建用户,张三。 | |
@Bean | |
UserDetailsService users() { | |
UserDetails user = User.builder() | |
.username("zhangsan") | |
.password(passwordEncoder().encode("zhangsan123")) | |
.roles("USER") | |
.build(); | |
return new InMemoryUserDetailsManager(user); | |
} | |
} |
3、创建授权服务器和客户端
张三
通过QQ登录
的方式来登录优快云
网站。
此处完成QQ授权服务器和客户端优快云的创建。
package com.huan.study.authorization.config; | |
import com.nimbusds.jose.jwk.JWKSet; | |
import com.nimbusds.jose.jwk.RSAKey; | |
import com.nimbusds.jose.jwk.source.JWKSource; | |
import com.nimbusds.jose.proc.SecurityContext; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.core.Ordered; | |
import org.springframework.core.annotation.Order; | |
import org.springframework.jdbc.core.JdbcTemplate; | |
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; | |
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; | |
import org.springframework.security.crypto.password.PasswordEncoder; | |
import org.springframework.security.oauth2.core.AuthorizationGrantType; | |
import org.springframework.security.oauth2.core.ClientAuthenticationMethod; | |
import org.springframework.security.oauth2.jwt.JwtDecoder; | |
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; | |
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; | |
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; | |
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; | |
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; | |
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; | |
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; | |
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; | |
import org.springframework.security.web.SecurityFilterChain; | |
import org.springframework.security.web.util.matcher.RequestMatcher; | |
import java.security.KeyPair; | |
import java.security.KeyPairGenerator; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.interfaces.RSAPrivateKey; | |
import java.security.interfaces.RSAPublicKey; | |
import java.time.Duration; | |
import java.util.UUID; | |
/** | |
* 认证服务器配置 | |
* | |
* @author huan.fu 2021/7/12 - 下午2:08 | |
*/ | |
@Configuration | |
public class AuthorizationConfig { | |
@Autowired | |
private PasswordEncoder passwordEncoder; | |
/** | |
* 个性化 JWT token | |
*/ | |
class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> { | |
@Override | |
public void customize(JwtEncodingContext context) { | |
// 添加一个自定义头 | |
context.getHeaders().header("client-id", context.getRegisteredClient().getClientId()); | |
} | |
} | |
/** | |
* 定义 Spring Security 的拦截器链 | |
*/ | |
@Bean | |
@Order(Ordered.HIGHEST_PRECEDENCE) | |
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { | |
// 设置jwt token个性化 | |
http.setSharedObject(OAuth2TokenCustomizer.class, new CustomOAuth2TokenCustomizer()); | |
// 授权服务器配置 | |
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = | |
new OAuth2AuthorizationServerConfigurer<>(); | |
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); | |
return http | |
.requestMatcher(endpointsMatcher) | |
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) | |
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) | |
.apply(authorizationServerConfigurer) | |
.and() | |
.formLogin() | |
.and() | |
.build(); | |
} | |
/** | |
* 创建客户端信息,可以保存在内存和数据库,此处保存在数据库中 | |
*/ | |
@Bean | |
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { | |
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) | |
// 客户端id 需要唯一 | |
.clientId("csdn") | |
// 客户端密码 | |
.clientSecret(passwordEncoder.encode("csdn123")) | |
// 可以基于 basic 的方式和授权服务器进行认证 | |
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC) | |
// 授权码 | |
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) | |
// 刷新token | |
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) | |
// 客户端模式 | |
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) | |
// 密码模式 | |
.authorizationGrantType(AuthorizationGrantType.PASSWORD) | |
// 简化模式,已过时,不推荐 | |
.authorizationGrantType(AuthorizationGrantType.IMPLICIT) | |
// 重定向url | |
.redirectUri("https://www.baidu.com") | |
// 客户端申请的作用域,也可以理解这个客户端申请访问用户的哪些信息,比如:获取用户信息,获取用户照片等 | |
.scope("user.userInfo") | |
.scope("user.photos") | |
.clientSettings(clientSettings -> { | |
// 是否需要用户确认一下客户端需要获取用户的哪些权限 | |
// 比如:客户端需要获取用户的 用户信息、用户照片 但是此处用户可以控制只给客户端授权获取 用户信息。 | |
clientSettings.requireUserConsent(true); | |
}) | |
.tokenSettings(tokenSettings -> { | |
// accessToken 的有效期 | |
tokenSettings.accessTokenTimeToLive(Duration.ofHours(1)); | |
// refreshToken 的有效期 | |
tokenSettings.refreshTokenTimeToLive(Duration.ofDays(3)); | |
// 是否可重用刷新令牌 | |
tokenSettings.reuseRefreshTokens(true); | |
}) | |
.build(); | |
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); | |
if (null == jdbcRegisteredClientRepository.findByClientId("csdn")) { | |
jdbcRegisteredClientRepository.save(registeredClient); | |
} | |
return jdbcRegisteredClientRepository; | |
} | |
/** | |
* 保存授权信息,授权服务器给我们颁发来token,那我们肯定需要保存吧,由这个服务来保存 | |
*/ | |
@Bean | |
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { | |
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); | |
class CustomOAuth2AuthorizationRowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper { | |
public CustomOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) { | |
super(registeredClientRepository); | |
getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); | |
this.setLobHandler(new DefaultLobHandler()); | |
} | |
} | |
CustomOAuth2AuthorizationRowMapper oAuth2AuthorizationRowMapper = | |
new CustomOAuth2AuthorizationRowMapper(registeredClientRepository); | |
authorizationService.setAuthorizationRowMapper(oAuth2AuthorizationRowMapper); | |
return authorizationService; | |
} | |
/** | |
* 如果是授权码的流程,可能客户端申请了多个权限,比如:获取用户信息,修改用户信息,此Service处理的是用户给这个客户端哪些权限,比如只给获取用户信息的权限 | |
*/ | |
@Bean | |
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { | |
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); | |
} | |
/** | |
* 对JWT进行签名的 加解密密钥 | |
*/ | |
@Bean | |
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException { | |
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); | |
keyPairGenerator.initialize(2048); | |
KeyPair keyPair = keyPairGenerator.generateKeyPair(); | |
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); | |
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); | |
RSAKey rsaKey = new RSAKey.Builder(publicKey) | |
.privateKey(privateKey) | |
.keyID(UUID.randomUUID().toString()) | |
.build(); | |
JWKSet jwkSet = new JWKSet(rsaKey); | |
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); | |
} | |
/** | |
* jwt 解码 | |
*/ | |
@Bean | |
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { | |
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); | |
} | |
/** | |
* 配置一些断点的路径,比如:获取token、授权端点 等 | |
*/ | |
@Bean | |
public ProviderSettings providerSettings() { | |
return new ProviderSettings() | |
// 配置获取token的端点路径 | |
.tokenEndpoint("/oauth2/token") | |
// 发布者的url地址,一般是本系统访问的根路径 | |
// 此处的 qq.com 需要修改我们系统的 host 文件 | |
.issuer("http://qq.com:8080"); | |
} | |
} | |
注意⚠️:
1、需要将 qq.com 在系统的 host 文件中与 127.0.0.1 映射起来。
2、因为客户端信息、授权信息(token信息等)保存到数据库,因此需要将表建好。
3、详细信息看上方代码的注释
五、测试
从上方的代码中可知:
资源所有者:
张三 用户名和密码为:zhangsan/zhangsan123客户端信息:
优快云 clientId和clientSecret:csdn/csdn123授权服务器地址:
qq.comclientSecret
的值不可泄漏给客户端,必须保存在服务器端。
1、授权码流程
1、获取授权码
http://qq.com:8080/oauth2/authorize?client_id=csdn&response_type=code&redirect_uri=https://www.baidu.com&scope=user.userInfo user.userInfo
client_id=csdn:表示客户端是谁
response_type=code:表示返回授权码
scope=user.userInfo user.userInfo:获取多个权限以空格分开
redirect_uri=https://www.baidu.com:跳转请求,用户同意或拒绝后
2、根据授权码获取token
curl -i -X POST \ | |
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ | |
'http://qq.com:8080/oauth2/token?grant_type=authorization_code&code=tDrZ-LcQDG0julJBcGY5mjtXpE04mpmXjWr9vr0-rQFP7UuNFIP6kFArcYwYo4U-iZXFiDcK4p0wihS_iUv4CBnlYRt79QDoBBXMmQBBBm9jCblEJFHZS-WalCoob6aQ&redirect_uri=https%3A%2F%2Fwww.baidu.com' |
Authorization: 携带具体的 clientId 和 clientSecret 的base64的值
grant_type=authorization_code 表示采用的方式是授权码
code=xxx:上一步获取到的授权码
3、流程演示
2、根据刷新令牌获取token
curl -i -X POST \ | |
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ | |
'http://qq.com:8080/oauth2/token?grant_type=refresh_token&refresh_token=Wpu3ruj8FhI-T1pFmnRKfadOrhsHiH1JLkVg2CCFFYd7bYPN-jICwNtPgZIXi3jcWqR6FOOBYWo56W44B5vm374nvM8FcMzTZaywu-pz3EcHvFdFmLJrqAixtTQZvMzx' |
3、客户端模式
此模式下,没有用户的参与,只有客户端和授权服务器之间的参与。
curl -i -X POST \ | |
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ | |
'http://qq.com:8080/oauth2/token?grant_type=client_credentials' |
4、撤销令牌
curl -i -X POST \ | |
'http://qq.com:8080/oauth2/revoke?token=令牌' |
5、查看token 的信息
curl -i -X POST \ | |
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \ | |
'http://qq.com:8080/oauth2/introspect?token=XXX' |
6、查看JWK信息
curl -i -X GET \ | |
'http://qq.com:8080/oauth2/jwks' |
六、完整代码
spring-cloud-parent: spring cloud 技术栈学习 - Gitee.com
七、参考地址
1、GitHub - spring-projects/spring-authorization-server: Spring Authorization Server
本文来自博客园,作者:huan1993,转载请注明原文链接:https://www.cnblogs.com/huan1993/p/15416077.html