文章目录

  • 1. 同时配置url和权限表达式
  • 1. /api/v1/doc 配置 antMatchers("/api/v1/doc").permitAll()
  • 2. /api/v1/doc 配置 @PreAuthorize("hasAuthority('knowledge')")
  • 3. 启动项目测试
  • 2. 配置白名单url不需要token认证
  • 1. 白名单属性配置类 WhiteUrlAutoConfiguration
  • 2. 资源服务器配置类 ResourceServerAutoConfiguration
  • 3. 配置拦截器 CommonWebMvcAutoConfiguration
  • 4. knowledge 业务服务
  • 5. 启动项目测试
  • 3. 配置白名单url不需要token认证也不需要鉴权
  • 1. 自定义权限表达式类 CustomMethodSecurityExpressionRoot
  • 2. 资源服务器配置类 ResourceServerAutoConfiguration
  • 3. 配置拦截器 CommonWebMvcAutoConfiguration
  • 4. knowledge 业务服务置文件中配置白名单 url
  • 5. 启动项目测试


1. 同时配置url和权限表达式

相关文章:

SpringSecurity Oauth2实战 - 08 SpEL权限表达式源码分析及两种权限控制方式原理

在前面文章中,我们分析了权限表达式的实现原理,并通过 debug 看了 url 权限表达式和注解权限表达式的调用过程,最终看到 url 权限表达式会在注解权限表达式之前执行,那么如果我们在资源服务器配置类 ResourceServerAutoConfiguration中配置了 permitAll 权限表达式,在方法注解中配置了 hasAuthority 权限表达式会怎么样呢?

1. /api/v1/doc 配置 antMatchers(“/api/v1/doc”).permitAll()

@Slf4j
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    @Value("${spring.application.name}")
    private String appName;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(appName);
        resources.tokenStore(tokenStore);
        resources.tokenExtractor(tokenExtractor());
    }

    @Bean
    @Primary
    public TokenExtractor tokenExtractor() {
        CustomTokenExtractor customTokenExtractor = new CustomTokenExtractor();
        return customTokenExtractor;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/v1/login", "/api/v1/token").permitAll()
            	 // 配置/api/v1/doc请求路径不需要认证就可以访问
                .antMatchers("/api/v1/doc").permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        http.formLogin().disable();
        http.httpBasic().disable();
    }
}

2. /api/v1/doc 配置 @PreAuthorize(“hasAuthority(‘knowledge’)”)

@RestController
@RequestMapping("/api/v1")
public class DocController {

    @PreAuthorize("hasAuthority('roleEdit')")
    @GetMapping("/doc")
    public String getDocList(){
        return "doc";
    }
}

3. 启动项目测试

用户具有的权限有:userEdit、superAdmin、knowledgeQuery、userQuery、knowledgeEdit

antMatchers("/api/v1/doc").permitAll():指定用户不需要授权就可以访问/api/v1/doc

@PreAuthorize("hasAnyAuthority('roleEdit')"):指定用户具备roleEdit权限才能访问 /api/v1/doc

因为 url 权限表达式会在注解权限表达式之前执行,因此将以注解权限表达式为准,用户没有权限访问;

java ip白名单过滤 springsecurity白名单_java

① 请求进入过滤器 OAuth2AuthenticationProcessingFilter 获取用户的认证信息

java ip白名单过滤 springsecurity白名单_java_02

② 因为该请求路径api/v1/doc没有在拦截器配置类中放行,因此请求在进入Controller层方法之前会被拦截器拦截,在拦截器中判断用户是否已经认证,如果用户没有认证将不会放行。

@Configuration
@EnableWebMvc
public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {

    @Bean
    public UserInfoInterceptor userInfoInterceptor() {
        return new UserInfoInterceptor();
    }

    @Override
    public void addInterceptors(@NonNull InterceptorRegistry registry) {
        registry.addInterceptor(userInfoInterceptor())
                // 放行的请求
                .excludePathPatterns("/api/v1/login");
    }
}

java ip白名单过滤 springsecurity白名单_java ip白名单过滤_03

③ 请求进入SecurityExpressionRoot类的 hasAuthority 方法而不是permitAll方法,说明最终请求会以注解表达式中配置的为准。

java ip白名单过滤 springsecurity白名单_服务器_04

