apache shiro使用 - 与spring boot集成

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,579评论 18 139
  • 一、架构 要学习如何使用Shiro必须先从它的架构谈起,作为一款安全框架Shiro的设计相当精妙。Shiro的应用...
    ITsupuerlady阅读 3,515评论 4 32
  • 前言 Spring boot 是什么,网上的很多介绍,这里博客就不多介绍了。如果不明白Spring boot是什么...
    xuezhijian阅读 17,894评论 13 39
  • Apache Shiro Apache Shiro 是一个强大而灵活的开源安全框架,它干净利落地处理身份认证,授权...
    罗志贇阅读 3,214评论 1 49
  • 从15岁起,kk就喜欢薰衣草,紫色的。从此,紫色成了kk的幸运色。后来他知道薰衣草的花语是:等待爱情!kk觉得自己...
    熊猫微刊阅读 669评论 0 5