SpringBoot-OAuth2-JWT-WebFlux-ResourceServer

摘要

依赖

  • springboot官方提供了支持

1
2
3
4
5
6
7
//webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

//本项目中使用了lombok减少代码量,非必须依赖
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

ReactiveSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.example.oauth2resourceserverwebfluxdemo.config;

import com.example.oauth2resourceserverwebfluxdemo.security.CustomReactiveAuthorizationManager;
import com.example.oauth2resourceserverwebfluxdemo.security.CustomServerAccessDeniedHandler;
import com.example.oauth2resourceserverwebfluxdemo.security.CustomServerAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;

import java.util.Collection;
import java.util.stream.Collectors;

/**
* <h1>安全认证配置</h1>
* Created by hanqf on 2020/11/19 10:26.
*/


@Configuration
//@EnableWebFluxSecurity //非必要
@EnableReactiveMethodSecurity //启用@PreAuthorize注解配置
public class ReactiveSecurityConfig {


@Autowired
private CustomServerAccessDeniedHandler customServerAccessDeniedHandler;

@Autowired
private CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;

@Autowired
private CustomReactiveAuthorizationManager customReactiveAuthorizationManager;

/**
* 注册安全验证规则
* 配置方式与HttpSecurity基本一致
*/
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { //定义SecurityWebFilterChain对安全进行控制,使用ServerHttpSecurity构造过滤器链;
return http.authorizeExchange()
//.anyExchange().authenticated() //所有请求都需要通过认证
.pathMatchers("/res/**", "/userInfo/**").authenticated()
//需要具备相应的角色才能访问
.pathMatchers("/user/**").hasAnyRole("admin", "user")
//不需要登录就可以访问
.pathMatchers("/swagger-ui/**", "/v3/api-docs**").permitAll()

//其它路径需要根据指定的方法判断是否有权限访问,基于权限管理模型认证
.anyExchange().access(customReactiveAuthorizationManager)
.and()
.csrf().disable() //关闭CSRF(Cross-site request forgery)跨站请求伪造
.httpBasic().disable() //不支持HTTP Basic方式登录
.formLogin().disable()//不支持login页面登录
.cors() //开启跨域支持
.and()

//鉴权时只支持Bearer Token的形式,不支持url后加参数access_token
.oauth2ResourceServer()//开启oauth2资源认证
.jwt() //token为jwt
//默认情况下,权限是scope,而我们希望使用的是用户的角色,所以这里需要通过转换器进行处理
.jwtAuthenticationConverter(jwt -> { //通过自定义Converter来指定权限,Converter是函数接口,当前上下问参数为JWT对象
Collection<SimpleGrantedAuthority> authorities =
((Collection<String>) jwt.getClaims()
.get("authorities")).stream() //获取JWT中的authorities
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());

//如果希望保留scope的权限,可以取出scope数据然后合并到一起,这样因为不是以ROLE_开头,所以需要使用hasAuthority('SCOPE_any')的形式
Collection<SimpleGrantedAuthority> scopes = ((Collection<String>) jwt.getClaims()
.get("scope")).stream().map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
.collect(Collectors.toSet());
//合并权限
authorities.addAll(scopes);
return Mono.just(new JwtAuthenticationToken(jwt, authorities));
})
.and()
.accessDeniedHandler(customServerAccessDeniedHandler)
.authenticationEntryPoint(customServerAuthenticationEntryPoint)
.and().build();
}
}

CustomReactiveAuthorizationManager

基于RBAC权限认证管理模型的认证方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.example.oauth2resourceserverwebfluxdemo.security;

import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;

import java.util.HashSet;
import java.util.Set;

/**
* <h1>ReactiveAuthorizationManager</h1>
* Created by hanqf on 2020/11/30 12:05.
*/

@Component
public class CustomReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
return authentication.map(auth -> {
ServerHttpRequest request = object.getExchange().getRequest();
Object principal = auth.getPrincipal();
String username;
if (principal instanceof Jwt) {
username = ((Jwt) principal).getClaimAsString("user_name");
} else {
username = principal.toString();
}
boolean hasPerssion = false;
if (StringUtils.hasText(username) && !"anonymousUser".equals(username)) {
//根据用户名查询用户资源权限,这里应该访问数据库查询
Set<String> uris = new HashSet<>();
for (String uri : uris) {
//验证用户拥有的资源权限是否与请求的资源相匹配
if (new AntPathMatcher().match(uri, request.getURI().toString())) {
hasPerssion = true;
break;
}
}
}

//暂时全部返回true
hasPerssion = true;
return new AuthorizationDecision(hasPerssion);
});
}
}

CustomServerAccessDeniedHandler

没有权限时的处理方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.oauth2resourceserverwebfluxdemo.security;


import com.example.oauth2resourceserverwebfluxdemo.exception.AjaxResponse;
import com.example.oauth2resourceserverwebfluxdemo.exception.CustomException;
import com.example.oauth2resourceserverwebfluxdemo.exception.CustomExceptionType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* <h1>没有权限时的处理方式</h1>
* Created by hanqf on 2020/11/20 11:56.
*/

