代码

spring-authorization-server 是 Spring 新退出的认证服务器。

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

2. 快速入门

详细代码在这里

2.1 生成代码

① 使用向导

通过https://start.spring.io/,来生成空工程。 后来将 springboot 升级到 2.7.0

② 上传 gitee

并将代码上传到:https://gitee.com/xiaoyu-learning/spring-sas-server

③ 添加阿里链接

为了加快 gradle 速度,可以添加阿里镜像

repositories {
maven {
url 'https://maven.aliyun.com/repository/public/'
}
maven {
url 'https://maven.aliyun.com/repository/spring/'
}
maven {
url 'https://maven.aliyun.com/repository/spring-plugin'
}
maven {
url 'https://maven.aliyun.com/repository/google'
}
mavenLocal()
mavenCentral()
}

④ 添加 gradle

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.3.0'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'net.sourceforge.htmlunit:htmlunit'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:3.9.0'

2.2 配置 security

今后可以用自己的UserDetailsService来进行

@EnableWebSecurity
public class DefaultSecurityConfig {
@Bean
UserDetailsService users(){
UserDetails user= User.withDefaultPasswordEncoder()
.username("user1")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(
authorizeRequests-> authorizeRequests.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}

2.3 配置 Authz Server

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
//return http.formLogin(Customizer.withDefaults()).build();
http
.exceptionHandling(exceptions ->
exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
);
return http.build();
}
/**
* 返回注册的客户端存储库
* Spring 提供基于JDB或者是基于内存的
* @param jdbcTemplate
* @return
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
// Save registered client in db as if in-memory
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
/**
* 返回OAuth2的认证服务
* @param jdbcTemplate
* @param registeredClientRepository
* @return
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 得到OAuth2确认服务
* @param jdbcTemplate
* @param registeredClientRepository
* @return
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
/**
* spring: 使用嵌入式数据源 EmbeddedDatabaseBuilder
* 嵌入式数据源作为应用的一部分运行,非常适合在开发和测试环境中使用,但是不适合用于生产环境。
* 因为在使用嵌入式数据源的情况下,你可以在每次应用启动或者每次运行单元测试之前初始化测试数据。
* @return
*/
@Bean
public EmbeddedDatabase embeddedDatabase() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.setScriptEncoding("UTF-8")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
.build();
}
}

这里有一个类JWKSource,其中使用了Jwks这个类,这个类在jose包中,具体不负责。

2.4 配置端口

server:
port: 9000

2.5 手工测试

① 进行登录-成功

测试输入正确的密码

② 进行登录-失败

测试输入错误密码

③ 得到 code

登录完毕后,在地址栏中输入/oauth2/authorize得到响应的 code

测试输入正确的密码

http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=DeEOyLOKLiUPykkoPuYsmEJ2GOcOc_4NMgF7uzFqofdZmXi4OyvWgARjwE13hVFsercSHyf-drgSKfQ5szfblN3BMAs9xK1kr66NB_XVt6MC4dTGLjfkXVd9IZ1AvqVz&state=state

④⑤⑥

④ 使用 code 换 token

curl --location --request POST 'http://localhost:9000/oauth2/token' \
--header 'Authorization: Basic cGlnOnBpZw==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=DeEOyLOKLiUPykkoPuYsmEJ2GOcOc_4NMgF7uzFqofdZmXi4OyvWgARjwE13hVFsercSHyf-drgSKfQ5szfblN3BMAs9xK1kr66NB_XVt6MC4dTGLjfkXVd9IZ1AvqVz' \
--data-urlencode 'redirect_uri=http://127.0.0.1:8080'

如果

curl --location --request POST \
--url 'http://localhost:9000/oauth2/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id=messaging-client \
--data client_secret=secret \
--data-urlencode 'code=DeEOyLOKLiUPykkoPuYsmEJ2GOcOc_4NMgF7uzFqofdZmXi4OyvWgARjwE13hVFsercSHyf-drgSKfQ5szfblN3BMAs9xK1kr66NB_XVt6MC4dTGLjfkXVd9IZ1AvqVz' \
--data-urlencode 'redirect_uri=http://127.0.0.1:8080'
http://auth-server:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&state=ntYfCGNHykm1-474ZqNTZM6i1CyyiYtsylwfU8aj6b8=&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=9VMhDHwj_J5Q_aEhWH8em4IHoArXgJTVsnYHnp_4seo

qqq

