相关文章:
SpringSecurity Oauth2实战 - 08 SpEL权限表达式源码分析及两种权限控制方式原理
在前面文章中,我们分析了权限表达式的实现原理,并通过 debug 看了 url 权限表达式和注解权限表达式的调用过程,最终看到 url 权限表达式会在注解权限表达式之前执行,那么如果我们在资源服务器配置类 ResourceServerAutoConfiguration中配置了 permitAll 权限表达式,在方法注解中配置了 hasAuthority 权限表达式会怎么样呢?
@Slf4j
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {@Autowiredprivate TokenStore tokenStore;@Value("${spring.application.name}")private String appName;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) {resources.resourceId(appName);resources.tokenStore(tokenStore);resources.tokenExtractor(tokenExtractor());}@Bean@Primarypublic TokenExtractor tokenExtractor() {CustomTokenExtractor customTokenExtractor = new CustomTokenExtractor();return customTokenExtractor;}@Overridepublic 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();}
}
@RestController
@RequestMapping("/api/v1")
public class DocController {@PreAuthorize("hasAuthority('roleEdit')")@GetMapping("/doc")public String getDocList(){return "doc";}
}
用户具有的权限有:userEdit、superAdmin、knowledgeQuery、userQuery、knowledgeEdit
antMatchers("/api/v1/doc").permitAll()
:指定用户不需要授权就可以访问/api/v1/doc
;
@PreAuthorize("hasAnyAuthority('roleEdit')")
:指定用户具备roleEdit
权限才能访问 /api/v1/doc
;
因为 url 权限表达式会在注解权限表达式之前执行,因此将以注解权限表达式为准,用户没有权限访问;
① 请求进入过滤器 OAuth2AuthenticationProcessingFilter 获取用户的认证信息
② 因为该请求路径api/v1/doc
没有在拦截器配置类中放行,因此请求在进入Controller层方法之前会被拦截器拦截,在拦截器中判断用户是否已经认证,如果用户没有认证将不会放行。
@Configuration
@EnableWebMvc
public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {@Beanpublic UserInfoInterceptor userInfoInterceptor() {return new UserInfoInterceptor();}@Overridepublic void addInterceptors(@NonNull InterceptorRegistry registry) {registry.addInterceptor(userInfoInterceptor())// 放行的请求.excludePathPatterns("/api/v1/login");}
}
③ 请求进入SecurityExpressionRoot类的 hasAuthority 方法而不是permitAll方法,说明最终请求会以注解表达式中配置的为准。
如果想要白名单不需要token认证:
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "chahua")
@EnableConfigurationProperties
public class WhiteUrlAutoConfiguration implements InitializingBean {/*** 白名单url:不需要token认证*/private Set whiteUrls = new HashSet<>();@Overridepublic void afterPropertiesSet() {if (whiteUrls != null && whiteUrls.size() > 0) {log.info("Load {} succeed: {}", "white-urls.yml", String.join(", ", whiteUrls));}}
}
@Slf4j
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {@Autowiredprivate WhiteUrlAutoConfiguration whiteUrlAutoConfiguration;@Autowiredprivate TokenStore tokenStore;@Value("${spring.application.name}")private String appName;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) {resources.resourceId(appName);resources.tokenStore(tokenStore);resources.tokenExtractor(tokenExtractor());}@Bean@Primarypublic TokenExtractor tokenExtractor() {CustomTokenExtractor customTokenExtractor = new CustomTokenExtractor();return customTokenExtractor;}@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/api/v1/login", "/api/v1/token").permitAll();// 配置白名单url不需要token认证和授权就可以访问Set 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();}
}
public class UserInfoInterceptor extends HandlerInterceptorAdapter {/*** 拦截所有请求,在Controller层方法之前调用*/@Overridepublic 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;}@Overridepublic 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;@Autowiredprivate WhiteUrlAutoConfiguration whiteUrlAutoConfiguration;@Beanpublic UserInfoInterceptor userInfoInterceptor() {return new UserInfoInterceptor();}@Overridepublic void addInterceptors(@NonNull InterceptorRegistry registry) {// 拦截器会拦截所有请求,需要配置放行的请求Set 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]));}
}
@RestController
@RequestMapping("/api/v1")
public class DocController {@GetMapping("/doc")public String getDocList(){return "doc";}
}
配置文件中配置白名单 url :
# 配置白名单url
chahua:white-urls:- /api/v1/doc
不携带 accessToken 访问/aoi/v1/doc
:
① 请求进入过滤器 OAuth2AuthenticationProcessingFilter,可以看到 accessToken=null,authentication=null:
② 请求进入 DocController 类的 getDocList 方法:
因为 ResourceServerAutoConfiguration 类中配置了资源不需要认证就可以访问,且拦截器配置了资源放行,因此请求直接进入了DocController中。
我们知道除了 http.authorizeRequests(authorize -> authorize.antMatchers(urlPatterns).permitAll()) 不做校验,所有的请求都会走到@PreAuthorize注解对应的方法里面,所以如果我们的请求url方法上配置了@PreAuthorize注解,即使在ResourceServerAutoConfiguration 配置了permitAll,仍然会进行鉴权。
在 @PreAuthorize 注解中常用的 hasAuthority、hasPermission、hasRole、hasAnyRole 都是由 SecurityExpressionRoot 类提供的,且他们都调用了 SecurityExpressionRoot 类的 hasAnyAuthorityName 方法完成鉴权,因此我们理论上只需要实现一个自定义权限表达式类继承 SecurityExpressionRoot 类并重写该类的hasAnyAuthorityName 方法即可。但是该方法是私有的,子类无法重写。因此我们可以直接自定义一个自定义权限表达式类但是不继承SecurityExpressionRoot 类,而是直接在该类中实现一个SecurityExpressionRoot 类的功能。
public class CustomMethodSecurityExpressionRoot implements MethodSecurityExpressionOperations{@Setterprivate RequestMatcher requestMatcher;/*** MethodSecurityExpressionOperations 接口方法的属性*/private Object filterObject;private Object returnObject;private Object target;/*** SecurityExpressionRoot 类中的属性*/protected Authentication authentication;private AuthenticationTrustResolver trustResolver;private RoleHierarchy roleHierarchy;private Set 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,直接返回truereturn true;}return false;}/*** 修改 SecurityExpressionRoot 类中的该方法** 登录请求url是否是白名单WhiteUrl,如果是则不需要校验权限,直接返回true*/private boolean hasAnyAuthorityName(String prefix, String... roles) {// 如果是白名单WhiteUrl或者公共CommonUrl,则不需要校验权限,直接返回trueif (isWhiteUrl()) {return true;}Set 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;}@Overridepublic Authentication getAuthentication() {return authentication;}// 判断当前用户具备的权限信息,是否存在指定权限@Overridepublic final boolean hasAuthority(String authority) {return hasAnyAuthority(authority);}// 判断当前用户具备的权限信息,是否存在指定权限中的任意一个@Overridepublic final boolean hasAnyAuthority(String... authorities) {return hasAnyAuthorityName(null, authorities);}// 判断当前用户具备的权限信息,是否存在指定角色@Overridepublic final boolean hasRole(String role) {return hasAnyRole(role);}// 判断当前用户具备的权限信息,是否存在指定角色中的任意一个@Overridepublic final boolean hasAnyRole(String... roles) {return hasAnyAuthorityName(defaultRolePrefix, roles);}// 允许所有的请求调用@Overridepublic final boolean permitAll() {return true;}// 拒绝所有的请求调用@Overridepublic final boolean denyAll() {return false;}// 当前用户是否是一个匿名用户@Overridepublic final boolean isAnonymous() {return trustResolver.isAnonymous(authentication);}// 判断用户是否已经认证成功@Overridepublic final boolean isAuthenticated() {return !isAnonymous();}// 当前用户是否通过RememberMe自动登录@Overridepublic final boolean isRememberMe() {return trustResolver.isRememberMe(authentication);}// 当前登录用户是否既不是匿名用户又不是通过RememberMe登录的@Overridepublic 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;}// 当前登录用户是否具有指定目标的指定权限@Overridepublic boolean hasPermission(Object target, Object permission) {return permissionEvaluator.hasPermission(authentication, target, permission);}// 当前登录用户是否具有指定目标的指定权限@Overridepublic boolean hasPermission(Object targetId, String targetType, Object permission) {return permissionEvaluator.hasPermission(authentication, (Serializable) targetId, targetType, permission);}private Set 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 接口中的实现方法,没有更改*/@Overridepublic void setFilterObject(Object filterObject) {this.filterObject = filterObject;}@Overridepublic Object getFilterObject() {return this.filterObject;}@Overridepublic void setReturnObject(Object returnObject) {this.returnObject = returnObject;}@Overridepublic Object getReturnObject() {return this.returnObject;}void setThis(Object target) {this.target = target;}@Overridepublic Object getThis() {return target;}
}
@Slf4j
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerAutoConfiguration extends ResourceServerConfigurerAdapter {/*** 权限表达式的自定义处理*/@Autowiredprivate GlobalMethodSecurityConfiguration globalMethodSecurityConfiguration;@Autowiredprivate WhiteUrlAutoConfiguration whiteUrlAutoConfiguration;@Autowiredprivate TokenStore tokenStore;@Value("${spring.application.name}")private String appName;@Overridepublic void configure(ResourceServerSecurityConfigurer resources) {resources.resourceId(appName);resources.tokenStore(tokenStore);resources.tokenExtractor(tokenExtractor());}@Bean@Primarypublic TokenExtractor tokenExtractor() {CustomTokenExtractor customTokenExtractor = new CustomTokenExtractor();return customTokenExtractor;}@Overridepublic 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 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();}@Beanpublic GlobalMethodSecurityConfiguration globalMethodSecurityConfiguration() {List handlers = new ArrayList<>(1);handlers.add(customMethodSecurityExpressionHandler());globalMethodSecurityConfiguration.setMethodSecurityExpressionHandler(handlers);return globalMethodSecurityConfiguration;}@Beanpublic MethodSecurityExpressionHandler customMethodSecurityExpressionHandler() {CustomMethodSecurityExpressionHandler expressionHandler = new CustomMethodSecurityExpressionHandler();expressionHandler.setWhiteUrlAutoConfiguration(whiteUrlAutoConfiguration);return expressionHandler;}
}
@Configuration
@EnableWebMvc
public class CommonWebMvcAutoConfiguration implements WebMvcConfigurer {@Value("${spring.application.name}")private String appName;@Autowiredprivate WhiteUrlAutoConfiguration whiteUrlAutoConfiguration;@Beanpublic UserInfoInterceptor userInfoInterceptor() {return new UserInfoInterceptor();}@Overridepublic void addInterceptors(@NonNull InterceptorRegistry registry) {// 拦截器会拦截所有请求,需要配置放行的请求Set 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]));}
}
@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
不携带 accessToken 访问/aoi/v1/doc
:
① 因为没有携带token访问,因此过滤器 OAuth2AuthenticationProcessingFilter 中返回的 authentication=null,代表用户未认证:
② 请求进入 @PreAuthorize(“hasAuthority(‘roleEdit’)”) 注解中配置的 hasAuthority 方法
③ 请求进入重写的 hasAnyAuthorityName 方法,在该方法中会判断请求url是否在白名单url中,如果在就不需要鉴权,直接返回true:
③ 请求进入 DocController
至此,我们就实现了白名单 url 不需要 token 认证也不需要鉴权就能访问。