Authorization Server

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

下面的例子取自Authorization Server Sample中,与 OAuth2 中的有重复,但是代码有不同

1. default-authorization-server

1.1 开发

这是一个最简单的 authorization-server 程序。

有几个可以参考的代码

  • 非对称加密工具类。实际项目中,建议用 OpenSSL 生成私钥来作为初始化工具。
  • 使用了嵌入数据库,作为持久化。实际项目中,建议用 Mysql

下面是具体的配置内容

@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.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;
}
@Bean
UserDetailsService users() {
UserDetails user = User.withUsername("user1")
.password("{noop}password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}

1.2 测试

单元测试 1

测试功能的描述

  • 登陆成功后,显示 404 页面:whenLoginSuccessfulThenDisplayNotFoundError
  • 输入错误密码,登陆失败后,显示登陆页面,页面中提示Bad credentialswhenLoginFailsThenDisplayBadCredentials
  • 没有登陆,直接访问/oauth2/authorize,显示登陆页面:whenNotLoggedInAndRequestingTokenThenRedirectToLogin
  • 登陆成功后,得到code,并且得到token:whenLoggingInAndRequestingTokenThenRedirectToClientApplication

代码技巧:

  • 使用@BeforeAll,要添加@TestInstance(TestInstance.Lifecycle.PER_CLASS)

  • 使用UriComponentsBuilder用来拼写或者获取标准的 URL 信息。

  • 使用@LocalServerPort 获得当前测试环境的端口号

  • webClient进行 Post 的例子

测试确认功能

  • 选择openid message.read message.write,点击确定后成功。whenUserConsentsToAllScopesThenReturnAuthorizationCode
  • 在确认页面,点击取消按钮,显示取消的页面。

功能描述:

  • mockOAuth2AuthorizationConsentService 返回空,是为了避免以前已经设置过了。
  • 其他没有啥注意事项了。

1.3 疑问

  • 两个配置类都使用了http.formLogin(withDefaults()); 是否是多余的内? 可以注释掉一个吗?

不行

2. message-resource

2.1 开发

配置 application.yml

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9000

安全配置

@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.authorizeRequests(request->request
.antMatchers(HttpMethod.GET,"/message/**").hasAuthority("SCOPE_message.read")
.anyRequest().authenticated()
);
http.oauth2ResourceServer().jwt();
return http.build();
}
}

这里使用了http.oauth2ResourceServer().jwt();

撰写测试的 rest 接口

@RestController
public class MessagesController {
@GetMapping("/messages")
public String[] getMessages(String name) {
if(StringUtils.hasText(name)){
return new String[] {name,"Message 1", "Message 2", "Message 3"};
}
return new String[] {"Message 1", "Message 2", "Message 3"};
}
}

复习以前的例子

下面三种写法,功能都是一样的。

http.oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()));
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
http.oauth2ResourceServer().jwt();

获取等前的登陆的用户

@GetMapping("/")
public String index(@AuthenticationPrincipal Jwt jwt){
return String.format("Hello %s!",jwt.getSubject());
}

3. message-client

3.1 开发

① 配置依赖

// 主依赖
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// html需要的依赖
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
// 要使用webclient的依赖,reactor-netty 是必须使用的
implementation "org.springframework:spring-webflux"
implementation "io.projectreactor.netty:reactor-netty"
// 本地需要的html资源文件
implementation "org.webjars:webjars-locator-core"
implementation "org.webjars:bootstrap:3.4.1"
implementation "org.webjars:jquery:3.4.1"

② 连接 authorization server

配置 yml

有几个重点:

  • 可以配置不同的 client registration
  • issuer-uri 是链接认证服务器的。
spring:
thymeleaf:
cache: false
security:
oauth2:
client:
registration:
messaging-client-oidc:
provider: spring
client-id: messaging-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: 'http://127.0.0.1:8080/login/oauth2/code/{registrationId}'
scope: openid
client-name: messaging-client-oidc
messaging-client-authorization-code:
provider: spring
client-id: messaging-client
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: 'http://127.0.0.1:8080/authorized'
scope: message.read,message.write
client-name: messaging-client-authorization-code
messaging-client-client-credentials:
provider: spring
client-id: messaging-client
client-secret: secret
authorization-grant-type: client_credentials
scope: message.read,message.write
client-name: messaging-client-client-credentials
provider:
spring:
issuer-uri: http://localhost:9000

