• spring security 用户授权原理


    目录

    1、授权步骤

    2、基于Filter的授权流程

    3、授权源码分析

    4、AccessDecisionManager 的三种实现


    1、授权步骤

    (1)URL 权限控制:调用 HttpSecurity#authorizeRequests() 方法,开始配置 URL 的权限控制

    1. @Override
    2. protected void configure(HttpSecurity http) throws Exception {
    3. http.formLogin();//表单提交
    4. // 授权 -> 认证拦截
    5. http.authorizeRequests()
    6. .antMatchers("/login.html", "/error.html", "/main.html").permitAll()
    7. .antMatchers("/admin/test").hasAnyAuthority("admin")
    8. .anyRequest().authenticated();// 所有请求都必须认证
    9. http.csrf().disable(); //关闭csrf防护
    10. }

    (2)方法权限控制:修改 WebSecurityConfig配置类,增加 @EnableGlobalMethodSecurity 注解,开启对 Spring Security 注解的方法,进行权限验证。

    1. @Configuration // 标记为注解类
    2. @EnableGlobalMethodSecurity(prePostEnabled = true)
    3. public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    4. }

    2、基于Filter的授权流程

    FilterSecurityInterceptor 进行资源拦截,当拦截资源为方法时,还会进入到拦截器MethodSecurityInterceptor,它们的继承关系如下图

    授权流程如下

    1.拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的FilterSecurityInterceptor 的子类拦截。

    2. 获取资源访问策略,FilterSecurityInterceptor 会从 SecurityMetadataSource 的子类DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限Collection。SecurityMetadataSource其实就是读取访问策略的抽象,读取的内容就是我们配置的访问规则。

    3. 最后,FilterSecurityInterceptor 会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。

    3、授权源码分析

    进入 FilterSecurityInterceptor,进入 FilterSecurityInterceptor#doFilter 方法

    1. public void doFilter(ServletRequest request, ServletResponse response,
    2. FilterChain chain) throws IOException, ServletException {
    3. FilterInvocation fi = new FilterInvocation(request, response, chain);
    4. // 从 invoke 开始
    5. invoke(fi);
    6. }

    然后进入 invoke 方法

    1. public void invoke(FilterInvocation fi) throws IOException, ServletException {
    2. if ((fi.getRequest() != null)
    3. && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
    4. && observeOncePerRequest) {
    5. // filter already applied to this request and user wants us to observe
    6. // once-per-request handling, so don't re-do security checking
    7. // 避免重新进行安全检查
    8. fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    9. }
    10. else {
    11. // first time this request being called, so perform security checking
    12. // 第一次调用此请求时,执行安全检查
    13. if (fi.getRequest() != null && observeOncePerRequest) {
    14. fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
    15. }
    16. // 通过这个方法进行权限验证
    17. InterceptorStatusToken token = super.beforeInvocation(fi);
    18. try {
    19. fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    20. }
    21. finally {
    22. super.finallyInvocation(token);
    23. }
    24. super.afterInvocation(token, null);
    25. }
    26. }

    进入 AbstractSecurityInterceptor#beforeInvocation,在该方法中,首先会去获取当前访问资源的权限,然后再进行权限验证的投票决策

    1. protected InterceptorStatusToken beforeInvocation(Object object) {
    2. Assert.notNull(object, "Object was null");
    3. final boolean debug = logger.isDebugEnabled();
    4. if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
    5. throw new IllegalArgumentException(
    6. "Security invocation attempted for object "
    7. + object.getClass().getName()
    8. + " but AbstractSecurityInterceptor only configured to support secure objects of type: "
    9. + getSecureObjectClass());
    10. }
    11. // 1-获取访问资源的权限->资源是authenticated,还是permitAll
    12. Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
    13. .getAttributes(object);
    14. if (attributes == null || attributes.isEmpty()) {
    15. if (rejectPublicInvocations) {
    16. throw new IllegalArgumentException(
    17. "Secure object invocation "
    18. + object
    19. + " was denied as public invocations are not allowed via this interceptor. "
    20. + "This indicates a configuration error because the "
    21. + "rejectPublicInvocations property is set to 'true'");
    22. }
    23. if (debug) {
    24. logger.debug("Public object - authentication not attempted");
    25. }
    26. publishEvent(new PublicInvocationEvent(object));
    27. return null; // no further work post-invocation
    28. }
    29. if (debug) {
    30. logger.debug("Secure object: " + object + "; Attributes: " + attributes);
    31. }
    32. if (SecurityContextHolder.getContext().getAuthentication() == null) {
    33. credentialsNotFound(messages.getMessage(
    34. "AbstractSecurityInterceptor.authenticationNotFound",
    35. "An Authentication object was not found in the SecurityContext"),
    36. object, attributes);
    37. }
    38. // 2-获取登陆用户的权限
    39. Authentication authenticated = authenticateIfRequired();
    40. // Attempt authorization
    41. try {
    42. // 3-核心投票方法:尝试进行权限验证
    43. this.accessDecisionManager.decide(authenticated, object, attributes);
    44. }
    45. catch (AccessDeniedException accessDeniedException) {
    46. publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
    47. accessDeniedException));
    48. throw accessDeniedException;
    49. }
    50. if (debug) {
    51. logger.debug("Authorization successful");
    52. }
    53. if (publishAuthorizationSuccess) {
    54. publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    55. }
    56. // Attempt to run as a different user
    57. Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
    58. attributes);
    59. if (runAs == null) {
    60. if (debug) {
    61. logger.debug("RunAsManager did not change Authentication object");
    62. }
    63. // no further work post-invocation
    64. return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
    65. attributes, object);
    66. }
    67. else {
    68. if (debug) {
    69. logger.debug("Switching to RunAs Authentication: " + runAs);
    70. }
    71. SecurityContext origCtx = SecurityContextHolder.getContext();
    72. SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
    73. SecurityContextHolder.getContext().setAuthentication(runAs);
    74. // need to revert to token.Authenticated post-invocation
    75. return new InterceptorStatusToken(origCtx, true, attributes, object);
    76. }
    77. }

    进入权限验证决策方法,AccessDecisionManager#decide,AccessDecisionManager 有三个实现,默认使用 AffirmativeBased,其中两种决策方式,文章后边也会介绍。此处,我们先看第一个

    进入 AffirmativeBased#decide 方法,我们去看它的决策实现

    1. public void decide(Authentication authentication, Object object,
    2. Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    3. int deny = 0;
    4. // 对权限进行循环遍历
    5. for (AccessDecisionVoter voter : getDecisionVoters()) {
    6. // 获取投票结果
    7. int result = voter.vote(authentication, object, configAttributes);
    8. if (logger.isDebugEnabled()) {
    9. logger.debug("Voter: " + voter + ", returned: " + result);
    10. }
    11. switch (result) {
    12. case AccessDecisionVoter.ACCESS_GRANTED://同意返回 -> 跳出循环
    13. return;
    14. case AccessDecisionVoter.ACCESS_DENIED: //拒绝
    15. deny++; // 统计拒绝次数
    16. break; // 继续下一次循环
    17. default:
    18. break; // 弃权 -> 继续下一次循环
    19. }
    20. }
    21. // 如果统计拒绝次数 > 0
    22. if (deny > 0) {
    23. throw new AccessDeniedException(messages.getMessage(
    24. "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    25. }
    26. // 如果每个投票者都选择放弃,默认也会抛出 AccessDeniedException
    27. // private boolean allowIfAllAbstainDecisions = false; 是否允许放弃决策,默认flase
    28. checkAllowIfAllAbstainDecisions();
    29. }

    至此,投票决策逻辑分析完成。

    4、AccessDecisionManager 的三种实现

    AccessDecisionManager 采用投票的方式来确定是否能够访问受保护资源。 AccessDecisionManager 中包含的一系列 AccessDecisionVoter 将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。

    1. // 做出最终的访问控制(授权)决策。
    2. public interface AccessDecisionManager {
    3. // ~ Methods
    4. // 为传递的参数解析访问控制决策。
    5. // authentication 调用方法的调用者(非空)
    6. // object 对象被调用的受保护对象
    7. // configAttributes 与被调用的受保护对象关联的配置属性
    8. void decide(Authentication authentication, Object object,
    9. Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
    10. InsufficientAuthenticationException;
    11. // 指示此AccessDecisionManager是否能够处理传入的ConfigAttribute。
    12. boolean supports(ConfigAttribute attribute);
    13. // 指示AccessDecisionManager实现是否能够为指定的受保护对象类型提供访问控制决策。
    14. boolean supports(Class<?> clazz);
    15. }

    AffirmativeBased

    如果有 AccessDecisionVoter 的投票为 ACCESS_GRANTED 则同意用户进行访问,但如果统计到反对票,则将抛出AccessDeniedException。

    1. public void decide(Authentication authentication, Object object,
    2. Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    3. int deny = 0;
    4. // 获取投票结果
    5. for (AccessDecisionVoter voter : getDecisionVoters()) {
    6. int result = voter.vote(authentication, object, configAttributes);
    7. if (logger.isDebugEnabled()) {
    8. logger.debug("Voter: " + voter + ", returned: " + result);
    9. }
    10. switch (result) {
    11. case AccessDecisionVoter.ACCESS_GRANTED: // 匹配到同意票,直接返回
    12. return;
    13. case AccessDecisionVoter.ACCESS_DENIED: // 决绝
    14. deny++;
    15. break;
    16. default:
    17. break;
    18. }
    19. }
    20. // 统计到拒绝票,抛出异常
    21. if (deny > 0) {
    22. throw new AccessDeniedException(messages.getMessage(
    23. "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    24. }
    25. // 全体弃票,抛出异常
    26. checkAllowIfAllAbstainDecisions();
    27. }

    Spring security默认使用的是AffirmativeBased。

    ConsensusBased(少数服从多数)

    如果赞成票多于反对票则表示通过,如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions 的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。

    1. public void decide(Authentication authentication, Object object,
    2. Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    3. int grant = 0;
    4. int deny = 0;
    5. for (AccessDecisionVoter voter : getDecisionVoters()) {
    6. // 1-获取投票结果
    7. int result = voter.vote(authentication, object, configAttributes);
    8. if (logger.isDebugEnabled()) {
    9. logger.debug("Voter: " + voter + ", returned: " + result);
    10. }
    11. switch (result) {
    12. case AccessDecisionVoter.ACCESS_GRANTED:
    13. grant++; // 统计同意次数
    14. break;
    15. case AccessDecisionVoter.ACCESS_DENIED:
    16. deny++; // 统计决绝次数
    17. break;
    18. default:
    19. break;
    20. }
    21. }
    22. if (grant > deny) { // 2-比较同意次数和拒绝次数
    23. return;
    24. }
    25. if (deny > grant) {
    26. throw new AccessDeniedException(messages.getMessage(
    27. "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    28. }
    29. if ((grant == deny) && (grant != 0)) { // 3-判断是否允许两个次数相等的情况
    30. if (this.allowIfEqualGrantedDeniedDecisions) {
    31. return;
    32. }
    33. else {
    34. throw new AccessDeniedException(messages.getMessage(
    35. "AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    36. }
    37. }
    38. // 4-对弃权票的处理
    39. checkAllowIfAllAbstainDecisions();
    40. }

    UnanimousBased(一票否决)

    如果没有反对票,但是有赞成票,则表示通过。 

    1. public void decide(Authentication authentication, Object object,
    2. Collection<ConfigAttribute> attributes) throws AccessDeniedException {
    3. int grant = 0;
    4. List<ConfigAttribute> singleAttributeList = new ArrayList<>(1);
    5. singleAttributeList.add(null);
    6. for (ConfigAttribute attribute : attributes) {
    7. singleAttributeList.set(0, attribute);
    8. for (AccessDecisionVoter voter : getDecisionVoters()) {
    9. // 获取投票结果
    10. int result = voter.vote(authentication, object, singleAttributeList);
    11. if (logger.isDebugEnabled()) {
    12. logger.debug("Voter: " + voter + ", returned: " + result);
    13. }
    14. switch (result) {
    15. case AccessDecisionVoter.ACCESS_GRANTED:
    16. grant++;
    17. break;
    18. case AccessDecisionVoter.ACCESS_DENIED: // 有拒绝,直接抛异常
    19. throw new AccessDeniedException(messages.getMessage(
    20. "AbstractAccessDecisionManager.accessDenied",
    21. "Access is denied"));
    22. default:
    23. break;
    24. }
    25. }
    26. }
    27. // To get this far, there were no deny votes
    28. if (grant > 0) { // 没有拒绝票
    29. return;
    30. }
    31. // To get this far, every AccessDecisionVoter abstained
    32. checkAllowIfAllAbstainDecisions();
    33. }

    至此,AccessDecisionManager 的三种实现分析完毕。

  • 相关阅读:
    ScrapeKit 和 Swift 编写程序
    图相似度计算——SIMGnn源码解读
    【2022年江西省研究生数学建模】冰壶运动 思路分析及代码实现
    记录:2022-9-15 罗马数字转正数 组合总和 分割回文串 信号量算法代码 文件共享 一致性语义 文件保护 文件系统结构
    关于开展2022年江苏省重点领域 首版次软件产品征集工作的通知
    计算机毕业设计Java宠物寄养预约系统(源码+系统+mysql数据库+lw文档)
    【JavaWeb】由HTML入门JavaWeb,html知识总结(附案例)
    PEG 衍生物Biotin-PEG1-OH(cas:95611-10-2,2-生物素氨基乙醇)优势说明
    JSTL标准标签库 EL表达式
    计算机毕业设计Java的自助旅游导航系统(源码+系统+mysql数据库+lw文档)
  • 原文地址:https://blog.csdn.net/swadian2008/article/details/126805301