java ip白名单过滤 springsecurity白名单_白名单_05

java ip白名单过滤 springsecurity白名单_jvm_06

2. 配置白名单url不需要token认证

如果想要白名单不需要token认证:

  1. 在 ResourceServerAutoConfiguration 中配置白名单url的权限表达式为 permitAll,代表不需要accessToken认证也能访问,不做任何鉴权。
  2. 在 CommonWebMvcAutoConfiguration 中配置白名单url放行;

1. 白名单属性配置类 WhiteUrlAutoConfiguration

@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "chahua")
@EnableConfigurationProperties
public class WhiteUrlAutoConfiguration implements InitializingBean {

    /**
     * 白名单url:不需要token认证
     */
    private Set<String> whiteUrls = new HashSet<>();

    @Override
    public void afterPropertiesSet() {
        if (whiteUrls != null && whiteUrls.size() > 0) {
            log.info("Load {} succeed: {}", "white-urls.yml", String.join(", ", whiteUrls));
        }
    }
}

2. 资源服务器配置类 ResourceServerAutoConfiguration

@Slf4j
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {

    @Autowired
    private WhiteUrlAutoConfiguration whiteUrlAutoConfiguration;

    @Autowired
    private TokenStore tokenStore;

    @Value("${spring.application.name}")
    private String appName;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(appName);
        resources.tokenStore(tokenStore);
        resources.tokenExtractor(tokenExtractor());
    }

    @Bean
    @Primary
    public TokenExtractor tokenExtractor() {
        CustomTokenExtractor customTokenExtractor = new CustomTokenExtractor();
        return customTokenExtractor;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/v1/login", "/api/v1/token").permitAll();
        // 配置白名单url不需要token认证和授权就可以访问
        Set<String> whiteUrls = whiteUrlAutoConfiguration.getWhiteUrls();
        if (whiteUrls.size() > 0) {
            String[] urlPatterns = whiteUrls.toArray(new String[0]);
            http.authorizeRequests(authorize -> authorize.antMatchers(urlPatterns).permitAll());
        }
        http.authorizeRequests().anyRequest().authenticated();
        http.formLogin().disable();
        http.httpBasic().disable();
    }
}

3. 配置拦截器 CommonWebMvcAutoConfiguration

