文章目录
- 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 权限表达式会在注解权限表达式之前执行,因此将以注解权限表达式为准,用户没有权限访问;
① 请求进入过滤器 OAuth2AuthenticationProcessingFilter 获取用户的认证信息
② 因为该请求路径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");
}
}
③ 请求进入SecurityExpressionRoot类的 hasAuthority 方法而不是permitAll方法,说明最终请求会以注解表达式中配置的为准。
2. 配置白名单url不需要token认证
如果想要白名单不需要token认证:
- 在 ResourceServerAutoConfiguration 中配置白名单url的权限表达式为 permitAll,代表不需要accessToken认证也能访问,不做任何鉴权。
- 在 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
:
① 请求进入过滤器 OAuth2AuthenticationProcessingFilter,可以看到 accessToken=null,authentication=null:
② 请求进入 DocController 类的 getDocList 方法:
因为 ResourceServerAutoConfiguration 类中配置了资源不需要认证就可以访问,且拦截器配置了资源放行,因此请求直接进入了DocController中。
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
:
① 因为没有携带token访问,因此过滤器 OAuth2AuthenticationProcessingFilter 中返回的 authentication=null,代表用户未认证:
② 请求进入 @PreAuthorize(“hasAuthority(‘roleEdit’)”) 注解中配置的 hasAuthority 方法
③ 请求进入重写的 hasAnyAuthorityName 方法,在该方法中会判断请求url是否在白名单url中,如果在就不需要鉴权,直接返回true:
③ 请求进入 DocController
至此,我们就实现了白名单 url 不需要 token 认证也不需要鉴权就能访问。