安全配置

这个配置与以前的不一样,后续会说明。这里先说重点:

  • WebSecurityCustomizer 用来配置那些忽略的 url
  • 使用了 oauth2Login
    • /oauth2/authorization/messaging-client-oidc
@EnableWebSecurity
public class SecurityConfig {
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/webjars/**");
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.oauth2Login(oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))
.oauth2Client(withDefaults());
return http.build();
}
}

如果不指定登陆页面,那么会显示一个简单的登陆页面,进行登陆,下图是一个示例。

③ 编写 controller

配置 webclient

这个 controller 会链接 resource server,所以需要先要配置 webclient,以便获取 token。

  • 要自定包含 oauth2Client.oauth2Configuration
  • 配置 OAuth2AuthorizedClientManager
    • 容器传入:ClientRegistrationRepository 客户端注册仓库与已经登陆的客户端仓库 OAuth2AuthorizedClientRepository
    • new 一个 OAuth2AuthorizedClientProvider
    • new 一个 DefaultOAuth2AuthorizedClientManager,并把 authorizedClientProvider 传入。
@Configuration
public class WebClientConfig {
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}

编写 controller

  • 已经配置好的 webClient
  • ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient方法来获取参数
@GetMapping(value = "/authorize", params = "grant_type=authorization_code")
public String authorizationCodeGrant(Model model
, @RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code")
OAuth2AuthorizedClient authorizedClient
){
String[] messages=this.webClient
.get()
.uri(this.messagesBaseUri+"?name=authorization_code")
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String[].class)
.block();
model.addAttribute("messages", messages);
return "index";
}

这里有一个疑问,会不会每次都调用 authorization server 来获得 token 呢?答案肯定是不会的。有两点可以证明

  • 通过跟踪 authorization server 的 OAuth2TokenEndpointFilter 类
  • 可以跟踪 client 工程中的 InMemoryOAuth2AuthorizedClientService 类,就会发现如果已经检索过了,就不会再检索了。

④ 编写 html

  • 使用了 bootstrap
    • 要了解:org.webjars:webjars-locator-core 如何使用
  • 使用了 thymeleaf

4.1 常见错误

开发的时候,有些拼写错误要注意:

① 明文密码要添加前缀{noop}

withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")

② 获取当前确认信息,要使用 Id,不是 clientId

RegisteredClient registeredClient=this.registeredClientRepository.findByClientId(clientId);
OAuth2AuthorizationConsent currentAuthorizationConsent= this.oAuth2AuthorizationConsentService
.findById(registeredClient.getId(), principal.getName());

③ model 中一些字符拼写错误

model 中一些字符拼写错误与模板中的变量不一致

4.2 测试方法

得到 code

  • 在浏览器的地址栏中输入:

    • http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid
      message.read&state=state&redirect_uri=http://127.0.0.1:8080/authorized
    • 注意事项

      • 只用 client_id,不需要密码
      • redirect_uri 一定要提前配置在系统中,并且与请求 token 时的保持一直
      • state 是客户端生成的随机码,用户保证服务器返回保持一致
  • 如果是第一次登陆,需要认证,请输入系统内置的用户名和密码:user password

  • 如果要确认 Scope,那么会弹出范围确认页面。

使用 Post 认证模式得到 token

curl --location --request POST 'http://localhost:9000/oauth2/token?grant_type=authorization_code&client_id=messaging-client&client_secret=secret&redirect_uri=http://127.0.0.1:8080/authorized&code=Dyr6YdNElqwzSDQqtXQpsZYCP0AUR45FhnFTyC9MGNUlwV2Pu5RW3JE9WJlTLpQkXz0Ae6LHeDOS8GTPMFe7XMePN3p5LtEJcC2tlIPlLP87EW4wrqaE09y4OX0P84kf'

4.3 开发

可以跟另外一个例子做对比。,这里使用了OAuth2AuthorizationServerConfigurer可以做更复杂的配置。

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception{
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer
.authorizationEndpoint(endPoint->endPoint.consentPage(CUSTOM_CONTENT_PAGE_URI));
RequestMatcher endpointsMatchers=authorizationServerConfigurer.getEndpointsMatcher();
http.requestMatcher(endpointsMatchers)
.authorizeRequests(request->request.anyRequest().authenticated())
.csrf(csrf->csrf.ignoringRequestMatchers(endpointsMatchers))
.apply(authorizationServerConfigurer);
return http.formLogin(Customizer.withDefaults()).build();
}

上一个例子的这部分配置比较简单,是个简化版。

@Bean
@Order(1) //参数值越小优先级越高
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception{
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}

OAuth2AuthorizationConsent可以获取用户已经授权的范围

RegisteredClient registeredClient=this.registeredClientRepository.findByClientId(clientId);
OAuth2AuthorizationConsent currentAuthorizationConsent= this.oAuth2AuthorizationConsentService
.findById(registeredClient.getId(), principal.getName());
Set<String> authorizedScopes;
if(currentAuthorizationConsent!=null){
authorizedScopes=currentAuthorizationConsent.getScopes();
}else{
authorizedScopes= Collections.emptySet();
}

5 Federated-Identity-authorization-server

可以参考[security-samples/oauth2/login]中关于 github 的例子。

5.1 开发

5.1.1 配置 yml

server:
port: 9000
spring:
security:
oauth2:
client:
registration:
github-idp:
provider: github
client-id: ${GITHUB_CLIENT_ID:fef49e9c1a0122055c1779}
client-secret: ${GITHUB_CLIENT_SECRET:9f3a9253a880sss871807ba4d21228939a15f2303ce140d}
scope: user:email, read:user
client-name: Sign in with GitHub
provider:
github:
user-name-attribute: login

对比一下与[security-samples/oauth2/login]的不同,上面的例子中,使用了

  • github-idp 来代替了 github,同时多出了一个 provider.github
  • 思考:如何自己做一个 idp 呢?
spring:
security:
oauth2:
client:
registration:
login-client:
provider: spring
client-id: login-client
client-secret: openid-connect
client-authentication-method: client_secret_basic
authorization-grant-type: authorization_code
redirect-uri: http://127.0.0.1:8080/login/oauth2/code/login-client
scope: openid,profile
client-name: Spring
github:
client-id: fef49e9c1a0122055c1779
client-secret: 9f39253a18808222271807ba4d28939a15f2303ce140d
provider:
spring:
authorization-uri: http://localhost:9000/oauth2/authorize
token-uri: http://localhost:9000/oauth2/token
jwk-set-uri: http://localhost:9000/oauth2/jwks

5.1.2 添加基本安全认证

添加基本的 Security,实现对要访问的 URL 进行保护,需要输入用户名与密码。

@EnableWebSecurity
public class DefaultSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.authorizeRequests(request->request
.mvcMatchers("/assets/**", "/webjars/**", "/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
;
return http.build();
}
@Bean
UserDetailsService users() {
UserDetails user = User.withUsername("user1")
.password("{noop}password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

5.1.3 添加 OAuth2 认证

这个章节有类似的例子,可以参考一下,上面的例子是保存在内容中,这个例子中使用了数据库。

① 指定加密算法

需要制作一个@Bean JWKSource<SecurityContext>

@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}

这里Jwks是一个单独的类,在实际的过程中,可以通过 openSSL 来生成密钥来进行处理。

② 指定ProviderSettings
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().issuer("http://localhost:9000").build();
}
③ 配置SecurityFilterChain
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) //参数值越小优先级越高
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.formLogin(Customizer.withDefaults()).build();
}
④ 配置客户端注册仓库

今后读取注册的客户端信息,都从这个仓库中读取。有存在内存的,有保存到数据库中的。

下面是一个存放到内容的例子:

@Bean
public RegisteredClientRepository registeredClientRepository(){
......
return new InMemoryRegisteredClientRepository(loginClient,registeredClient);
}

那如何存放到数据库中呢?

将 jdbcTemplate 传入,并返回一个:JdbcRegisteredClientRepository

@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.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;
}
⑤ 实现数据库保存

这里使用了一个嵌入式数据库,所以第一步要初始化这个数据库

@Bean
public EmbeddedDatabase embeddedDatabase() {
// @formatter:off
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();
// @formatter:on
}

实际上就是定义了一个DataSource

public interface EmbeddedDatabase extends DataSource {
/**
* Shut down this embedded database.
*/
void shutdown();
}
⑥ 实现两个 Service
  • OAuth2AuthorizationService
  • OAuth2AuthorizationConsentService
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}

