本文最后更新于 2023-12-09,文章内容可能已经过时。

Spring Security Oauth2:

Spring Security OAuth2 是 Spring Security 提供的一个子项目,用于支持 OAuth 2.0 认证协议。OAuth 2.0 是一种开放标准的授权协议,广泛用于在不暴露用户凭据的情况下,允许第三方应用程序访问用户资源。

1.依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

2.配置security:

这是spring security的配置类,主要有两个功能,一是配置要拦截的接口,二是配置provider,每个provider代表一个授权者.

package com.elevator.auth.security.config;
​
import com.elevator.auth.security.extension.idNumPwd.IdNumPasswordAuthenticationProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
​
/**
 * 安全配置
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
​
    private final UserDetailsService sysUserDetailsService;
​
    private final StringRedisTemplate redisTemplate;
    //HttpSecurity配置
    //我们可以通过它来控制接口的安全访问策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                // 指定的接口直接放行
                .antMatchers("/oauth/**").permitAll()
                // @link https://gitee.com/xiaoym/knife4j/issues/I1Q5X6 (接口文档knife4j需要放行的规则)
                .antMatchers("/webjars/**", "/doc.html", "/swagger-resources/**", "/v3/api-docs", "/swagger-ui/**").permitAll()
                // 其他的接口都需要认证后才能请求
                .anyRequest().authenticated()
                .and()
                // 禁用 CSRF
                .csrf().disable();
    }
    /**
     * 认证管理对象
     *
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    //注册了多个provider,用于在不同模式,获取在数据库内的用户信息并作认证
    @Override
    public void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(idNumPasswordAuthenticationProvider())
                .authenticationProvider(daoAuthenticationProvider());
    }
​
    /**
     * 身份证号密码认证授权提供者
     *
     * @return
     */
    @Bean
    public IdNumPasswordAuthenticationProvider idNumPasswordAuthenticationProvider() {
        IdNumPasswordAuthenticationProvider provider = new IdNumPasswordAuthenticationProvider();
        // SysUserServiceImpl实现UserDetailsService接口的子类UserService,复写了loadUserByUsername方法
        provider.setUserDetailsService(sysUserDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }
​
​
    /**
     * 用户名密码认证授权提供者
     *
     * @return
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(sysUserDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        provider.setHideUserNotFoundExceptions(false); // 是否隐藏用户不存在异常,默认:true-隐藏;false-抛出异常;
        return provider;
    }
​
    /**
     * 密码编码器
     * <p>
     * 委托方式,根据密码的前缀选择对应的encoder,例如:{bcypt}前缀->标识BCYPT算法加密;{noop}->标识不使用任何加密即明文的方式
     * 密码判读 IdNumPasswordAuthenticationProvider
     */
    @Bean
    PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}
​

3.配置AuthorizationServerConfig:授权服务器:

主要对oauth的端点进行配置.

/**
 * @author Dave Syer
 *
 */
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    /**
    * 用来配置令牌端点的安全约束
    */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }
​
    /**
    * 用来配置客户端信息服务,客户端详情信息在这里初始化,可以写死在代码里,也可以放到配置文件或者数据库
    */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }
    /**
    * 用来配置令牌的访问端点和令牌服务
    */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }
}
​

这是配置示例:

package com.elevator.auth.security.config;
​
import com.alibaba.druid.support.json.JSONUtils;
import com.elevator.auth.security.detail.UserDetail;
import com.elevator.auth.security.extension.idNumPwd.IdNumPasswordTokenGranter;
import com.elevator.auth.security.extension.refresh.PreAuthenticatedUserDetailsService;
import com.elevator.auth.security.service.ClientDetailsServiceImpl;
import com.elevator.auth.security.service.SysUserServiceImpl;
import com.elevator.common.constants.SecurityConstant;
import com.elevator.common.response.Response;
import com.elevator.common.response.ResponseCode;
import lombok.RequiredArgsConstructor;
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.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
​
import java.security.KeyPair;
import java.util.*;
​
/**
 * OAuth 认证授权配置
 */
@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
​
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private ClientDetailsServiceImpl clientDetailsService;
    @Autowired
    private SysUserServiceImpl sysUserDetailsService;
​
    /**
     * jks文件
     */
    @Value("${jks.file}")
    private String jksFile;
    /**
     * jks密码
     */
    @Value("${jks.password}")
    private String jksPassword;
​
    /**
     * OAuth2客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //自定义ClientDetailsService
        clients.withClientDetails(clientDetailsService);
    }
​
    /**
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // Token增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
​
        //token存储模式设定 默认为InMemoryTokenStore模式存储到内存中
        endpoints.tokenStore(jwtTokenStore());
​
        // 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
        List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));
​
        //添加密码授权模式的授权者
        granterList.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory()
        ));
        // 添加身份证号授权模式的授权者
        granterList.add(new IdNumPasswordTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(),
                this.authenticationManager));
​
​
        CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
        endpoints
                .authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .tokenGranter(compositeTokenGranter)
​
                .tokenServices(tokenServices(endpoints))
        ;
    }
​
    /**
     * jwt token存储模式
     */
    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
