移动端项目,鉴权需求比较简单,Spring Cloud Gateway 只做 JWT 校验及角色鉴权,登录之类的全部是自定义处理的,微服务间传递 JWS 达到传递凭证的目的,下游服务无需鉴权也不依赖 Spring Security,需要当前用户的代码直接解析 JWS 获取当前用户
还有就是 Spring Security 5.4 + WebSecurityConfigurerAdapter
已被标记为弃用,非 reactive 项目也开始推荐使用 Bean 注入了,所以下面的代码也可以算是 5.4 + 的迁移指南了
核心代码:
JwsService
提供 JWS 的签名与校验,返回对应的 JwsPayload
对象,JwsPayload
为 POJO,提供当前 principal,包含一个 List<Authority> authoritys
属性,Authority
为 POJO,因为放在了 lib
模块中,没有引入 Spring Security 依赖,直接继承自 Object
,也可以看到下面代码手动进行转换为 GrantedAuthority
package com.seliote.bubble.gwsvr.security;
import com.seliote.bubble.svrlib.config.JwsProps;
import com.seliote.bubble.svrlib.service.JwsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* JWS Filter
* If authorization header exists, will set to security context
*
* @author seliote
* @since 2022-07-06
*/
@Slf4j
@Component
public class JwsFilter implements WebFilter {
private final JwsService jwsService;
private final String headerName;
@Autowired
public JwsFilter(JwsService jwsService, JwsProps jwsProps) {
this.jwsService = jwsService;
this.headerName = jwsProps.getHeader();
}
@NonNull
@Override
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
var header = exchange.getRequest().getHeaders().getFirst(headerName);
var payloadOpt = jwsService.verify(header);
if (payloadOpt.isPresent() && payloadOpt.get().available()) {
var payload = payloadOpt.get();
List<? extends GrantedAuthority> authorities = new ArrayList<>();
if (payload.getAuthorities() != null && payload.getAuthorities().size() != 0) {
authorities = payload.getAuthorities().stream().map(r -> (GrantedAuthority) r::getAuthority).toList();
}
var authentication = new UsernamePasswordAuthenticationToken(payload, null, authorities);
log.trace("Set security context {}", payload);
return chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
}
return chain.filter(exchange);
}
}
因为是完全自定义去实现登录之类的操作,所以提供了一个空的 ReactiveAuthenticationManager
,对应 MVC 里默认实现的 authenticationManager()
package com.seliote.bubble.gwsvr.security;
import com.seliote.bubble.svrlib.domain.Authority;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
/**
* Security context config
*
* @author seliote
* @since 2022-07-06
*/
@Slf4j
@EnableWebFluxSecurity
public class SecurityConfig {
private final JwsFilter jwsFilter;
@Autowired
public SecurityConfig(JwsFilter jwsFilter) {
this.jwsFilter = jwsFilter;
}
@Bean
public ReactiveAuthenticationManager authenticationManager() {
return authentication -> Mono.empty();
}
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
final var permitAll = new String[]{"/user-svr/country/list"};
return http.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.authorizeExchange().pathMatchers(permitAll).permitAll()
.pathMatchers("/**").hasAuthority(Authority.USER)
.anyExchange().authenticated()
.and().addFilterAt(jwsFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
}