5.1.4 添加 github 登陆

① 配置 github

在 github 上配置回调地址

http://localhost:9000/login/oauth2/code/github-idp
# 下面是一个老的程序
http://127.0.0.1:8080/login/oauth2/code/github
② DefaultSecurityConfig 中添加联合身份配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
// 添加联合身份配置类
FederatedIdentityConfigurer federatedIdentityConfigurer = new FederatedIdentityConfigurer()
.oauth2UserHandler(new UserRepositoryOAuth2UserHandler());
http.authorizeRequests(request->request
.mvcMatchers("/assets/**", "/webjars/**", "/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.apply(federatedIdentityConfigurer) // 将联合身份配置类加载进去
;
return http.build();
}

添加了两行

③ AuthorizationServerConfig 中添加联合身份配置
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 添加联合身份配置类
http.apply(new FederatedIdentityConfigurer());
return http.formLogin(Customizer.withDefaults()).build();
}
// 添加联合身份Id Token的定制类
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {
return new FederatedIdentityIdTokenCustomizer();
}

添加了:

  • http.apply(new FederatedIdentityConfigurer());
  • @Bean OAuth2TokenCustomizer 用来解析定义的 ID

5.1.5 跳转流程

  • 不用一下子就获取 token,可以在具体访问的时候,再获取 token
  • 从 8080 跳转到 9000 主要是为了获得 code