http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=v7rj6pvj6RL6in7zgl2a4hZ2iqH-K9KCwhEFn9JaX0QRRCcQ-msZcNdPkVudg2quEmx0GE6iv9p_eICNzqIRY1Nayf3ZiA39N-QXBOqz8GhLYK4B8OfMsA_gmkiSFWC3&state=ntYfCGNHykm1-474ZqNTZM6i1CyyiYtsylwfU8aj6b8%3D

又跳转

http://auth-server:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&state=rF12Cw5mVVkguewo-_uw8bVuupAjN04t5WFS-0rS4Ho%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=IkVaCmBxoGgfx89j8_GeyXtk5taDlbBeWEgqQRbYL94

111

http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=pjuEVvYOCj1q9Tn9EjZVynsLjbMr65PFaS1KS_WKiR12DEFFv3l8Z5A-PhdVfzSmFxaoQUnzuXpBU1fCQ2VHgrX41Cq-UrgVqab4Z9V2ZJxl24ED-SNZXYwot-yOmZy_&state=rF12Cw5mVVkguewo-_uw8bVuupAjN04t5WFS-0rS4Ho%3D
org.springframework.security.oauth2.server.authorization.authentication
OAuth2AuthorizationCodeAuthenticationProvider
if (StringUtils.hasText(authorizationRequest.getRedirectUri()) &&
!authorizationRequest.getRedirectUri().equals(authorizationCodeAuthentication.getRedirectUri())) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
https%3A%2F%2Foidcdebugger.com%2Fdebug
https://oidcdebugger.com/debug
https://oidcdebugger.com/debug
使用URLEncoder进行转码
@EnableOAuth2Sso
{
"access_token": "eyJraWQiOiJjODI4NjhkZS1jZGRlLTQ1YmQtODNhOS1lNzIwN2FhODQxZDYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsImF1ZCI6Im1lc3NhZ2luZy1jbGllbnQiLCJuYmYiOjE2MzczNzU4NDAsInNjb3BlIjpbIm9wZW5pZCJdLCJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTAwMCIsImV4cCI6MTYzNzM3NjE0MCwiaWF0IjoxNjM3Mzc1ODQwfQ.ZIhRrc3g4D4H8-R9z7dro0i_SSbaao1zmUTPnyRq7S2n2bJ-4dnyKfP_XMx3oZVIyCrpRx6KMlSzUGdP6EL21MjmAi1D7kHTidTiv7Y0S-loS7l1djqgCJ4G8xWfw63FVl7_acqLYgtmYmt43WbXGWeG4JJqgbK0VvM3eyljDNTW9xDUBzzKdBjjD-1Bjn1hPC61xmb41rpdrblWrLqiRNX342bvKZq9NbeNovuV33ZaS8ChiD-UiF5BrMYDpVez4qlT5SvNudRWOIAN7TwvV__XtB9bDHQr5FcMfY7ZDPjAQNrHvyHIxMR-xypX4Ru2MrATEgmyN-RfYSzHIwpyVQ",
"refresh_token": "H_CSpUZt4c_Dl-97RXa1REyRky14zN_n1eW531zptnV9LvBBBNaph7sqTxJI_TTtNtMKHjc7Hhg0NoLyK2jgv7qiSewy0QdADlFKWc3lyANUptsp8-ckXUOOstQcLSZZ",
"scope": "openid",
"id_token": "eyJraWQiOiJjODI4NjhkZS1jZGRlLTQ1YmQtODNhOS1lNzIwN2FhODQxZDYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsImF1ZCI6Im1lc3NhZ2luZy1jbGllbnQiLCJhenAiOiJtZXNzYWdpbmctY2xpZW50IiwiaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjkwMDAiLCJleHAiOjE2MzczNzc2NDAsImlhdCI6MTYzNzM3NTg0MCwibm9uY2UiOiI1bTkwaW1jZjN6cSJ9.Kfto7TgM7odTZfVJK_wSoLCzVMne-maY4TCgNg2HYEBDdIRMsfRL2Q6Gx_1lZq7uoBCAik1DFqDaQsudfZxuAso1gL3oMuE793Vvm0b2-Zmbk1bWCr5oQF9k4aU1tunnO2Ah2zmTvhOxbTP3i8F-LjVf_85c3HcCjsH7a9X-Yd9WRk185tSIDQihnw_45sXKCYY4jAtf7h73YS73icgmiIUGpR1JuKFCwJs_ZBRcOK1cyjp6kTnwOpoQ7uKABIOrzqmU0ghR1k2ZIg4OYdzIvr5XA83u6wOpx7sxNbkJCdHgNMSzMBsBLq1V_djFqOR8DbEwuAbMSG3Dfh8kEooMFQ",
"token_type": "Bearer",
"expires_in": 300
}