Spring Security + OAuth2.0 构建微服务统一认证解决方案(四)
搭建过程可以分为以下几步
- 构建简单的Spring Security + OAuth2.0 认证服务
- 优化认证服务(使用JWT技术加强token,自定义auth接口以及返回结果)
- 配置gateway服务完成简单鉴权功能
- 优化gateway配置(添加复杂鉴权逻辑等等)
(四)优化网关鉴权,添加复杂鉴权逻辑
接上篇,对简单的网关鉴权逻辑进行优化
一. 构建 AuthorizationManager 自定义鉴权逻辑
@Service
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
// 预检请求直接放行
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
// 如果token以"Bearer "为前缀,到此方法里说明JWT有效即已认证,其他前缀的token则拦截
String token = request.getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(token) || !token.startsWith("Bearer ")) {
return Mono.just(new AuthorizationDecision(false));
}
return Mono.just(new AuthorizationDecision(true));
}
}
在之前创建的 springSecurityFilterChain 中进行配置
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
@Autowired
private AuthorizationManager authenticationManager;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.oauth2ResourceServer().jwt();
http.authorizeExchange()
// 配置鉴权
.anyExchange().access(authenticationManager)
.and().csrf().disable();
return http.build();
}
}
此时请求header中Authorization字段前缀非"Bearer "时就会被直接拦截
二. 配置鉴权白名单
如获取Token这类接口,在发送请求的时候是没法携带有效token的,如果请求走网关则会被拦截。此时就需要配置白名单,跳过鉴权。
在yaml中配置一些白名单路径
secure:
ignore:
urls:
- "/user-service/login"
- "/pgcp-auth/oauth/token"
构建配置类,服务启动时读取yaml配置
@Component
@Data
@ConfigurationProperties(prefix="secure.ignore")
public class WhiteListUrlsConfig {
private List<String> urls;
}
在之前的 AuthorizationManager 中加入白名单逻辑
@Service
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private WhiteListUrlsConfig whiteListUrlsConfig;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
URI uri = request.getURI();
PathMatcher pathMatcher = new AntPathMatcher();
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
// 鉴权白名单逻辑
List<String> ignoreUrls = whiteListUrlsConfig.getUrls();
for (String ignoreUrl : ignoreUrls) {
if (pathMatcher.match(ignoreUrl, uri.getPath())) {
return Mono.just(new AuthorizationDecision(true));
}
}
// 如果token以"Bearer "为前缀,到此方法里说明JWT有效即已认证,其他前缀的token则拦截
String token = request.getHeaders().getFirst(Auth.JWT_TOKEN_HEADER);
if (StringUtils.isBlank(token) || !token.startsWith(Auth.JWT_TOKEN_PREFIX)) {
return Mono.just(new AuthorizationDecision(false));
}
return Mono.just(new AuthorizationDecision(true));
}
}
验证:通过网关路由生成token请求,可以看到在未携带token的情况下请求成功,返回了想要的结果。
三. 配置自定义gateway filter
可能在后续的请求需要用到userId这个字段,但是这个字段是被编码在JWT中的。如果把JWT带到后续的各种接口中,需要的时候进行解析,会产生大量重复代码。
干脆就在gateway这里解析,并把需要的userId字段放置入header中,后续需要自取即可
自定义filter如下完成解析的工作
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst(Auth.JWT_TOKEN_HEADER);
if (StringUtils.isBlank(token)) {
return chain.filter(exchange);
}
try {
String accessToken = token.replace(Auth.JWT_TOKEN_PREFIX, "");
JWSObject object = JWSObject.parse(accessToken);
String userId = (String) object.getPayload().toJSONObject().get("userId");
// 从token中解析出userId字段设置到header中
ServerHttpRequest request = exchange.getRequest().mutate().header("userId", userId).build();
exchange = exchange.mutate().request(request).build();
} catch (ParseException e) {
Asserts.fail(ResultCode.UNAUTHORIZED);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
验证如下,在filesystem-service中写一个简单的测试接口
@RestController
public class TestController {
@Autowired
private HttpServletRequest httpServletRequest;
@GetMapping("/t")
public String get(@RequestParam String str) {
return str + " " + httpServletRequest.getHeader("userId");
}
}
postman测试,成功!
四. 更加复杂的鉴权逻辑(Todo)
可以设置url对应的用户角色访问权限(用redis预先设置),配置鉴权逻辑进 AuthorizationManager 中。
Spring Security + OAuth2.0 构建微服务统一认证解决方案(一)
Spring Security + OAuth2.0 构建微服务统一认证解决方案(二)
Spring Security + OAuth2.0 构建微服务统一认证解决方案(三)
github 仓库