① 跳转到 900 登陆
  • 访问 8080 首页时,会马上跳转到自己的登陆页面:oauth2/authorization/messaging-client-oidc
  • 发现没有认证的时候,跳转到 9000 的/oauth2/authorize,获取 code 页面。但是发现没有认证,就跳转到 9000 的登陆页
  • 在这个页面中,点击使用 github 登陆:/oauth2/authorization/github-idp
链接说明
http://localhost:8080/手工输入,返回 302,要按照 Location 的内容进行跳转
http://localhost:8080/oauth2/authorization/messaging-client-oidc自动跳转,返回 302,要按照 Location 的内容进行跳转
http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&state=state&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=cc自动跳转,返回 302,要按照 Location 的内容进行跳转
http://localhost:9000/login这是登陆页面,需要手工输入登陆口令
点击登陆 github 链接:http://localhost:9000/oauth2/authorization/github-idp
② 跳转回 8080
  • 接上回,点击 9000 的 github 登陆:/oauth2/authorization/github-idp

  • 跳转到github.com/login/oauth/authorize,来获得 code

  • 把 github 得到 code,返回到 9000 的/oauth2/code/github-idp,为了后来获得 token

  • 本来认为要得到 github 的 token 的,谁知道回调到上会显示登陆前的页面:9000/oauth2/authorize?response_type=code

  • 这次因为登陆成功了,所以获取了 code,并返回到8080/login/oauth2/code/messaging-client-oidc

  • 但是返回了错误给这个请求:8080/oauth2/authorization/messaging-client-oidc

  • 由于 8080 登陆错误,然后就重新访问9000/oauth2/authorize?response_type=code

  • 这次 9000 已经登陆成功了,所以成功将 code 返回给8080/login/oauth2/code/messaging-client-oidc

  • 8080 得到 code 后,就跳转到了首页。

