Spring Security源码解析

      现在流行的通用授权框架有apache的shiro和Spring家族的Spring Security,在涉及今天的微服务鉴权时,需要利用我们的授权框架搭建自己的鉴权服务,今天总理了Spring Security,便于在微服务架构体系中拓展.对于shiro之前用过,单体应用使用起来还是很灵活轻便的.

#################################################################

Spring Security是通过AccessDecisionManager进行授权管理的.就先从他开打.

Spring Security访问决策管理中,我们能看到基于实现AccessDecisionManager ,Spring Security内部实现这个接口的有4个类,逐个查看:

Spring Security源码解析_第1张图片

AbstractAccessDecisionManager:

这个抽象类从图可以看到实际下面被继承了3个类,分别是3种不同的投票策略.这里不一一说明了,有兴趣的可以翻阅源码.

Spring Security源码解析_第2张图片

3种不同的策略分别是:

AffirmativeBased的策略:

  • 只要有投通过(ACCESS_GRANTED)票,则直接判为通过。
  • 如果没有投通过票且反对(ACCESS_DENIED)票在两个及其以上的,则直接判为不通过。

UnanimousBased的策略:

  • 无论多少投票者投了多少通过(ACCESS_GRANTED)票,只要有反对票(ACCESS_DENIED),那都判为不通过。
  • 如果没有反对票且有投票者投了通过票,那么就判为通过。

ConsensusBased的策略:

  • 通过的票数大于反对的票数则判为通过。
  • 通过的票数小于反对的票数则判为不通过。
  • 通过的票数和反对的票数相等,则可根据配置allowIfEqualGrantedDeniedDecisions(默认为true)进行判断是否通过。

###################################################################

源码中可以看出这三种不同的策略都用到了AccessDecisionVoter接口(访问决策投票器),接口定义了投票方法


投票方法Spring Security内部实现了2种,如图

Spring Security源码解析_第3张图片

AuthenticatedVoter:先判定用户是否通过登录认证,没有投反对票,在内部根据用户登录包含的角色信息获取用户实际的权限和当前的请求需要的角色匹配

RoleVoter:只处理ROLE_开头的配置角色参数进行投票(可通过配置rolePrefix的值进行改变)

上面已认证投票方法中需要注意的是:

当前的请求需要的角色和用户登录包含的角色信息获取用户实际的权限这2个核心参数Spring Scerity是如何管理的呢?

我们接着往下走读,查看Spring Scerity core源码

Spring Security源码解析_第4张图片

这里userdetails目录全是是Spring Security 管理的用户信息.

这里不一一描述了,需要深入了解可以查看官方源码,主要实现思路如下:

Spring Security源码解析_第5张图片

UserDetails中getAuthorities方法,用来获取当前用户所具有的角色,在实际项目中可以根据这个方法描述当前用户的角色

示例代码:

public Collection getAuthorities() {  

 List authorities = new ArrayList<>();    

    for (Role role : roles) {  

            //  roles中获取当前用户所具有的角色,构造SimpleGrantedAuthority

     authorities.add(new SimpleGrantedAuthority(role.getName()));

   }    

return authorities;

}

#################################################################

loadUserByUsername方法:

Spring Security源码解析_第6张图片

在执行登录的过程中,这个方法将根据用户名去查找用户,如果用户不存在,则抛出UsernameNotFoundException异常,返回用户对象

示例代码:

@Service

@Transactional

public class HrService implements UserDetailsService {

@Autowired

HrMapper hrMapper;    

@Override    

public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

       Hr hr = hrMapper.loadUserByUsername(s);        

if (hr == null) {

           throw new UsernameNotFoundException("用户名不对");      

 }        return hr;  

 }

}

源码说明:

根据用户名定位用户。在实际的实现中,搜索可能是区分大小写的,或者不区分大小写,这取决于如何配置*实现实例。在这种情况下,UserDetails对象返回的用户名可能与实际请求的不同。

DefaultFilterInvocationSecurityMetadataSource:

该类实现了FilterInvocationSecurityMetadataSource,主要功能就是通过当前的请求地址,获取该地址需要的用户角色

示例代码:

@Component

public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

   @Autowired    MenuService menuService;

   AntPathMatcher antPathMatcher = new AntPathMatcher();

   @Override

   public Collection getAttributes(Object o) throws IllegalArgumentException {

       //从getAttributes(Object o)方法的参数o中提取出当前的请求url   

String requestUrl = ((FilterInvocation) o).getRequestUrl();

       if ("/login_p".equals(requestUrl)) {

           return null;

       }

        //查询数据库中url pattern和role的对应关系,Menu类有两个核心属性,一个是url pattern,即匹配规则(比如/admin/**),还有一个是List,即这种规则的路径需要哪些角色才能访问。

       List

allMenu = menuService.getAllMenu();

       for (Menu menu : allMenu) {

        //url pattern一一对照,匹配当前的url pattern所对应的角色,遍历角色

                //这里涉及到一个优先级问题,比如我的地址是/employee/basic/hello,这个地址既能被/employee/**匹配,

                //也能被/employee/basic/**匹配,这就要求我们从数据库查询的时候对数据进行排序,将/employee/basic/**类型的url pattern放在集合的前面去比较

           if (antPathMatcher.match(menu.getUrl(), requestUrl)&&menu.getRoles().size()>0) {

               List roles = menu.getRoles();

               int size = roles.size();

               String[] values = new String[size];

               for (int i = 0; i < size; i++) {

                   values[i] = roles.get(i).getName();

               }

                //SecurityConfig.createList方法来创建一个角色集合

               return SecurityConfig.createList(values);

           }

       }        

        //没有匹配上的资源,都是登录访问

        return SecurityConfig.createList("ROLE_LOGIN");

        //.如果返回null的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。

       //return null;

   }

   @Override


   public Collection getAllConfigAttributes() {

       return null;

   }

   @Override

   public boolean supports(Class aClass) {

       return FilterInvocation.class.isAssignableFrom(aClass);

   }

}