public class UserInfoInterceptor extends HandlerInterceptorAdapter {
    /**
     * 拦截所有请求,在Controller层方法之前调用
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断用户是否被认证,如果没有认证不放行
        boolean isAuthenticated = request.authenticate(response);
        if (!isAuthenticated) {
            return false;
        }
        // 存储用户信息到本地线程
        Principal userPrincipal = request.getUserPrincipal();
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) userPrincipal;
        AuthUser ngsocUser = (AuthUser) oAuth2Authentication.getUserAuthentication().getPrincipal();
        UserInfo userInfo = ngsocUser.getUserInfo();
        UserInfoShareHolder.setUserInfo(userInfo);
        // 放行,继续执行Controller层的方法
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserInfoShareHolder.remove();
        super.afterCompletion(request, response, handler, ex);
    }
}

因为我们在项目中引入了拦截器,该拦截器会在Controller层方法执行之前拦截所有请求,判断用户是否认证,如果用户未认证请求将不会放行,因此需要配置白名单url放行;

@Configuration
@EnableWebMvc
public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {

    @Value("${spring.application.name}")
    private String appName;

    @Autowired
    private WhiteUrlAutoConfiguration whiteUrlAutoConfiguration;

    @Bean
    public UserInfoInterceptor userInfoInterceptor() {
        return new UserInfoInterceptor();
    }

    @Override
    public void addInterceptors(@NonNull InterceptorRegistry registry) {
        // 拦截器会拦截所有请求,需要配置放行的请求
        Set<String> whiteUrls = whiteUrlAutoConfiguration.getWhiteUrls();
        if("authority".equals(appName)){
            whiteUrls.add("/api/v1/login");
            whiteUrls.add("/api/v1/token");
        }
        registry.addInterceptor(userInfoInterceptor())
                // 因为白名单url不需要token认证就可以访问,如果不放行,拦截器的preHandle()方法会返回false
                .excludePathPatterns(whiteUrls.toArray(new String[0]));
    }
}

4. knowledge 业务服务

@RestController
@RequestMapping("/api/v1")
public class DocController {

    @GetMapping("/doc")
    public String getDocList(){
        return "doc";
    }
}

配置文件中配置白名单 url :

# 配置白名单url
chahua:
  white-urls:
    - /api/v1/doc

5. 启动项目测试

不携带 accessToken 访问/aoi/v1/doc

java ip白名单过滤 springsecurity白名单_服务器_07

① 请求进入过滤器 OAuth2AuthenticationProcessingFilter,可以看到 accessToken=null,authentication=null:

java ip白名单过滤 springsecurity白名单_jvm_08

② 请求进入 DocController 类的 getDocList 方法:

因为 ResourceServerAutoConfiguration 类中配置了资源不需要认证就可以访问,且拦截器配置了资源放行,因此请求直接进入了DocController中。

java ip白名单过滤 springsecurity白名单_白名单_09

java ip白名单过滤 springsecurity白名单_服务器_10

3. 配置白名单url不需要token认证也不需要鉴权

我们知道除了 http.authorizeRequests(authorize -> authorize.antMatchers(urlPatterns).permitAll()) 不做校验,所有的请求都会走到@PreAuthorize注解对应的方法里面,所以如果我们的请求url方法上配置了@PreAuthorize注解,即使在ResourceServerAutoConfiguration 配置了permitAll,仍然会进行鉴权。

在 @PreAuthorize 注解中常用的 hasAuthority、hasPermission、hasRole、hasAnyRole 都是由 SecurityExpressionRoot 类提供的,且他们都调用了 SecurityExpressionRoot 类的 hasAnyAuthorityName 方法完成鉴权,因此我们理论上只需要实现一个自定义权限表达式类继承 SecurityExpressionRoot 类并重写该类的hasAnyAuthorityName 方法即可。但是该方法是私有的,子类无法重写。因此我们可以直接自定义一个自定义权限表达式类但是不继承SecurityExpressionRoot 类,而是直接在该类中实现一个SecurityExpressionRoot 类的功能。

1. 自定义权限表达式类 CustomMethodSecurityExpressionRoot

public class CustomMethodSecurityExpressionRoot implements MethodSecurityExpressionOperations{

    @Setter
    private RequestMatcher requestMatcher;

    /**
     * MethodSecurityExpressionOperations 接口方法的属性
     */
    private Object filterObject;
    private Object returnObject;
    private Object target;

    /**
     *  SecurityExpressionRoot 类中的属性
     */
    protected Authentication authentication;
    private AuthenticationTrustResolver trustResolver;
    private RoleHierarchy roleHierarchy;
    private Set<String> roles;
    private String defaultRolePrefix = "ROLE_";
    private PermissionEvaluator permissionEvaluator;