链接说明
http://localhost:9000/oauth2/authorization/github-idp返回 302,要按照 Location 的内容进行跳转
https://github.com/login/oauth/authorize?response_type=code&client_id=779&scope=user:email%20read:user&state=state&redirect_uri=http://localhost:9000/login/oauth2/code/github-idp返回 302,要按照 Location 的内容进行跳转
http://localhost:9000/login/oauth2/code/github-idp?code=f44f00d456feca6d9a5c&state=state返回 302,要按照 Location 的内容进行跳转
http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&state=state&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=cc返回 302,要按照 Location 的内容进行跳转
http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=kIKvJfM1hjYzt5OE6ta43OC7Uc6jNxiOLulz_skmiT7rpR-B0nqgqVZbymsjYS5Wq_N-W-DedJrG4SfBQJ3ywIsFWA0hpZK-1ojKCean-_QBE5_hpUaKuykf7xxXyzLt&state=state返回 302,返回到 8080 登陆
http://127.0.0.1:8080/oauth2/authorization/messaging-client-oidc?error获取 oidc 错误,返回 302,
http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&state=state&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=cc获取 code,返回 302,
http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=9TfRzYV6C1dRRO4UCK2W5eLUZ6bWg_KgZnZ6RPkSPPtOT8KVIiWSsxQuofTtjU-kL6oDireBgg4-W1KJrDrQ4U7gBXRrqo_BazeptpATiUJIFVYRVheF0oCLtNVyrzmP&state=state获取 code
http://127.0.0.1:8080/
③ 8080 首页点击请求
  • 进入了首页,单击某个链接,这个链接内部会访问 resource server,访问 resource server 需要 token
  • 这时候,访问了 9000 得到 code,并附带上范围。
  • 发现以前没有出现过的范围,就出现授权页面。授权通过后,返回到 8080 的8080/authorized?code,然后返回到具体的页面

5.2 源码分析

① 疑问

  • 代码中的http.formLogin(Customizer.withDefaults())是否重复
    • 不重复,如果注释了,就无法post /login ,可以修改from例子
  • 两个 config 类中,都使用了http.apply(new FederatedIdentityConfigurer()); 是否可以删除一个?
    • 不行,但是没有测试
  • 跟踪代码中的authorizationRequestUri
  • 如何与 github 关联上了?
  • 为什么有时候,会出现错误?

5.3 测试

从简到复杂,分为三步:

  • 第一步:添加基本的 Security,实现对要访问的 URL 进行保护,需要输入用户名与密码。
  • 第二步:添加 Authorization Server,可以通过 Code 获取 Token。
  • 第三步:添加 github 登陆。

权限认证还是比较复杂的,所以要添加好单元测试:

  • 第一步的单元测试
    • 直接访问 index 页面,就跳转到登陆页面。
    • 访问/test页面,跳转到登陆页面,然后输入用户名和密码,就可以得到/test页面返回的内容。
  • 第二步的单元测试
    • 没有范围确认页
      • 使用user1用户登陆成功,然后获取 code,接着通过 code 获取 token。
    • 有范围确认页
      • 模拟user1用户登陆成功,跳转到获取 code 链接,显示Consent required页面,在页面中选择范围,然后点击提交按钮后获取 code。
      • 模拟user1用户登陆成功,跳转到获取 code 链接,显示Consent required页面,在页面中选择取消按钮,就显示error=access_denied错误。

6. 重点功能

6.1 单点登陆

以前 Spring 通过@EnableOAuth2Sso来实现单点登陆,但是后来这个不推荐了,所以有人在外网问有没有替代的方法:Why is @EnableOAuth2Sso deprecated?

网上有人回答:在 Spring Security 5.2.x 中,这些注释已被弃用,我们需要使用 DSL 方法。本指出是Spring OAuth2 迁移指南中规定的。

public class SecurityConf extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Client(); //equivalent to @EnableOAuth2Client
http.oauth2Login(); //equivalent to @EnableOAuth2Sso
}

所以本例做了演示。

6.1.1 开发

① 单点登陆
② logout

在 html 文件中添加下面的代码,跳转到 9000 认证服务段,然后退出。

<div>
<a href="http://127.0.0.1:9000/logout">logout</a>
</div>