AccessDecisionManager接口: 进行最终的访问控制(授权)决策

getAttributes(Object o)方法返回的集合最终会来到AccessDecisionManager接口中,作为传递的参数解析访问控制决策

Spring Security源码解析_第7张图片

方法说明:

authentication是调用此方法的主体对象,就是当前用户,保存了当前登录用户的角色信息

object:被调用的安全对象,rest资源对象

configAttributes:与安全保护相关的配置属性,即当前请求需要的角色


示例代码:

@Component

public class UrlAccessDecisionManager implements AccessDecisionManager {

   @Override

   public void decide(Authentication authentication, Object o, Collection collection) throws AccessDeniedException, AuthenticationException {

       Iterator iterator = collection.iterator();

       while (iterator.hasNext()) {

           ConfigAttribute ca = iterator.next();

           //当前请求需要的权限

           String needRole = ca.getAttribute();

            //当前请求需要的权限为/ROLE_LOGIN则表示登录即可访问,和角色没有关系,

           if ("ROLE_LOGIN".equals(needRole)) {

                //需要判断authentication是不是AnonymousAuthenticationToken的一个实例,是表示当前用户没有登录,没有登录就抛一个BadCredentialsException异常,登录了就直接返回,则这个请求将被成功执行

               if (authentication instanceof AnonymousAuthenticationToken) {

                   throw new BadCredentialsException("未登录");

               } else

                   return;

           }

           //当前用户所具有的权限

           Collection authorities = authentication.getAuthorities();

            //查看当前用户的角色列表中是否具备需要的权限,如果具备就直接返回,否则就抛异常

           for (GrantedAuthority authority : authorities) {

               if (authority.getAuthority().equals(needRole)) {

                   return;

               }

           }  

     }

       throw new AccessDeniedException("权限不足!");

   }  

 @Override

   public boolean supports(ConfigAttribute configAttribute) {

       return true;

   }

   @Override

   public boolean supports(Class aClass) {

       return true;

   }

}


AccessDeniedHandler接口:处理内部AccessDeniedException异常

Spring Security源码解析_第8张图片

示例代码:

@Component
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
        out.flush();
        out.close();
    }

}

WebSecurityConfig:

Spring Security源码解析_第9张图片

Spring Security源码解析_第10张图片

configure(HttpSecurity http)方法中,通过withObjectPostProcessor将刚刚创建的UrlFilterInvocationSecurityMetadataSource和UrlAccessDecisionManager注入进来。到时候,请求都会经过刚才的过滤器(除了configure(WebSecurity web)方法忽略的请求)

successHandler中配置登录成功时返回的JSON,登录成功时返回当前用户的信息

failureHandler表示登录失败,登录失败的原因可能有多种,我们根据不同的异常输出不同的错误提示即可

示例代码:


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    HrService hrService;
    @Autowired
    UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
    @Autowired
    UrlAccessDecisionManager urlAccessDecisionManager;
    @Autowired
    AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(hrService).passwordEncoder(new BCryptPasswordEncoder());
    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/index.html", "/static/**","/login_p");
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor() {
                    @Override
                    public O postProcess(O o) {
                        o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
                        o.setAccessDecisionManager(urlAccessDecisionManager);
                        return o;
                    }
                }).and().formLogin().loginPage("/login_p").loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password").permitAll().failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                StringBuffer sb = new StringBuffer();
                sb.append("{\"status\":\"error\",\"msg\":\"");
                if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
                    sb.append("用户名或密码输入错误,登录失败!");
                } else if (e instanceof DisabledException) {
                    sb.append("账户被禁用,登录失败,请联系管理员!");
                } else {
                    sb.append("登录失败!");
                }
                sb.append("\"}");
                out.write(sb.toString());
                out.flush();
                out.close();
            }
        }).successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                ObjectMapper objectMapper = new ObjectMapper();
                String s = "{\"status\":\"success\",\"msg\":" + objectMapper.writeValueAsString(HrUtils.getCurrentHr()) + "}";
                out.write(s);
                out.flush();
                out.close();
            }
        }).and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler);
    }

}

最后,针对自己的项目整理Spring Security的访问鉴权策略的基本思路为:

AccessDecisionManager来判断所请求的url + httpmethod 是否符合我们数据库中的配置。然而,AccessDecisionManager并没有来判定类似需求的相关Voter, 因此,我们需要自定义一个Voter的实现(默认注册的AffirmativeBased的策略是只要有Voter投出ACCESS_GRANTED票,则判定为通过,这也正符合我们的需求)。实现voter后,有2个关键参数(Collection attributes,Authentication authentication)进行结合业务进行实现Spring Security的core下的userdetails部分接口进行组装)

##########################################################

篇外话:其实看源码最好还是从官方文档的入手,从系统策略开始在逐个深入,各个击破.

Spring Security源码解析_第11张图片

Spring Security源码解析_第12张图片

你可能感兴趣的:(系统设计,Spring,Scurity,鉴权)