<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
application.yml
server:
port: 8089
spring:
main:
allow-bean-definition-overriding: true
@EnableWebFlux
@SpringBootApplication
public class SpringSecurityOAuth2TestApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(SpringSecurityOAuth2TestApplication.class);
application.setWebApplicationType(WebApplicationType.REACTIVE);
application.run(args);
}
}
表单验证登陆时 Serverlet 与 Webflux 的相关核心类的对照情况
@RestController
public class LoginController {
@GetMapping("/") // 默认登陆成功后跳转
public Mono<String> main(){
return Mono.just("main");
}
@GetMapping("/test1")
public Mono<String> test1(){
return Mono.just("test1");
}
@GetMapping("/test2")
public Mono<String> test2(){
return Mono.just("test2");
}
@GetMapping("/test3")
public Mono<Integer> test3(){
return Mono.fromSupplier(() -> new Random().nextInt());
}
@GetMapping("/test4")
public Mono<Double> test4(){
return Mono.fromSupplier(() -> new Random().nextDouble());
}
}
@Configuration
@EnableWebFluxSecurity
public class WebfluxConfiguration {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){
http.authorizeExchange(exchanges -> exchanges // 对于请求进行匹配
.pathMatchers("/test1").permitAll()
.pathMatchers("/test2").hasAnyRole("root")
.pathMatchers("/test3").hasAnyRole("admin")
.pathMatchers("/test4").hasAnyAuthority("root","user")
.anyExchange().authenticated()
);
http.formLogin(Customizer.withDefaults());// 开启表单验证
http.httpBasic(Customizer.withDefaults());// 开启 Basic 验证
http.csrf(csrf -> csrf.disable().headers().disable()); // csrf 防护进行配置
http.cors(cors -> cors.configurationSource( // 对跨域请求进行配置
exchange -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setExposedHeaders(Collections.singletonList("Content-Disposition"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
return config;
}
));
return http.build();
}
@Bean // 密码加密器曝露,其也会被自动注入到 webflux security 中
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean // 开启表单验证时一定要曝露一个 ReactiveUserDetailService,他会被自动注入到 WebfluxSecurity 中
public MapReactiveUserDetailsService userDetailsService(){
PasswordEncoder passwordEncoder = passwordEncoder();
UserDetails root = User.withUsername("root") // 创建两个 UserDetail
.password(passwordEncoder.encode("123456"))
// .authorities("root","ROLE_user","user")
.roles("root","user") // role 与 authorities 会相互覆盖只能用一个
.build();
UserDetails user = User.withUsername("usr")
.password(passwordEncoder.encode("password"))
.authorities("ROLE_user","user")
.build();
UserDetails role = User.withUsername("test")
.password(passwordEncoder.encode("admin"))
.authorities("ROLE_admin") // role 为 admin 的权限就是 ROLE_admin
.build();
return new MapReactiveUserDetailsService(root,user,role);
// 注意: MapReactiveUserDetailsService 在此段代码中只是用于模拟自我实现的 ReactiveUserDetailService
// 在实际开发中可以自需要自己实现这个接口
}
}
进入登陆页面,输入 test 的用户名和密码,在登陆成功后请求 test3 可以看到被校验通过
WebFlux 与 Servelet 的 OAuth2 核心类对照表
WebFlux | Servelet |
---|---|
ClientRegistration | ClientRegistration |
ReactiveClientRegistrationRepository | ClientRegistrationRepository |
OAuth2AuthorizedClient | ClientRegistrationOAuth2AuthorizedClient |
ServerOAuth2AuthorizedClientRepository / ReactiveOAuth2AuthorizedClientService | OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService |
ReactiveOAuth2AuthorizedClientManager / ReactiveOAuth2AuthorizedClientProvider | OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider |
application.yml
auth_server: http://localhost:8088/ # 指定授权服务器地址
spring:
main:
allow-bean-definition-overriding: true
security:
oauth2:
client:
registration:
test: # registrationId
clientId: client # clientId
clientSecret: yourSecret # clientSecret
authorizationGrantType: password # authorization_code # 授权类型
scope: all # 授权范围
provider:
test: # providerId
authorizationUri: ${auth_server}/oauth/authorize # 验证授权的uri
tokenUri: ${auth_server}/oauth/token # 获取 token 的 uri
@Configuration
@EnableWebFluxSecurity
public class WebfluxConfiguration {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){
http.authorizeExchange(exchanges -> exchanges // 对于请求进行匹配
.pathMatchers("/oauth/**").permitAll()
.anyExchange().authenticated()
);
http.oauth2Client(Customizer.withDefaults());// 使用 OAuth2 Client
http.csrf(csrf -> csrf.disable().headers().disable()); // csrf 防护进行配置
http.cors(cors -> cors.configurationSource( // 对跨域请求进行配置
exchange -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setExposedHeaders(Collections.singletonList("Content-Disposition"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
return config;
}
));
return http.build();
}
}
@Data
public class UserDto {
private String username;
private String password;
}
OAuth2Configuration
@Configuration
public class OAuth2Configuration {
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters,
// map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
UserDto exchange = authorizeRequest.getAttribute(UserDto.class.getName());
if (StringUtils.hasText(exchange.getUsername()) && StringUtils.hasText(exchange.getPassword())) {
contextAttributes = new HashMap<>();
// `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, exchange.getUsername());
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, exchange.getPassword());
}
return Mono.just(contextAttributes);
};
}
Oauth2Controller
@RestController
public class OAuth2Controller {
@Autowired
ReactiveOAuth2AuthorizedClientManager clientManager;
@PostMapping("/oauth/login")
public Mono<String> login(@RequestBody UserDto user){
Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("test")
.principal(authentication)
.attribute(UserDto.class.getName(),user)
.build();
return clientManager.authorize(authorizeRequest).map(oAuth2AuthorizedClient -> {
OAuth2AccessToken accessToken = oAuth2AuthorizedClient.getAccessToken();
if(accessToken != null && StringUtils.hasLength(accessToken.getTokenValue())){
System.out.println(accessToken.getTokenValue());
return accessToken.getTokenValue();
}
return "no info";
});
}
}
OAuth2Configuration
@Bean
public WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient(){
return new WebClientReactiveAuthorizationCodeTokenResponseClient();
}
Oauth2Controller
@Autowired
WebClientReactiveAuthorizationCodeTokenResponseClient client;
@Autowired
ReactiveClientRegistrationRepository clientRegistrationRepository;
@GetMapping("/oauth/loginWithCode")
public Mono<String> test(@PathParam("code")String code){
if(StringUtils.hasLength(code)){
OAuth2AuthorizationRequest oAuth2AuthorizeRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("http://localhost:8088/oauth/authorize")
.clientId("client")
.build();
OAuth2AuthorizationResponse response = OAuth2AuthorizationResponse.success(code).
redirectUri("http://www.baidu.com").build();
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(oAuth2AuthorizeRequest,response);
OAuth2AuthorizationCodeGrantRequest request = new OAuth2AuthorizationCodeGrantRequest(
clientRegistrationRepository.findByRegistrationId("test").block() ,exchange);
return client.getTokenResponse(request).map(res -> res.getAccessToken().getTokenValue());
}
return Mono.just("false");
}
server:
port: 8089
auth_server: http://localhost:8088/ # 指定授权服务器地址
spring:
main:
allow-bean-definition-overriding: true
security:
oauth2:
client:
registration:
test: # registrationId
clientId: client # clientId
clientSecret: yourSecret # clientSecret
redirectUri: http://localhost:${server.port}/test/2
authorizationGrantType: password # authorization_code # 授权类型
scope: all # 授权范围
provider:
test: # providerId
authorizationUri: ${auth_server}/oauth/authorize # 验证授权的uri
tokenUri: ${auth_server}/oauth/token # 获取 token 的 uri
resourceserver:
jwt:
public-key-location: classpath:public.cert # 指定公钥位置
WebfluxConfiguration
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){
http.authorizeExchange(exchanges -> exchanges // 对于请求进行匹配
.pathMatchers("/oauth/**").permitAll()
.anyExchange().authenticated()
);
http.oauth2Client(Customizer.withDefaults());// 使用 OAuth2 Client
// 对资源服务器进行相关配置
http.oauth2ResourceServer(resource ->{
resource.jwt(); // 开启资源服务器的 Jwt
});
http.csrf(csrf -> csrf.disable().headers().disable()); // csrf 防护进行配置
http.cors(cors -> cors.configurationSource( // 对跨域请求进行配置
exchange -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setExposedHeaders(Collections.singletonList("Content-Disposition"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
return config;
}
));
return http.build();
}
@Autowired
public ReactiveJwtDecoder jwtDecoder;
@PostMapping("/oauth/login")
public Mono<String> login(@RequestBody UserDto user){
Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("test")
.principal(authentication)
.attribute(UserDto.class.getName(),user)
.build();
return clientManager.authorize(authorizeRequest).map(oAuth2AuthorizedClient -> {
OAuth2AccessToken accessToken = oAuth2AuthorizedClient.getAccessToken();
if(accessToken != null && StringUtils.hasLength(accessToken.getTokenValue())){
System.out.println(accessToken.getTokenValue());
jwtDecoder.decode(accessToken.getTokenValue()).subscribe(jwt -> { // 解码
System.out.println(jwt.getClaims()); // 打印信息
System.out.println(jwt.getId()); // 打印 jwt
});
return accessToken.getTokenValue();
}
return "noinfo";
});
}