​
    public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
​
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancerChain);
​
        // 多用户体系下,刷新token再次认证客户端ID和 UserDetailService 的映射Map
        Map<String, UserDetailsService> clientUserDetailsServiceMap = new HashMap<>();
        clientUserDetailsServiceMap.put(SecurityConstant.ADMIN_WEB_CLIENT_ID, sysUserDetailsService); // 系统管理web客户端
        clientUserDetailsServiceMap.put(SecurityConstant.ADMIN_APP_CLIENT_ID, sysUserDetailsService); // 系统管理移动客户端
        clientUserDetailsServiceMap.put(SecurityConstant.MAINTAIN_CLIENT_ID, sysUserDetailsService); // 维保app客户端
        clientUserDetailsServiceMap.put(SecurityConstant.USE_CLIENT_ID, sysUserDetailsService); // 物业app客户端
​
        // 刷新token模式下,重写预认证提供者替换其AuthenticationManager,可自定义根据客户端ID和认证方式区分用户体系获取认证用户信息
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(clientUserDetailsServiceMap));
        tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
​
        /** refresh_token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
         *  1 重复使用:access_token过期刷新时, refresh_token过期时间未改变,仍以初次生成的时间为准
         *  2 非重复使用:access_token过期刷新时, refresh_token过期时间延续,在refresh_token有效期内刷新便永不失效达到无需再次登录的目的
         */
        tokenServices.setReuseRefreshToken(true);
        return tokenServices;
​
    }
​
    /**
     * 使用对称加密算法对token签名
     */
//    @Bean
//    public JwtAccessTokenConverter jwtAccessTokenConverter() {
//        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
//        accessTokenConverter.setSigningKey("key"); //对称加密key
//        return accessTokenConverter;
//    }
​
    /**
     * 使用非对称加密算法对token签名
     */
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }
​
​
    /**
     * 密钥库中获取密钥对(公钥+私钥)
     */
    @Bean
    public KeyPair keyPair() {
        return new KeyStoreKeyFactory(
                new ClassPathResource(jksFile), jksPassword.toCharArray())
                .getKeyPair("jwt");
    }
​
    /**
     * JWT内容增强
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            Map<String, Object> additionalInfo = new HashMap<>();
            Object principal = authentication.getUserAuthentication().getPrincipal();
            if (principal instanceof UserDetail) {
                UserDetail userDetails = (UserDetail) principal;
                additionalInfo.put("id", userDetails.getId());
                additionalInfo.put("username", userDetails.getUsername());
                additionalInfo.put("idNum", userDetails.getIdNum());
                additionalInfo.put("unitId",userDetails.getUnitId());
                additionalInfo.put("unitType",userDetails.getUnitType());
                additionalInfo.put("roleList",userDetails.getAuthorities());
                additionalInfo.put("nickName",userDetails.getNickName());
            } else {
​
            }
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
            return accessToken;
        };
    }
​
​
    /**
     * 自定义认证异常响应数据
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, e) -> {
            response.setStatus(HttpStatus.OK.value());
            response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Cache-Control", "no-cache");
            Response<Object> resp = Response.failed(ResponseCode.CLIENT_AUTHENTICATION_FAILED);
            response.getWriter().print(JSONUtils.toJSONString(resp));
            response.getWriter().flush();
        };
    }
}

4.配置登陆端点:

 @ApiOperation(value = "OAuth2认证", notes = "登录入口")
    @PostMapping("/token")
    public Object postAccessToken(
            Principal principal,
            @RequestParam Map<String, String> parameters
    ) throws HttpRequestMethodNotSupportedException {
        //用户名密码登录时,md5加密的密码怎么办?
        /**
         * 获取登录认证的客户端ID
         * 放在请求头(Request Headers)中的Authorization字段,且经过加密,例如 Basic Y2xpZW50OnNlY3JldA== 明文等于 client:secret
         */
        String clientId = RequestUtils.getOAuth2ClientId();
        log.info("OAuth认证授权 客户端ID:{},请求参数:{}", clientId, JSONUtils.toJSONString(parameters));
​
        OAuth2AccessToken accessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
        if(!Objects.equals(parameters.get("grant_type"), "refresh_token")){
            String unitType = parameters.get("unitType");
            if(!unitType.equals(accessToken.getAdditionalInformation().get("unitType").toString())){
                return Response.failed("单位类型对应错误");
            }
        }

5.配置资源服务器:

资源服务器主要负责验证token安全性,解析token,获取相关的权限信息.

