在搭建完 spring-security-oauth2 整个微服务框架后,来了一个需求:
每个微服务都需要对访问进行鉴权,每个微服务应用都需要明确当前访问用户和他的权限。
auth 系统的主要功能是授权认证和鉴权。
授权认证已经完成,那么如何对用户的访问进行鉴权呢?
首先需要明确什么时候发生鉴权?
鉴权发生在用户已经认证后携带了 access_token 信息但还没用访问到目标资源的时候。
知道了鉴权发生的时间,需要明白怎么鉴权?
我的想法是添加一个用于鉴权的过滤器,Spring Security 默认的过滤器链(官网):
别名 |
类名称 |
Namespace Element or Attribute |
CHANNEL_FILTER |
ChannelProcessingFilter |
http/intercept-url@requires-channel |
SECURITY_CONTEXT_FILTER |
SecurityContextPersistenceFilter |
http |
CONCURRENT_SESSION_FILTER |
ConcurrentSessionFilter |
session-management/concurrency-control |
HEADERS_FILTER |
HeaderWriterFilter |
http/headers |
CSRF_FILTER |
CsrfFilter |
http/csrf |
LOGOUT_FILTER |
LogoutFilter |
http/logout |
X509_FILTER |
X509AuthenticationFilter |
http/x509 |
PRE_AUTH_FILTER |
AbstractPreAuthenticatedProcessingFilter( Subclasses) |
N/A |
CAS_FILTER |
CasAuthenticationFilter N/A |
|
FORM_LOGIN_FILTER |
UsernamePasswordAuthenticationFilter |
http/form-login |
BASIC_AUTH_FILTER |
BasicAuthenticationFilter |
http/http-basic |
SERVLET_API_SUPPORT_FILTER |
SecurityContextHolderAwareRequestFilter |
http/@servlet-api-provision |
JAAS_API_SUPPORT_FILTER |
JaasApiIntegrationFilter |
http/@jaas-api-provision |
REMEMBER_ME_FILTER |
RememberMeAuthenticationFilter |
http/remember-me |
ANONYMOUS_FILTER |
AnonymousAuthenticationFilter |
http/anonymous |
SESSION_MANAGEMENT_FILTER |
SessionManagementFilter |
session-management |
EXCEPTION_TRANSLATION_FILTER |
ExceptionTranslationFilter |
http |
FILTER_SECURITY_INTERCEPTOR |
FilterSecurityInterceptor |
http |
SWITCH_USER_FILTER |
SwitchUserFilter |
N/A |
过滤器顺序从上到下
FilterSecurityInterceptor
是 filterchain 中比较复杂,也是比较核心的过滤器,主要负责web应用安全授权的工作。
我想添加的过滤器是添加在 FilterSecurityInterceptor
之后。
Oauth2FilterSecurityInterceptor
是模仿 FilterSecurityInterceptor 实现,继承 AbstractSecurityInterceptor 和实现 Filter 接口。
整个过程需要依赖 AuthenticationManager、AccessDecisionManager 和 FilterInvocationSecurityMetadataSource。
- AuthenticationManager是认证管理器,实现用户认证的入口;
- AccessDecisionManager是访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源;
- FilterInvocationSecurityMetadataSource是资源源数据定义,即定义某一资源可以被哪些角色访问。
自定义鉴权过滤器 Oauth2FilterSecurityInterceptor
的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| package com.fengxuechao.examples.auth.authorization;
import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; import org.springframework.security.access.intercept.InterceptorStatusToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.web.FilterInvocation;
import javax.servlet.*; import java.io.IOException;
@Slf4j public class Oauth2FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
private Oauth2FilterInvocationSecurityMetadataSource securityMetadataSource;
@Override public void init(FilterConfig filterConfig) throws ServletException { if (log.isInfoEnabled()) { log.info("Oauth2FilterSecurityInterceptor init"); } }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (log.isInfoEnabled()) { log.info("Oauth2FilterSecurityInterceptor doFilter"); } FilterInvocation filterInvocation = new FilterInvocation(request, response, chain); invoke(filterInvocation); }
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException { InterceptorStatusToken interceptorStatusToken = super.beforeInvocation(filterInvocation); try { filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()); } finally { super.afterInvocation(interceptorStatusToken, null); } }
@Override public void destroy() {
}
@Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; }
@Override public SecurityMetadataSource obtainSecurityMetadataSource() { return securityMetadataSource; }
public void setOauth2AccessDecisionManager(Oauth2AccessDecisionManager accessDecisionManager) { super.setAccessDecisionManager(accessDecisionManager); }
@Override public void setAuthenticationManager(AuthenticationManager authenticationManager) { super.setAuthenticationManager(authenticationManager); }
public void setSecurityMetadataSource(Oauth2FilterInvocationSecurityMetadataSource securityMetadataSource) { this.securityMetadataSource = securityMetadataSource; } }
|
看下父类的 beforeInvocation
方法,其中省略了一些不重要的代码片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public abstract class AbstractSecurityInterceptor implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware { protected InterceptorStatusToken beforeInvocation(Object object) { Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object);
Authentication authenticated = authenticateIfRequired();
try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));
throw accessDeniedException; }
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| package com.fengxuechao.examples.auth.authorization;
import com.fengxuechao.examples.auth.service.UserRolePermissionService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component;
import java.util.Collection;
@Slf4j @Component public class Oauth2FilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource, InitializingBean {
private UserRolePermissionService service;
public Oauth2FilterInvocationSecurityMetadataSource(UserRolePermissionService service) { this.service = service; }
@Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { if ("/user/profile".equals(((FilterInvocation) object).getRequestUrl())) { return null; }
return SecurityConfig.createList("ROLE_ADMIN"); }
@Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; }
@Override public boolean supports(Class<?> clazz) { return true; }
@Override public void afterPropertiesSet() throws Exception {
} }
|
为了调试的方便,直接定死任何访问请求都需要管理员权限(/user/profile 除外),调试通过后,再往里面添加业务逻辑代码。
自定义决策管理器 Oauth2AccessDecisionManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| package com.fengxuechao.examples.auth.authorization;
import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.FilterInvocation; import org.springframework.stereotype.Component;
import java.util.Collection; import java.util.Iterator;
@Slf4j @Component public class Oauth2AccessDecisionManager implements AccessDecisionManager {
@Override public void decide(Authentication authentication, Object resource, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { log.info("[决策管理器]:开始判断请求 {} 需要的权限", ((FilterInvocation) resource).getRequestUrl()); if (configAttributes == null || configAttributes.isEmpty()) { log.info("[决策管理器]:请求 {} 无需权限", ((FilterInvocation) resource).getRequestUrl()); return; } log.info("[决策管理器]:请求 {} 需要的权限 - {}", ((FilterInvocation) resource).getRequestUrl(), configAttributes); Iterator<ConfigAttribute> ite = configAttributes.iterator(); log.info("[决策管理器]:用户 {} 拥有的权限 - {}", authentication.getName(), authentication.getAuthorities()); while (ite.hasNext()) { ConfigAttribute neededAuthority = ite.next(); String neededAuthorityStr = neededAuthority.getAttribute(); for (GrantedAuthority existingAuthority : authentication.getAuthorities()) { if (neededAuthorityStr.equals(existingAuthority.getAuthority())) { return; } } } log.info("[决策管理器]:用户 {} 没有访问资源 {} 的权限!", authentication.getName(), ((FilterInvocation) resource).getRequestUrl()); throw new AccessDeniedException("权限不足!"); }
@Override public boolean supports(ConfigAttribute attribute) { return true; }
@Override public boolean supports(Class<?> clazz) { return true; } }
|
配置自定义鉴权过滤器 Oauth2FilterSecurityInterceptor
在 Spring Security 过滤器链中的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| package com.fengxuechao.examples.auth.config;
import com.fengxuechao.examples.auth.authorization.Oauth2AccessDecisionManager; import com.fengxuechao.examples.auth.authorization.Oauth2FilterInvocationSecurityMetadataSource; import com.fengxuechao.examples.auth.authorization.Oauth2FilterSecurityInterceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@Slf4j @EnableResourceServer @Configuration public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired AuthenticationManager manager;
@Autowired Oauth2AccessDecisionManager accessDecisionManager;
@Autowired Oauth2FilterInvocationSecurityMetadataSource securityMetadataSource;
@Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated(); http.addFilterAfter(createApiAuthenticationFilter(), FilterSecurityInterceptor.class); }
private Oauth2FilterSecurityInterceptor createApiAuthenticationFilter() { Oauth2FilterSecurityInterceptor interceptor = new Oauth2FilterSecurityInterceptor(); interceptor.setAuthenticationManager(manager); interceptor.setAccessDecisionManager(accessDecisionManager); interceptor.setSecurityMetadataSource(securityMetadataSource); return interceptor; } }
|
配置用户权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.fengxuechao.examples.auth.userdetails;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component;
@Component public class UserDetailsServiceImpl implements UserDetailsService {
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); } }
|
演示结果
用户拥有资源所需权限
请求:
request1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| GET http://localhost:8080/order/1
HTTP/1.1 200 X-Application-Context: application:inMemory X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: text/plain;charset=UTF-8 Content-Length: 12 Date: Tue, 18 Jun 2019 01:50:48 GMT
order id : 1
Response code: 200; Time: 57ms; Content length: 12 bytes
|
日志:
1 2 3 4 5 6 7 8
| 2019-06-18 09:50:48.955 INFO 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : Oauth2FilterSecurityInterceptor doFilter 2019-06-18 09:50:48.955 DEBUG 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /order/1; Attributes: [ROLE_USER] 2019-06-18 09:50:48.956 DEBUG 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.oauth2.provider.OAuth2Authentication@f5aeefea: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=127.0.0.1, tokenType=bearertokenValue=<TOKEN>; Granted Authorities: ROLE_USER 2019-06-18 09:50:48.956 INFO 5288 --- [nio-8080-exec-3] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:开始判断请求 /order/1 需要的权限 2019-06-18 09:50:48.956 INFO 5288 --- [nio-8080-exec-3] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:请求 /order/1 需要的权限 - [ROLE_USER] 2019-06-18 09:50:48.956 INFO 5288 --- [nio-8080-exec-3] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:用户 user 拥有的权限 - [ROLE_USER] 2019-06-18 09:50:48.956 DEBUG 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : Authorization successful 2019-06-18 09:50:48.957 DEBUG 5288 --- [nio-8080-exec-3] .f.e.a.a.Oauth2FilterSecurityInterceptor : RunAsManager did not change Authentication object
|
用户没有资源所需权限
请求:
request1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| GET http://localhost:8080/order/1
HTTP/1.1 403 Cache-Control: no-store Pragma: no-cache X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block X-Frame-Options: DENY Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Tue, 18 Jun 2019 01:44:49 GMT
{ "error": "access_denied", "error_description": "权限不足!" }
Response code: 403; Time: 35ms; Content length: 53 bytes
|
日志:
1 2 3 4 5 6 7 8 9
| 2019-06-18 09:44:44.684 INFO 10624 --- [nio-8080-exec-2] .f.e.a.a.Oauth2FilterSecurityInterceptor : Oauth2FilterSecurityInterceptor doFilter 2019-06-18 09:44:44.685 DEBUG 10624 --- [nio-8080-exec-2] .f.e.a.a.Oauth2FilterSecurityInterceptor : Public object - authentication not attempted 2019-06-18 09:44:49.448 INFO 10624 --- [nio-8080-exec-6] .f.e.a.a.Oauth2FilterSecurityInterceptor : Oauth2FilterSecurityInterceptor doFilter 2019-06-18 09:44:49.449 DEBUG 10624 --- [nio-8080-exec-6] .f.e.a.a.Oauth2FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /order/1; Attributes: [ROLE_ADMIN] 2019-06-18 09:44:49.449 DEBUG 10624 --- [nio-8080-exec-6] .f.e.a.a.Oauth2FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.oauth2.provider.OAuth2Authentication@22d262ad: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=127.0.0.1, tokenType=bearertokenValue=<TOKEN>; Granted Authorities: ROLE_USER 2019-06-18 09:44:49.450 INFO 10624 --- [nio-8080-exec-6] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:开始判断请求 /order/1 需要的权限 2019-06-18 09:44:49.450 INFO 10624 --- [nio-8080-exec-6] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:请求 /order/1 需要的权限 - [ROLE_ADMIN] 2019-06-18 09:44:49.450 INFO 10624 --- [nio-8080-exec-6] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:用户 user 拥有的权限 - [ROLE_USER] 2019-06-18 09:44:49.451 INFO 10624 --- [nio-8080-exec-6] c.f.e.a.a.Oauth2AccessDecisionManager : [决策管理器]:用户 user 没有访问资源 /order/1 的权限!
|
返回结果和日志符合期望结果
参考资源
http://www.spring4all.com/article/422