    /**
     * 判断是否是白名单WhiteUrl,不校验权限,不需要token
     */
    private boolean isWhiteUrl() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        if (requestMatcher != null && requestMatcher.matches(request)) {
            // 白名单url,直接返回true
            return true;
        }
        return false;
    }

    /**
     * 修改 SecurityExpressionRoot 类中的该方法
     *
     * 登录请求url是否是白名单WhiteUrl,如果是则不需要校验权限,直接返回true
     */
    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        // 如果是白名单WhiteUrl或者公共CommonUrl,则不需要校验权限,直接返回true
        if (isWhiteUrl()) {
            return true;
        }
        Set<String> roleSet = getAuthoritySet();
        for (String role : roles) {
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }
        return false;
    }



    /**
     * 下面的方法都是 SecurityExpressionRoot 类中的实现方法,没有更改
     */

    public void setAuthentication(Authentication authentication) {
        if (authentication == null) {
            throw new IllegalArgumentException("Authentication object cannot be null");
        }
        this.authentication = authentication;
    }

    public CustomMethodSecurityExpressionRoot(Authentication authentication) {
        if (authentication == null) {
            throw new IllegalArgumentException("Authentication object cannot be null");
        }
        this.authentication = authentication;
    }

    @Override
    public Authentication getAuthentication() {
        return authentication;
    }

    // 判断当前用户具备的权限信息,是否存在指定权限
    @Override
    public final boolean hasAuthority(String authority) {
        return hasAnyAuthority(authority);
    }

    // 判断当前用户具备的权限信息,是否存在指定权限中的任意一个
    @Override
    public final boolean hasAnyAuthority(String... authorities) {
        return hasAnyAuthorityName(null, authorities);
    }

    // 判断当前用户具备的权限信息,是否存在指定角色
    @Override
    public final boolean hasRole(String role) {
        return hasAnyRole(role);
    }

    // 判断当前用户具备的权限信息,是否存在指定角色中的任意一个
    @Override
    public final boolean hasAnyRole(String... roles) {
        return hasAnyAuthorityName(defaultRolePrefix, roles);
    }

    // 允许所有的请求调用
    @Override
    public final boolean permitAll() {
        return true;
    }

    // 拒绝所有的请求调用
    @Override
    public final boolean denyAll() {
        return false;
    }

    // 当前用户是否是一个匿名用户
    @Override
    public final boolean isAnonymous() {
        return trustResolver.isAnonymous(authentication);
    }

    // 判断用户是否已经认证成功
    @Override
    public final boolean isAuthenticated() {
        return !isAnonymous();
    }

    // 当前用户是否通过RememberMe自动登录
    @Override
    public final boolean isRememberMe() {
        return trustResolver.isRememberMe(authentication);
    }

    // 当前登录用户是否既不是匿名用户又不是通过RememberMe登录的
    @Override
    public final boolean isFullyAuthenticated() {
        return !trustResolver.isAnonymous(authentication) && !trustResolver.isRememberMe(authentication);
    }

    public Object getPrincipal() {
        return authentication.getPrincipal();
    }

    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        this.trustResolver = trustResolver;
    }

    public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }

    public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
        this.permissionEvaluator = permissionEvaluator;
    }

    // 当前登录用户是否具有指定目标的指定权限
    @Override
    public boolean hasPermission(Object target, Object permission) {
        return permissionEvaluator.hasPermission(authentication, target, permission);
    }

    // 当前登录用户是否具有指定目标的指定权限
    @Override
    public boolean hasPermission(Object targetId, String targetType, Object permission) {
        return permissionEvaluator.hasPermission(authentication, (Serializable) targetId, targetType, permission);
    }

    private Set<String> getAuthoritySet() {
        if (roles == null) {
            roles = new HashSet<>();
            Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();
            if (roleHierarchy != null) {
                userAuthorities = roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }
            roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }
        return roles;
    }

    public void setDefaultRolePrefix(String defaultRolePrefix) {
        this.defaultRolePrefix = defaultRolePrefix;
    }

    private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
        if (role == null) {
            return role;
        }
        if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
            return role;
        }
        if (role.startsWith(defaultRolePrefix)) {
            return role;
        }
        return defaultRolePrefix + role;
    }

    /**
     * 下面的方法都是 MethodSecurityExpressionOperations 接口中的实现方法,没有更改
     */

    @Override
    public void setFilterObject(Object filterObject) {
        this.filterObject = filterObject;
    }

    @Override
    public Object getFilterObject() {
        return this.filterObject;
    }

    @Override
    public void setReturnObject(Object returnObject) {
        this.returnObject = returnObject;
    }

    @Override
    public Object getReturnObject() {
        return this.returnObject;
    }

    void setThis(Object target) {
        this.target = target;
    }

    @Override
    public Object getThis() {
        return target;
    }
}

2. 资源服务器配置类 ResourceServerAutoConfiguration

