SpringSecurity问题分析之AccessDeniedException: Access is denied 源码分析

问题描述

org.springframework.security.access.AccessDeniedException: Access is denied
2021-04-10 16:17:01.950 DEBUG [authorization-server,8c5f48650de0f659,83580d93d6acb342,false] 7328 — [nio-8000-exec-1] o.s.s.w.u.matcher.AntPathRequestMatcher : Checking match of request : ‘/oauth/token’; against ‘/oauth/token’
2021-04-10 16:17:01.950 DEBUG [authorization-server,8c5f48650de0f659,83580d93d6acb342,false] 7328 — [nio-8000-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor : Secure object: FilterInvocation: URL: /oauth/token?scope=read&grant_type=password; Attributes: [fullyAuthenticated]
2021-04-10 16:17:01.951 DEBUG [authorization-server,8c5f48650de0f659,83580d93d6acb342,false] 7328 — [nio-8000-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@a50fab08: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@7798: RemoteIpAddress: 10.68.23.139; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
2021-04-10 16:17:01.953 DEBUG [authorization-server,8c5f48650de0f659,83580d93d6acb342,false] 7328 — [nio-8000-exec-1] o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@1bbd303b, returned: -1


注意到倒数第三行,在config中配置的时候我们已经将所有权限配置为permitAll,但是仍旧出现这个问题
 http.csrf().disable();
        http.authorizeRequests()
//                .anyRequest().permitAll();
                .antMatchers("/**").permitAll()
//                .antMatchers("/actuator/**").permitAll()
//                .antMatchers("/oauth/**").permitAll()
//                .anyRequest().authenticated()
                .and()
                .cors();

这个问题出现的场景是当前端获取token并存于vue的store中时,当在请求头中携带有token再次请求登陆接口时报错,通过源码分析产生问题的原因

首先进到decide方法
at org.springframework.security.access.vote.AffirmativeBased.decide
 public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;
        Iterator var5 = this.getDecisionVoters().iterator();

        while(var5.hasNext()) {
            AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
            int result = voter.vote(authentication, object, configAttributes);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Voter: " + voter + ", returned: " + result);
            }

            switch(result) {
            case -1:
                ++deny;
                break;
            case 1:
                return;
            }
        }

        if (deny > 0) {
            throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
            //就是这里抛出的异常
        } else {
            this.checkAllowIfAllAbstainDecisions();
        }
    }

从这里我们可以看到,是通过DecisionVoters()的vote方法进行投票判断是否有权限,如果一个voter返回-1,那么返回错误,来看一下voter是什么
public interface AccessDecisionVoter<S> {
    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = -1;

    boolean supports(ConfigAttribute var1);

    boolean supports(Class<?> var1);

    int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}

我们可以看到voter是一个抽象类接口,分别有有权限、弃权(避开)和ACCESS_DENIED三种状态

在这里插入图片描述
进来看到第一个实现类RoleVoter

public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if (authentication == null) {
            return -1;
        } else {
            int result = 0;
            Collection<? extends GrantedAuthority> authorities = this.extractAuthorities(authentication);
            Iterator var6 = attributes.iterator();

            while(true) {
                ConfigAttribute attribute;
                //这是一个存储着SpringConfig配置对象类,内部有一个静态list列表存储设置对象
                do {
                    if (!var6.hasNext()) {
                        return result;
                        //到达最后一个,返回0
                    }

                    attribute = (ConfigAttribute)var6.next();
                    //循环
                } while(!this.supports(attribute));

                result = -1;
                Iterator var8 = authorities.iterator();

                while(var8.hasNext()) {
                    GrantedAuthority authority = (GrantedAuthority)var8.next();
                    if (attribute.getAttribute().equals(authority.getAuthority())) {
                        return 1;
                    }
                }
            }
        }
    }
//判断角色是否以ROLE_开头,即从数据库/配置文件中读取的角色列表
public boolean supports(ConfigAttribute attribute) {
        return attribute.getAttribute() != null && attribute.getAttribute().startsWith(this.getRolePrefix());
    }
        

这里的逻辑大概是每次从配置文件中读取一个符合格式的ROlE,即可以访问该URL的ROLE角色,然后从当前角色Authority列表中遍历,如果找到符合角色直接返回1;如果到达最后一个,仍未找到符合角色对象,那么返回-1,退出循环

public abstract class AbstractAccessDecisionManager implements AccessDecisionManager, InitializingBean, MessageSourceAware {
    protected final Log logger = LogFactory.getLog(this.getClass());
    private List<AccessDecisionVoter<? extends Object>> decisionVoters;
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private boolean allowIfAllAbstainDecisions = false;

回到最前面的decide方法,我们可以知道AbstractAccessDecisionManager中维护一个decisionVoters列表,来判断当前访问的url是否有权限,不仅仅包括角色,同时还有其他的Voter实现类

我们进到判断权限的类

Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) {
        return authentication.getAuthorities();
    }
public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

Authentication实现了Principal接口,存储着用户的一系列权限,还可以设置授权状态
看他的实现类

