共计 10043 个字符,预计需要花费 26 分钟才能阅读完成。
一、背景
在 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 信息。
实现案例:张三
通过 QQ 登录
的形式来登录 CSDN
网站。
登录后,CSDN 就能够获取到 QQ 颁发的 token
,CSDN 网站拿着 token 就能够获取张三在 QQ 资源服务器上的 个人信息
了。
角色剖析 张三:
用户即资源拥有者 CSDN:
客户端 QQ:
受权服务器 个人信息:
即用户的资源,保留在资源服务器中
四、外围代码编写
1、引入受权服务器依赖
<dependency>
<groupId>org.springframework.security.experimental</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.1.2</version>
</dependency>
2、创立受权服务器用户
张三
通过 QQ 登录
的形式来登录 CSDN
网站。
此处实现用户 张三
的创立,这个张三是受权服务器的用户,此处即 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 登录
的形式来登录 CSDN
网站。
此处实现 QQ 受权服务器和客户端 CSDN 的创立。
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;
/**
* 定义 Spring Security 的拦截器链
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 受权服务器配置
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("messaging-client")) {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客户端信息:
CSDN clientId 和 clientSecret:csdn/csdn123受权服务器地址:
qq.comclientSecret
的值不可透露给客户端,必须保留在服务器端。
1、受权码流程
1、获取受权码
http://qq.com:8080/oauth2/aut… 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'
六、残缺代码
https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server
七、参考地址
1、https://github.com/spring-projects-experimental/spring-authorization-server