@Component
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {
@SneakyThrows
@Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) {
return setErrorResponse(serverWebExchange.getResponse(),e.getMessage());
}

protected Mono<Void> setErrorResponse(ServerHttpResponse response, String message) throws JsonProcessingException {
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ObjectMapper objectMapper = new ObjectMapper();
AjaxResponse ajaxResponse = AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, message));
return response.writeWith(Mono.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(ajaxResponse))));

}
}

CustomServerAuthenticationEntryPoint

token格式错误或过期时的处理方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.oauth2resourceserverwebfluxdemo.security;


import com.example.oauth2resourceserverwebfluxdemo.exception.AjaxResponse;
import com.example.oauth2resourceserverwebfluxdemo.exception.CustomException;
import com.example.oauth2resourceserverwebfluxdemo.exception.CustomExceptionType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* <h1>ServerAuthenticationEntryPoint</h1>
* Created by hanqf on 2020/11/20 12:01.
* <p>
* token格式错误或过期时的处理方式
*/
@Component
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
@SneakyThrows
@Override
public Mono<Void> commence(ServerWebExchange serverWebExchange, AuthenticationException e) {
return setErrorResponse(serverWebExchange.getResponse(), e.getMessage());
}

protected Mono<Void> setErrorResponse(ServerHttpResponse response, String message) throws JsonProcessingException {
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ObjectMapper objectMapper = new ObjectMapper();
AjaxResponse ajaxResponse = AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, message));
return response.writeWith(Mono.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(ajaxResponse))));

}
}

WebFluxConfig

配置跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.oauth2resourceserverwebfluxdemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;

/**
* <h1>WebFlux配置类</h1>
* Created by hanqf on 2020/11/18 17:43.
*
* 我们若需要配置Spring WebFlux只需让配置配实现接口WebFluxConfigurer,
* 这样我们既能保留Spring Boot给WebFlux配置又能添加我们的定制配置。
* 若我们向完全控制WebFlux,则在配置类添加注解@EnableWebFlux
* 配置方式和Spring MVC类似
*/

@Configuration
public class WebFluxConfig implements WebFluxConfigurer {

//跨域设置
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
.allowedMethods("GET","POST", "PUT", "DELETE") //开放哪些Http方法,允许跨域访问
.allowedHeaders("*") //允许HTTP请求中的携带哪些Header信息
//When allowCredentials is true, allowedOrigins cannot contain the special value "*"since that cannot be set on the "Access-Control-Allow-Origin" response header.
// To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
//.allowedOrigins("*") //开放哪些ip、端口、域名的访问权限
.allowedOriginPatterns("*")
.allowCredentials(true); //是否允许发送Cookie信息

}
}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

#资源服务器端口号
server:
port: 8083
spring:
application:
name: oauth2-resource-server-webflux

#oauth2 配置
security:
oauth2:
resourceserver:
jwt:
# 公钥文件路径
# public-key-location: classpath:oauth2_key.pub

# 认证服务器提供的密钥验证路径,这种方式每次验证access_token都需要访问认证服务器
jwk-set-uri: http://localhost:8080/.well-known/jwks.json

资源接口示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.example.oauth2resourceserverwebfluxdemo.controller;


import com.example.oauth2resourceserverwebfluxdemo.exception.AjaxResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import java.security.Principal;
import java.util.HashMap;
import java.util.Map;

/**
* <h1>res</h1>
* Created by hanqf on 2020/11/6 17:22.
*/

@RestController
public class UserController {

//注意权限区分大小写
@PreAuthorize("hasRole('admin') or hasRole('user')")
//@PreAuthorize("#oauth2.hasScope('any')") //不支持oauth2表达式
@RequestMapping(value = "/user")
public Mono<AjaxResponse> user(Principal principal) {
//principal在经过security拦截后,是org.springframework.security.authentication.UsernamePasswordAuthenticationToken
//在经OAuth2拦截后,是OAuth2Authentication
return Mono.just(AjaxResponse.success(principal));
}

//注意权限区分大小写
@PreAuthorize("hasAuthority('SCOPE_any')")
@RequestMapping(value = "/user2")
public Mono<AjaxResponse> user2(Principal principal) {
//principal在经过security拦截后,是org.springframework.security.authentication.UsernamePasswordAuthenticationToken
//在经OAuth2拦截后,是OAuth2Authentication
return Mono.just(AjaxResponse.success(principal));
}

/**
* 获取用户的claim信息
*/
@RequestMapping("/userInfo")
public Mono<Map<String, Object>> userInfo(Authentication authentication){
Map<String,Object> map = new HashMap<>();
Object principal = authentication.getPrincipal();
if(principal instanceof Jwt){
map.put("username", ((Jwt) principal).getClaim("user_name"));
map.putAll(((Jwt) principal).getClaims());
}

return Mono.just(map);
}

}