@Slf4j
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {
    /**
     * 权限表达式的自定义处理
     */
    @Autowired
    private GlobalMethodSecurityConfiguration globalMethodSecurityConfiguration;

    @Autowired
    private WhiteUrlAutoConfiguration whiteUrlAutoConfiguration;

    @Autowired
    private TokenStore tokenStore;

    @Value("${spring.application.name}")
    private String appName;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(appName);
        resources.tokenStore(tokenStore);
        resources.tokenExtractor(tokenExtractor());
    }

    @Bean
    @Primary
    public TokenExtractor tokenExtractor() {
        CustomTokenExtractor customTokenExtractor = new CustomTokenExtractor();
        return customTokenExtractor;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/v1/login", "/api/v1/token").permitAll();
        // 配置白名单url不需要token认证和授权就可以访问
        // 如果url接口上使用了@PreAuthorize注解权限表达式,那么这里就不需要配置permitAll了,即使配置了,逻辑也会被覆盖掉
        // 如果url接口上没有使用@PreAuthorize注解权限表达式,这里又没有配置permitAll,那么url接口访问就会没有权限
        // 为了防止url接口上没有使用@PreAuthorize注解权限表达式,这里仍然配置permitAll,让白名单url不需要token认证和鉴权也可以访问
        Set<String> whiteUrls = whiteUrlAutoConfiguration.getWhiteUrls();
        if (whiteUrls.size() > 0) {
            String[] urlPatterns = whiteUrls.toArray(new String[0]);
            http.authorizeRequests(authorize -> authorize.antMatchers(urlPatterns).permitAll());
        }
        http.authorizeRequests().anyRequest().authenticated();
        http.formLogin().disable();
        http.httpBasic().disable();
    }

    @Bean
    public GlobalMethodSecurityConfiguration globalMethodSecurityConfiguration() {
        List<MethodSecurityExpressionHandler> handlers = new ArrayList<>(1);
        handlers.add(customMethodSecurityExpressionHandler());
        globalMethodSecurityConfiguration.setMethodSecurityExpressionHandler(handlers);
        return globalMethodSecurityConfiguration;
    }

    @Bean
    public MethodSecurityExpressionHandler customMethodSecurityExpressionHandler() {
        CustomMethodSecurityExpressionHandler expressionHandler = new CustomMethodSecurityExpressionHandler();
        expressionHandler.setWhiteUrlAutoConfiguration(whiteUrlAutoConfiguration);
        return expressionHandler;
    }
}

3. 配置拦截器 CommonWebMvcAutoConfiguration

@Configuration
@EnableWebMvc
public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {

    @Value("${spring.application.name}")
    private String appName;

    @Autowired
    private WhiteUrlAutoConfiguration whiteUrlAutoConfiguration;

    @Bean
    public UserInfoInterceptor userInfoInterceptor() {
        return new UserInfoInterceptor();
    }

    @Override
    public void addInterceptors(@NonNull InterceptorRegistry registry) {
        // 拦截器会拦截所有请求,需要配置放行的请求
        Set<String> whiteUrls = whiteUrlAutoConfiguration.getWhiteUrls();
        if("authority".equals(appName)){
            whiteUrls.add("/api/v1/login");
            whiteUrls.add("/api/v1/token");
        }
        registry.addInterceptor(userInfoInterceptor())
                // 因为白名单url不需要token认证就可以访问,如果不放行,拦截器的preHandle()方法会返回false
                .excludePathPatterns(whiteUrls.toArray(new String[0]));
    }
}

4. knowledge 业务服务置文件中配置白名单 url

@RestController
@RequestMapping("/api/v1")
public class DocController {

	// 用户不具备roleEdit权限,因此如果用户需要鉴权,那么就会返回无权限访问,如果不需要鉴权就会返回doc
    @PreAuthorize("hasAuthority('roleEdit')")
    @GetMapping("/doc")
    public String getDocList(){
        return "doc";
    }
}
# 配置白名单url
chahua:
  white-urls:
    - /api/v1/doc

5. 启动项目测试

不携带 accessToken 访问/aoi/v1/doc

java ip白名单过滤 springsecurity白名单_白名单_11

① 因为没有携带token访问,因此过滤器 OAuth2AuthenticationProcessingFilter 中返回的 authentication=null,代表用户未认证:

java ip白名单过滤 springsecurity白名单_java_12

② 请求进入 @PreAuthorize(“hasAuthority(‘roleEdit’)”) 注解中配置的 hasAuthority 方法

java ip白名单过滤 springsecurity白名单_jvm_13

③ 请求进入重写的 hasAnyAuthorityName 方法,在该方法中会判断请求url是否在白名单url中,如果在就不需要鉴权,直接返回true:

java ip白名单过滤 springsecurity白名单_java ip白名单过滤_14

java ip白名单过滤 springsecurity白名单_jvm_15

③ 请求进入 DocController

java ip白名单过滤 springsecurity白名单_jvm_16

java ip白名单过滤 springsecurity白名单_java_17

至此,我们就实现了白名单 url 不需要 token 认证也不需要鉴权就能访问。