美文网首页我爱编程
apache shiro使用 - 与spring boot集成

apache shiro使用 - 与spring boot集成

作者: aaron1993 | 来源:发表于2017-08-20 15:02 被阅读0次

    1. 前言

    最近项目中使用到了shiro作为用户身份验证以及访问权限控制的安全框架,本文主要简单介绍一下使用过程。

    2. shiro简介

    shiro是apache上的一个java实现的开源安全框架,提供了以下基本功能:

    1. anthentication,身份认证,比如username/password的验证。
    2. authorization,权限验证,即访问控制,比如restful api中判断某个用户是否拥有GET的权限。
    3. session manager,会话管理,和web中session概念类似。
    4. Cryptography,数据加密传输。
      shiro的架构图如下:
    shiro架构图

    说明

    1. Subject, shiro Subject可以理解成访问当前程序(当前程序也就是需要使用安全框架保护的程序)的用户,用户可以是web上登录的用户,也可以某个正在试图访问当前程序的调度程序等等。shiro提供的功能可以通过Subject一组方法完成,比如(只列举了一部分):

      • boolean isPermitted(String permission)
      • boolean isPermitted(Permission permission)
        属于shiro的authorization功能,string类型的权限最终也会转换成具体的Permission类,isPermitted用来判断用户(subject)是否具有perssion权限,也就是说当前subject拥有的权限是否是permission的超集。
      • public void login(AuthenticationToken token)
      • public void logout()
        shiro的authentication功能。提供用户身份认证。
        登录,比如用户使用username/password登录时,可以这样调用:
         //获取当前user的Subject
         Subject currentUser = SecurityUtils.getSubject();
         currentUser.login(new new UsernamePasswordToken(username, password)).
        
      • Session getSession();
        获得会话,没有会创建新的。
    2. SecurityManager
      SecurityManager时shiro的核心,从上图可以看出SecurityManager包含了一些组件共同提供了shiro的所有功能,Subject类似更像一个shell,Subject的调用都会走向SecurityManager来完成核心的authentication/authorization/sessionmanager等功能。

    3. Realm
      上图中Pluggable Realms,shiro中唯一需要我们自己实现的部分,SecurityManager通过authenticato/authorizer替我们完成用户认证以及权限控制,但是验证用户身份时的用户身份信息,验证用户权限时用户的权限信息都需要我们自己定义好告诉shiro,这部分工作就是realm应该完成的,realm因此可以理解成数据源,比如你的用户信息存储在mysql里,那么你自定义的realm就需要从mysql里获得用户省份信息,以及权限信息。

    3. 和spring boot结合

    在和spring boot结合的过程中,我需要做的主要有以下几个部分:

    1. 自定义realm,继承抽象类AuthorizingRealm实现它的几个抽象方法获取用户身份信息,以及用户权限信息。我的用户身份信息保存在mysql中。
    2. 自定义Permission,实现Permission接口。Permission对应用户的访问权限信息。
    3. 实现ShiroConfig类,由于spring boot不像springMVC中从“applicationContext.xml”加载所有bean信息,因此定义了ShiroConfig,并使用@Configuration,通过java标注的方式加载装配shiro各个组件
      注:(关于使用xml配置文件和springMVC结合可以参考apache shiro:integrate with springshiro 与spring集成
    4. 实现Filter,在springMVC中,通过DispactherServlet来完成对不同url的处理(包括调用filter以及使用其他servlet处理等), 和springboot结合中,想要shiro 的authorizition/anthentication组件发挥作用,需要定义filter来对不同的url做出处理。

    3.1 自定义realm

    自定义realm,继承AuthorizingRealm并实现它的抽象方法:

    public class ShiroMysqlRealm extends AuthorizingRealm {
    
      @Autowired
      private UserDAO userDAO;
    
      public ShiroMysqlRealm() {
      }
      
     /**
       实现这个方法获取用户权限,这里不需要你检查权限,只需要获取用户权限返回就行了,当调用Subject#isPermitted(Permission permission)时,
       会调用这个方法获得用户拥有的权限,然后对用户拥有的每一个Permission
      都会调用Permission # public boolean implies(Permission permission)。
      implies的参数是permission是要检查的权限(或者叫用户本次操作需要拥有的权限),调用implies检查用户拥有的权限是否包含要检查的permission。
       */
      @Override
     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = (String) principalCollection.getPrimaryPrincipal();
       // 通过userDAO获取到用户拥有的所有权限
        List<Permission> permissions = userDAO.getUserPermissions(username);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addObjectPermissions(permissions);
        return authorizationInfo;
      }
    
    /*
      实现这个方法,完成用户身份验证,当调用Subject#login时,最终会走向这个方法.
     方法参数‘ authenticationToken’是当前用户使用的username/password,我们  需要验证username/password是否有效。
     验证过程很简单,通过userDAO从mysql获取用户username/password, 然后和参数传过来的比较一下就行了。
      */
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        // 调用subject#login时,我们使用的是UsernamePasswordToken,这里校验一下,不是就反回null,返回null就意味着用户身份验证不通过。
        if(!(authenticationToken instanceof UsernamePasswordToken)){
           return null;
        }
        UsernamePasswordToken login = (UsernamePasswordToken) authenticationToken;
        // 通过userDAO获取到存在mysql里的username/password
        User user = userDAO.getUserByName(login.getUsername()
    )
       // 判断一下和数据库存的password是不是一样的,一样表示验证通过。
        if (user != null && StringUtils.equals(login.getPassword(), user.getPassword))) {
          return new SimpleAuthenticationInfo(login.getUsername(), String.valueOf(login.getPassword()), getName());
        } else {
          return null;
        }
      }
    }
    
    ------------------------------------------------
    关于doGetAuthenticationInfo这个方法,它的参数时当前尝试登录用户传过来的登录信息,它返回的是我们存储的的该用户的真实的信息,最终是拿两者比较来确定是否登录成功。
    login时有这样一个调用链:
    Subject # login -> ... -> AuthenticatingRealm # getAuthenticationInfo -> 
    AuthenticatingRealm # doGetAuthenticationInfo -> AuthenticatingRealm # assertCredentialsMatch
    我重写了doGetAuthenticationInfo, 直接在里面完成验证,也可以重写assertCredentialsMatch去完成验证。
    

    3.2 自定义permission

    permission即用户权限,上面自定义realm中doGetAuthorizationInfo通过userDAO从数据库中获取某个用户的所有权限,这里就需要我们自定义权限类,这里主要检查restful接口的一些操作权限如下:

    1. GET
    2. POST
    3. PUT
    4. DELETE
    5. ALL (拥有以上所有权限)
      类如下:
    //自定义的permission需要实现Permission接口
    public class RestPermission implements org.apache.shiro.authz.Permission{
        private RestType restType;
    
        public RestPermission(RestType restType) {
            this.restType = restType;
        }
    
       pulic RestPermission(String restType){
          this(RestType.fromString(restType));
       }
    
       /**
         只要实现这一个方法,判断当前拥有的权限(也就是this)是否包含p。
        */
        @Override
        public boolean implies(org.apache.shiro.authz.Permission p) {
            if(p == null || !(p instanceof RestPermission)){
                return false;
            }
    
            RestPermission rp = (RestPermission) p;
            //当前拥有all权限或者当前权限和检查的权限一样返回true,验证通过
            if(this.restType == RestType.ALL || this.restType == rp.restType){
                return true;
            }
    
            return false;
        }
    
        enum RestType{
            GET("GET"),
            POST("POST"),
            PUT("PUT"),
            DELETE("DELETE"),
            ALL("ALL"),
            UNKOWN("UNKOWN");
    
            private String type;
    
            RestType(String type) {
                this.type = type;
            }
    
            static RestType fromString(String type){
                if(StringUtils.isBlank(type)){
                    return UNKOWN;
                }
    
                String t = type.trim().toUpperCase();
                if(t.equals("GET")){
                    return GET;
                }else if(t.equals("POST")){
                    return POST;
                }else if(t.equals("PUT")){
                    return PUT;
                }else if(t.equals("DELETE")){
                    return DELETE;
                }else if(t.equals("ALL")){
                    return ALL;
                }else{
                    return UNKOWN;
                }
            }
        }
    }
    

    3.3 定义ShiroConfig

    spring boot中不使用从xml文件加载bean,因此实现ShiroConfig,并使用@Configuration注解来完成shiro中必要组件加载组装。

    @Configuration
    public class ShiroConfig {
    
      public ShiroConfig() {
      }
    
      @Bean
      public Realm shiroRealm() {
       return new ShiroMysqlRealm();
      }
    
      @Bean(name = "lifecycleBeanPostProcessor")
      public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
      }
    
      @Bean(name = "securityManager")
      public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
        sm.setRealm(shiroRealm());
        sm.setSessionManager(sessionManager());
        return sm;
      }
    
      private SessionManager sessionManager() {
        DefaultWebSessionManager sm = new DefaultWebSessionManager();
        Cookie cookie = new SimpleCookie("XXXXXXX");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(60 * 60); // expire in 1 hour
        sm.setSessionIdCookie(cookie);
        return sm;
      }
    
      @Bean
      public MethodInvokingFactoryBean methodInvokingFactoryBean() {
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
        methodInvokingFactoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
        methodInvokingFactoryBean.setArguments(new Object[]{securityManager()});
        return methodInvokingFactoryBean;
      }
    
      @Bean
      public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager sm) {
        AuthorizationAttributeSourceAdvisor sa = new AuthorizationAttributeSourceAdvisor();
        sa.setSecurityManager(sm);
        return sa;
      }
    
     /**
       通过FactoryBean方式创建过滤器,‘ shiroFilter’自身也是一个Filter,它的作用有点类似‘ServleDispacther’, 它里面注册了很多url到filter的映射,
    url的访问会先走向‘ shiroFilter’,然后‘ shiroFilter’根据url使用模式匹配去匹配到filterChain, 然后逐个激活filterChain里的filter。
       */
      @Bean(name = "shiroFilter")
      public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager sm) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
       // 设置登录跳转页面
        shiroFilter.setLoginUrl("/login");
       //登录成功后跳转页面
        shiroFilter.setSuccessUrl("/index");
       // 权限检查不过的跳转页面
        shiroFilter.setUnauthorizedUrl("/forbidden");
       //这是一个url pattern到过滤器名称的映射,一个url有多个filter时,过滤器名称用','隔开。
      // 注意这里使用了LinkedHashMap,因为url添加的顺序很重要,当'shiroFilter'尝试根据当前url去匹配符合该url的模式时,只返回这个map里第一个匹配成功的url 模式
        Map<String, String> filterChainDefinitionMapping = new LinkedHashMap<>();
       //为url 模式 设置filter, anon是filter name
        filterChainDefinitionMapping.put("/login", "anon");
        filterChainDefinitionMapping.put("/", "authc");
        filterChainDefinitionMapping.put("/api/v1/**", "authz-rest");
        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMapping);
        shiroFilter.setSecurityManager(sm);
    
        Map<String, Filter> filters = new HashMap<>();
       /* 设置filter name到filter实例的映射。这里除了你通过现实的设置filtername 
         到filter的映射以外, 当前spring  容器里面所有实现了‘javax.servlet.Filter’的bean也会被默认添加进来,它的name就是bean name。
        filters.put("anon", new AnonymousFilter());
        filters.put("authc", new FormAuthenticationFilter());
        filters.put("authz-rest", new RESTAuthzFilter());
        shiroFilter.setFilters(filters);
        return shiroFilter;
      }
    }
    

    上面代码中我只自定义了一个filter,即RESTAuthzFilter, 当访问的url符合"/api/v1/**"模式时就会调用这个filter。 前面我们已经定义了GET、POST、PUT等等的一些rest操作的权限,现在在这个filter里就需要检查一下当前登录用户拥有权限去尽心这个url的操作,实现如下:

    public class RESTAuthzFilter extends FormAuthenticationFilter {
    
      // servlet filter中最关键的是doFilter方法,这里实现isAccessAllowed最终会被foFilter调用。在这里检查访问权限。
      @Override
      protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //获得当前用户subject
        Subject subject = this.getSubject(request, response);
        //检查一下是否登录并通过身份认证
        if (subject.isAuthenticated()) {
          String uri = WebUtils.toHttp(request).getRequestURI().toString();
          //这里getMethod拿到本次url请求的操作(PUT,GET等等),然后构造出permission表示本次操作需要的权限。
          RestPermission permission = new RestPermission(WebUtils.toHttp(request).getMethod());
          //检查用户是否具有权限,上面提到过这次调用会走到ShiroMysqlRealm#doGetAuthorizationInfo获取用户所拥有的权限,然后对返回的权限再调用RestPermission # implies检查。
          return subject.isPermitted(permission)
        } else {
          return false;
        }
      }
    
    }
    

    附:本文参考

    1. shiro简介
    2. apache shiro

    相关文章

      网友评论

        本文标题:apache shiro使用 - 与spring boot集成

        本文链接:https://www.haomeiwen.com/subject/rxcxdxtx.html