上述修改,会有一个严重的问题,就是如果是在浏览器中打开多个标签的情况下,只在其中一个标签点击 logout,并不会影响其他的标签退出。

如何解决这个问题?可能有两种思路:

1、使用 javaScript,来把当前页面全部重置了。

2、使用一个不透明的 athorizationServer

6.1.2 测试

第一步: 输入http://127.0.0.1:8080

第二步:在登陆页面,输入 user1,password

第三步:跳回到 index 页面,可以看到一个 8081 的链接。

第四步:点击链接,跳转到 8081 这个服务,没有出现登陆窗口。

6.2 RememberMe

6.2.1 问题描述

Spring-Authorization-Server 应该是有一个 Bug。

  • RememberMeAuthenticationFilter 放在OAuth2AuthorizationEndpointFilter后面,这样就不能自动登陆了。
  • 按照 Spring 的设计,普通的 Security 与 OAuth2 有两个 http 容器。 会生成两个 Remember,但是这两个的 Key 不一至,这样会不能验证通过。

普通 SpringSecurity 中的 Filter

0 = {DisableEncodeUrlFilter@8012}
1 = {WebAsyncManagerIntegrationFilter@8011}
2 = {SecurityContextPersistenceFilter@8010}
3 = {HeaderWriterFilter@8009}
4 = {CsrfFilter@8008}
5 = {LogoutFilter@8006}
6 = {UsernamePasswordAuthenticationFilter@8005}
7 = {DefaultLoginPageGeneratingFilter@7993}
8 = {DefaultLogoutPageGeneratingFilter@7992}
9 = {RequestCacheAwareFilter@7991}
10 = {SecurityContextHolderAwareRequestFilter@7990}
11 = {RememberMeAuthenticationFilter@7989}
12 = {AnonymousAuthenticationFilter@7988}
13 = {SessionManagementFilter@7985}
14 = {ExceptionTranslationFilter@7979}
15 = {FilterSecurityInterceptor@9743}

OAuth2 中的 Filter

0 = {DisableEncodeUrlFilter@8210}
1 = {WebAsyncManagerIntegrationFilter@8209}
2 = {SecurityContextPersistenceFilter@8208}
3 = {ProviderContextFilter@8207}
4 = {HeaderWriterFilter@8197}
5 = {CsrfFilter@8196}
6 = {LogoutFilter@8195}
7 = {OAuth2AuthorizationEndpointFilter@8194}
8 = {OidcProviderConfigurationEndpointFilter@8193}
9 = {NimbusJwkSetEndpointFilter@8192}
10 = {OAuth2AuthorizationServerMetadataEndpointFilter@8191}
11 = {OAuth2ClientAuthenticationFilter@8190}
12 = {RequestCacheAwareFilter@8189}
13 = {SecurityContextHolderAwareRequestFilter@8188}
14 = {AnonymousAuthenticationFilter@8187}
15 = {SessionManagementFilter@8186}
16 = {ExceptionTranslationFilter@8181}
17 = {FilterSecurityInterceptor@9627}
18 = {OAuth2TokenEndpointFilter@10352}
19 = {OAuth2TokenIntrospectionEndpointFilter@10353}
20 = {OAuth2TokenRevocationEndpointFilter@10354}
21 = {OidcUserInfoEndpointFilter@10355}

OAuth2 中添加 RememberMeFilter

这时候,会有一个 bug,因为OAuth2AuthorizationEndpointFilterRememberMeAuthenticationFilter 后面,也就是没有认证。

