摘要
- 通过本文,你将知道如何搭建一个基于SpringBoot-OAuth2-JWT-WebFlux的客户端服务器
- 本文基于springboot:2.4.0,项目基于Gradle-6.6.1构建
- 本文基于SpringBoot-OAuth2-JWT-ClientServer并实现其功能,可以参考对比
- 代码地址:https://github.com/hanqunfeng/springbootchapter/tree/master/chapter48
依赖
- springboot官方提供了支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
//本项目使用了自定义授权页面,所以引入视图模板依赖和基于webjar的bootstrap,jquery
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
//webjars https://www.webjars.org
implementation 'org.webjars:bootstrap:4.5.3'
implementation 'org.webjars.bower:jquery:3.5.1'
// 可以在html中去掉webjars的版本号,这样升级的时候直接修改上面引入的webjars中的版本号即可,页面中不需要修改
implementation 'org.webjars:webjars-locator:0.40'
//r2dbc mysql 库
implementation 'dev.miku:r2dbc-mysql'
//Spring r2dbc 抽象层
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
//本项目中使用了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
89
90
91
92
93package com.example.oauth2clientwebfluxdemo.config;
import com.example.oauth2clientwebfluxdemo.security.CustomReactiveAuthorizationManager;
import com.example.oauth2clientwebfluxdemo.security.CustomServerAccessDeniedHandler;
import com.example.oauth2clientwebfluxdemo.security.CustomServerAuthenticationEntryPoint;
import com.example.oauth2clientwebfluxdemo.security.CustomServerOAuth2AuthorizedClientRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import java.net.URI;
/**
* <h1>安全认证配置</h1>
* Created by hanqf on 2020/11/19 10:26.
*/
//必要
//启用@PreAuthorize注解配置
public class ReactiveSecurityConfig {
private String oauth2_server_logout;
private CustomServerAccessDeniedHandler customServerAccessDeniedHandler;
private CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;
private CustomReactiveAuthorizationManager customReactiveAuthorizationManager;
private CustomServerOAuth2AuthorizedClientRepository customServerOAuth2AuthorizedClientRepository;
/**
* 注册安全验证规则
* 配置方式与HttpSecurity基本一致
*/
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { //定义SecurityWebFilterChain对安全进行控制,使用ServerHttpSecurity构造过滤器链;
return http.authorizeExchange()
//.anyExchange().authenticated() //所有请求都需要通过认证
.pathMatchers("/").authenticated()
//需要具备相应的角色才能访问
.pathMatchers("/user/**", "/user2/**").hasAuthority("SCOPE_any")
//不需要登录就可以访问
.pathMatchers("/login","/webjars/**").permitAll()
//其它路径需要根据指定的方法判断是否有权限访问,基于权限管理模型认证
.anyExchange().access(customReactiveAuthorizationManager)
//.anyExchange().permitAll()
.and()
.csrf().disable() //关闭CSRF(Cross-site request forgery)跨站请求伪造
//必须post访问
.logout().logoutUrl("/logout").logoutSuccessHandler(serverLogoutSuccessHandler())
.and()
//开启oauth2登录认证
.oauth2Login()
.and()
//开启基于oauth2的客户端信息
.oauth2Client()
//客户端信息基于数据库,基于内存去掉下面配置即可
.authorizedClientRepository(customServerOAuth2AuthorizedClientRepository)
.and().exceptionHandling()
.accessDeniedHandler(customServerAccessDeniedHandler)
.authenticationEntryPoint(customServerAuthenticationEntryPoint)
.and()
.build();
}
/**
* 退出重定向到认证登录页面,默认"/login?logout"
*/
public ServerLogoutSuccessHandler serverLogoutSuccessHandler(){
RedirectServerLogoutSuccessHandler redirectServerLogoutSuccessHandler = new RedirectServerLogoutSuccessHandler();
redirectServerLogoutSuccessHandler.setLogoutSuccessUrl(URI.create(oauth2_server_logout));
return redirectServerLogoutSuccessHandler;
}
}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
53package 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.
*/
public class CustomReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
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
38package 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.
*/
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {
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
39package 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格式错误或过期时的处理方式
*/
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
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
33package 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类似
*/
public class WebFluxConfig implements WebFluxConfigurer {
//跨域设置
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信息
}
}CustomServerOAuth2AuthorizedClientRepository
基于jdbc存储token信。
这里有一个注意事项:查询时不要使用.fetch()方法,其会按照字段名称的字母排序进行赋值,导致结果中key和value匹配混乱
1 | package com.example.oauth2clientwebfluxdemo.security; |
AuthController
自定义登录跳转页面
1 | package com.example.oauth2clientwebfluxdemo.controller; |
login.html
自定义登录页面
1 |
|
application.yml
1 |
|
ResController
请求资源服务示例
1 | package com.example.oauth2clientwebfluxdemo.controller; |
---------------- The End ----------------