public abstract class AbstractAuthenticationToken{
	public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString()).append(": ");
        sb.append("Principal: ").append(this.getPrincipal()).append("; ");
        sb.append("Credentials: [PROTECTED]; ");
        sb.append("Authenticated: ").append(this.isAuthenticated()).append("; ");
        sb.append("Details: ").append(this.getDetails()).append("; ");
        if (!this.authorities.isEmpty()) {
            sb.append("Granted Authorities: ");
            int i = 0;

            GrantedAuthority authority;
            for(Iterator var3 = this.authorities.iterator(); var3.hasNext(); sb.append(authority)) {
                authority = (GrantedAuthority)var3.next();
                if (i++ > 0) {
                    sb.append(", ");
                }
            }
        } else {
            sb.append("Not granted any authorities");
        }

        return sb.toString();
    }
}

直接看到toString方法!!Granted Authorities: ROLE_ANONYMOUS我们可以直接看到就是这里打出来的匿名用户消息,胜利就在前方!

看到这个抽象类的实现类,直接看到OAuthAuthentication

public class OAuth2Authentication extends AbstractAuthenticationToken {
    private static final long serialVersionUID = -4809832298438307309L;
    private final OAuth2Request storedRequest;
    private final Authentication userAuthentication;

    public OAuth2Authentication(OAuth2Request storedRequest, Authentication userAuthentication) {
    //构造函数,传入Oath2request和当前用户信息,Security把登陆请求的token存储在Oauth2Request中
        super(userAuthentication == null ? storedRequest.getAuthorities() : userAuthentication.getAuthorities());
        this.storedRequest = storedRequest;
        this.userAuthentication = userAuthentication;
    }

    public Object getCredentials() {
        return "";
    }

    public Object getPrincipal() {
        return this.userAuthentication == null ? this.storedRequest.getClientId() : this.userAuthentication.getPrincipal();
    }

    public boolean isClientOnly() {
        return this.userAuthentication == null;
    }

    public OAuth2Request getOAuth2Request() {
        return this.storedRequest;
    }

    public Authentication getUserAuthentication() {
        return this.userAuthentication;
    }

    public boolean isAuthenticated() {
        return this.storedRequest.isApproved() && (this.userAuthentication == null || this.userAuthentication.isAuthenticated());
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        if (this.userAuthentication != null && CredentialsContainer.class.isAssignableFrom(this.userAuthentication.getClass())) {
            ((CredentialsContainer)CredentialsContainer.class.cast(this.userAuthentication)).eraseCredentials();
        }

    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (!(o instanceof OAuth2Authentication)) {
        //传入的是不是OAUTH2Authentication接口实现类。如果是,继续
            return false;
        } else if (!super.equals(o)) {
        //调用当前父类equals方法,父类方法是判断两个token是否相等
            return false;
        } else {
            OAuth2Authentication that = (OAuth2Authentication)o;
            if (!this.storedRequest.equals(that.storedRequest)) {
                return false;
            } else {
                label33: {
                    if (this.userAuthentication != null) {
                        if (this.userAuthentication.equals(that.userAuthentication)) {
                            break label33;
                        }
                    } else if (that.userAuthentication == null) {
                        break label33;
                    }

                    return false;
                }
//先判断userAuthentication是否相等,在判断detail是否相等,如果都相等再返回true
                if (this.getDetails() != null) {
                    if (!this.getDetails().equals(that.getDetails())) {
                    }
                } else if (that.getDetails() != null) {
                }

                return true;
            }
        }
    }

    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + this.storedRequest.hashCode();
        result = 31 * result + (this.userAuthentication != null ? this.userAuthentication.hashCode() : 0);
        return result;
    }
}

看完这里还是没有看见匿名用户在哪。。。先看看这个Authentication是从哪来的
在类 AbstractSecurityInterceptor中找到一段

Authentication authenticated = this.authenticateIfRequired();

private Authentication authenticateIfRequired() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Previously Authenticated: " + authentication);
            }

            return authentication;
        } else {
            authentication = this.authenticationManager.authenticate(authentication);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Successfully Authenticated: " + authentication);
            }

            SecurityContextHolder.getContext().setAuthentication(authentication);
            return authentication;
        }
    }

原来是从SpringContextHolder里来的,由spring管理的threadlocal存储的用户登陆状态,在用户发送请求时候全局进行存储的,通过用户发送的token解析出登陆状态交由spring管理,我这里之前打印出了 this.logger.debug("Previously Authenticated: " + authentication);
说明是已经通过token解析了我的登陆信息了,也就是说我带来了token,没有过期,但是我当前访问的url我没有权限,也就是/oauth/**没有权限,但是不带token是可以正常访问接口的。。。

service.interceptors.request.use(config => {
  // if (!(typeof(store.getters.token)=='undefined')) {
  //   config.headers['Authorization'] = 'Bearer '+getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  // }
  加上上面一段后,当有token时候不能访问登陆接口,我的需求是返回用户已经登陆了
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

未完待续。。。。

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉默终止

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值