package com.elevator.gateway.security;
​
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.elevator.common.constants.SecurityConstant;
import com.nimbusds.jose.JWSObject;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
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.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
​
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
​
/**
 * 网关自定义鉴权管理器
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {
​
    private final RedisTemplate redisTemplate;
​
    @SneakyThrows
    @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));
        }
​
        PathMatcher pathMatcher = new AntPathMatcher(); // 【声明定义】Ant路径匹配模式,“请求路径”和缓存中权限规则的“URL权限标识”匹配
        String method = request.getMethodValue();
        String path = request.getURI().getPath();
        String restfulPath = method + " " + path;
​
        // 如果token以"bearer "为前缀,到此方法里说明JWT有效即已认证
        String token = request.getHeaders().getFirst(SecurityConstant.AUTHORIZATION_KEY);
        if (StrUtil.isEmpty(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstant.JWT_PREFIX) ) {
            return Mono.just(new AuthorizationDecision(false));
        }
​
        // 解析JWT
        token = StrUtil.replaceIgnoreCase(token, SecurityConstant.JWT_PREFIX, Strings.EMPTY);
        String payload = StrUtil.toString(JWSObject.parse(token).getPayload());
        JSONObject jsonObject = JSONUtil.parseObj(payload);
        // 获取 ClientId
        String clientId = jsonObject.getStr(SecurityConstant.CLIENT_ID_KEY);
​
​
        /**
         * 鉴权开始
         * 缓存取 [URL权限-角色权限集合] 规则数据
         * urlPermRolesRules = [{'key':'GET /admin/user/*','value':['role1', 'role2']},...]
         */
        // 判断是否存在权限对应数据
        if (!redisTemplate.hasKey(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        if (!redisTemplate.hasKey(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        if (!redisTemplate.hasKey(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        if (!redisTemplate.hasKey(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY)) {
            return Mono.just(new AuthorizationDecision(false));
        }
        Map<String, Object> webUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
        Set<String> webUrlPermsSet = webUrlPermRolesRules.keySet();
        Map<String, Object> appUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.ADMIN_APP_URL_PERM_ROLES_KEY);
        Set<String> appUrlPermsSet = appUrlPermRolesRules.keySet();
        Map<String, Object> useUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.USE_APP_URL_PERM_ROLES_KEY);
        Set<String> useUrlPermsSet = useUrlPermRolesRules.keySet();
        Map<String, Object> maintainUrlPermRolesRules = redisTemplate.opsForHash().entries(SecurityConstant.MAINTAIN_APP_URL_PERM_ROLES_KEY);
        Set<String> maintainUrlPermsSet = maintainUrlPermRolesRules.keySet();
        // 判断端
        Map<String, Object> urlPermRolesRules = null;
        if (SecurityConstant.ADMIN_WEB_CLIENT_ID.equals(clientId)) {
            urlPermRolesRules = webUrlPermRolesRules;
            // 如果访问的接口路径在web端没有,禁止访问
            if (!webUrlPermsSet.contains(restfulPath)) {
                return Mono.just(new AuthorizationDecision(false));
            }
        } else if (SecurityConstant.ADMIN_APP_CLIENT_ID.equals(clientId)) {
            urlPermRolesRules = appUrlPermRolesRules;
            // 如果访问的接口路径在app端没有,禁止访问
            if (!appUrlPermsSet.contains(restfulPath)) {
                return Mono.just(new AuthorizationDecision(false));
            }
        }else if(SecurityConstant.USE_CLIENT_ID.equals(clientId)){
            urlPermRolesRules = useUrlPermRolesRules;
            // 如果访问的接口路径在使用app端没有,禁止访问
            if (!useUrlPermsSet.contains(restfulPath)) {
                return Mono.just(new AuthorizationDecision(false));
            }
        }else if(SecurityConstant.MAINTAIN_CLIENT_ID.equals(clientId)){
            urlPermRolesRules = maintainUrlPermRolesRules;
            // 如果访问的接口路径在维保app端没有,禁止访问
            if (!maintainUrlPermsSet.contains(restfulPath)) {
                return Mono.just(new AuthorizationDecision(false));
            }
        }
​
        // 根据请求路径获取有访问权限的角色列表
        List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
        boolean requireCheck = false; // 是否需要鉴权,默认未设置拦截规则不需鉴权
​
        for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
            String perm = permRoles.getKey();
            if (pathMatcher.match(perm, restfulPath)) {
                List<String> roles = Convert.toList(String.class, permRoles.getValue());
                authorizedRoles.addAll(roles);
                if (!requireCheck) {
                    requireCheck = true;
                }
            }
        }
        // 没有设置拦截规则放行
        if (requireCheck == false) {
            return Mono.just(new AuthorizationDecision(true));
        }
​
        // 判断JWT中携带的用户角色权限是否有权限访问
        Mono<AuthorizationDecision> authorizationDecisionMono = mono
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(authority -> {
                    String permCode = StrUtil.removePrefix(authority, SecurityConstant.AUTHORITY_PREFIX);
                    boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(permCode);
                    return hasAuthorized;
                })
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
        return authorizationDecisionMono;
    }
}
​