0 = {DisableEncodeUrlFilter@9264}
1 = {WebAsyncManagerIntegrationFilter@9263}
2 = {SecurityContextPersistenceFilter@8077}
3 = {ProviderContextFilter@8076}
4 = {HeaderWriterFilter@8075}
5 = {CsrfFilter@8074}
6 = {LogoutFilter@8073}
7 = {OAuth2AuthorizationEndpointFilter@8071}
8 = {OidcProviderConfigurationEndpointFilter@9312}
9 = {NimbusJwkSetEndpointFilter@9311}
10 = {OAuth2AuthorizationServerMetadataEndpointFilter@9310}
11 = {OAuth2ClientAuthenticationFilter@9309}
12 = {RequestCacheAwareFilter@9308}
13 = {SecurityContextHolderAwareRequestFilter@9307}
14 = {RememberMeAuthenticationFilter@9302}
15 = {AnonymousAuthenticationFilter@9332}
16 = {SessionManagementFilter@9331}
17 = {ExceptionTranslationFilter@9329}
18 = {FilterSecurityInterceptor@9702}
19 = {OAuth2TokenEndpointFilter@9703}
20 = {OAuth2TokenIntrospectionEndpointFilter@9704}
21 = {OAuth2TokenRevocationEndpointFilter@9705}
22 = {OidcUserInfoEndpointFilter@9706}

OAuth2AuthorizationEndpointConfigurer.class

Thank you for your reply.

The PR purpose is to modify OAuth2AuthorizationEndpointConfigurer.class, Because the wrong filter's order will cause bugs in the “remember-me” function.

RememberMeAuthenticationFilter代码中,会判断是否登陆过:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.authorizationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
......................................
if (!authorizationCodeRequestAuthenticationResult.isAuthenticated()) {
// If the Principal (Resource Owner) is not authenticated then
// pass through the chain with the expectation that the authentication process
// will commence via AuthenticationEntryPoint
filterChain.doFilter(request, response);
return;
}
......................................
} catch (OAuth2AuthenticationException ex) {
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}

将 OAuth2AuthorizationEndpointFilter 放在后面的效果。

0 = {DisableEncodeUrlFilter@7168}
1 = {WebAsyncManagerIntegrationFilter@7167}
2 = {SecurityContextPersistenceFilter@7166}
3 = {ProviderContextFilter@7165}
4 = {HeaderWriterFilter@7164}
5 = {CsrfFilter@7163}
6 = {LogoutFilter@7162}
7 = {OidcProviderConfigurationEndpointFilter@7154}
8 = {NimbusJwkSetEndpointFilter@7153}
9 = {OAuth2AuthorizationServerMetadataEndpointFilter@7152}
10 = {OAuth2ClientAuthenticationFilter@7151}
11 = {RequestCacheAwareFilter@7150}
12 = {SecurityContextHolderAwareRequestFilter@7149}
13 = {RememberMeAuthenticationFilter@7147}
14 = {AnonymousAuthenticationFilter@8135}
15 = {SessionManagementFilter@8134}
16 = {OAuth2AuthorizationEndpointFilter@8128}
17 = {ExceptionTranslationFilter@8662}
18 = {FilterSecurityInterceptor@8663}
19 = {OAuth2TokenEndpointFilter@8664}
20 = {OAuth2TokenIntrospectionEndpointFilter@8665}
21 = {OAuth2TokenRevocationEndpointFilter@8666}
22 = {OidcUserInfoEndpointFilter@8667}

6.2.2 修改方法

在 github 上提交了一个 pull request

To implement the “remember-me” feature , need to modify oauth2authorizationendpointconfigurer.class, put oauth2authorizationendpointfilter after remembermeauthenticationfilter.

Step1: modify OAuth2AuthorizationEndpointConfigurer.class's code of spring-authorization-server

// builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
builder.addFilterBefore(postProcess(authorizationEndpointFilter), ExceptionTranslationFilter.class);

Step2: Modify the default-authorizationserver's code in samples and test it

  • Add rememberMe in AuthorizationServerConfig.class
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, UserDetailsService users) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// @formatter:off
http
.exceptionHandling(exceptions ->
exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
);
http.rememberMe((rememberMe) -> {
rememberMe.userDetailsService(users);
rememberMe.key("123");
});
// @formatter:on
return http.build();
}
  • Add rememberMe in DefaultSecurityConfig.class
public class DefaultSecurityConfig {
// @formatter:off
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, UserDetailsService users) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(withDefaults());
http.rememberMe((rememberMe) -> {
rememberMe.userDetailsService(users);
rememberMe.key("123");
});
return http.build();
}

Step3: Test

  • run application

    • run default-authorizationserver
    • run messages-resource
    • run messages-client
  • http://127.0.0.1:8080