Spring Boot集成无状态Shiro--内容详细介绍 - Go语言中文社区

Spring Boot集成无状态Shiro--内容详细介绍


这里对昨天的shiro项目做个说明,整个项目主要参考的是GitHub的一个项目,他是基于session会话的,有集成redis,如果需要的话大家可以参考下:https://github.com/lovelyCoder/springboot-shiro
我的项目GitHub地址:https://github.com/rhettpang/Springboot-Shiro

现在说下我的无状态的shiro,先把项目结构列出来好做说明:
这里写图片描述

  • config:shiro的配置;
  • constant:自己用到的常量;
  • controller:为测试用的接口:DefaultExceptionHandler这个类专门用来处理shiro抛出的异常,SimpleErrorController捕获项目异常,返回一个友好的json格式;
  • filter:自己重写的shiro的filter
  • redis:对redis的集成(原打算用的,需求有变化,后来发现暂时用不到了,这里提供出来给大家参考下)
  • shiro:shiro的realm和相关的一些实现 util:这里目前只有一个MD5加密工具类,用来对密码加密的
  • mapper和service大家都理解

resources中还有些配置,大家自己把代码宕下来看看就好。

1. ShiroConfig

ShiroConfig中的部分代码:

*
     Filter Chain定义说明
     1、一个URL可以配置多个Filter,使用逗号分隔
     2、当设置多个过滤器时,全部验证通过,才视为通过
     3、部分过滤器可指定参数,如perms,roles
     *
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
        log.info("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean  = new ShiroFilterFactoryBean();

        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/usersPage");
        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        //自定义拦截器
        Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
        filtersMap.put("myAccessControlFilter", new MyAccessControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);

        //拦截器.
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();

        //我做的是无状态的,这里的东西实际上是用不到的,仅供参考
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/css/**","anon");
        filterChainDefinitionMap.put("/js/**","anon");
        filterChainDefinitionMap.put("/img/**","anon");
        filterChainDefinitionMap.put("/font-awesome/**","anon");


      //  filterChainDefinitionMap.put("/users", "anon");
        filterChainDefinitionMap.put("/createPermission", "anon");
        filterChainDefinitionMap.put("/**", "myAccessControlFilter");
//        filterChainDefinitionMap.put("/**", "authc");

        //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
        //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        //自定义加载权限资源关系
//        List<Resources> resourcesList = resourcesService.queryAll();
//        for(Resources resources:resourcesList){
//
//            if (StringUtil.isNotEmpty(resources.getResurl())) {
//                String permission = "perms[" + resources.getResurl()+ "]";
//                filterChainDefinitionMap.put(resources.getResurl(),permission);
//            }
//        }

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

这里有很多备注,相信大家都能看懂,简单说下其中的重点。

//自定义拦截器
        Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
        filtersMap.put("myAccessControlFilter", new MyAccessControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);

这块内容备注的是自定义拦截器,准确说来,这里应该是加载自定义的拦截器。在我项目的filter包中的MyAccessControlFilter就是在这里加载进来的。必须要说下,这里只能通过new MyAccessControlFilter()这种形式加载,使用@Autowired是不生效的。

filterChainDefinitionMap.put("/**", "myAccessControlFilter");

这行代码是将我上面定义的拦截器具体使用,我用自己定义的拦截器myAccessControlFilter替代了authc(这个是指FormAuthenticationFilter,我有对这个filter重写,但是最后没用到,可以看下filter中的MyFormAuthenticationFilter),使用继承了AccessControlFilter的类来替代FormAuthenticationFilter。
网上的参考内容一般都是说无状态的一般都是通过集成AccessControlFilter来实现的,这里我跟着大部队走。主要还是这个filter的方法,下面会具体介绍。

        //自定义加载权限资源关系
//        List<Resources> resourcesList = resourcesService.queryAll();
//        for(Resources resources:resourcesList){
//
//            if (StringUtil.isNotEmpty(resources.getResurl())) {
//                String permission = "perms[" + resources.getResurl()+ "]";
//                filterChainDefinitionMap.put(resources.getResurl(),permission);
//            }
//        }

这块被我备注起来,我参考的代码是用来加载数据库中url与拦截器对应关系的。如果项目中需要单独设置的url多了,最好是在数据库中配置,当然在这里一一列出也好,看个人需求,这里保留的目的只是想说提供了这个功能。

    @Bean
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        return new DefaultAdvisorAutoProxyCreator();
    }

这两个bean是为了下面这个功能定义的

@RequiresRoles(value={"admin","user"},logical = Logical.OR)
@RequiresPermissions(value={"add","update"},logical = Logical.OR)

如果不加这种注解的权限验证不会生效,只能在进入方法后判断了。

    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        //设置realm.
//        securityManager.setAuthenticator(modularRealmAuthenticator());
        securityManager.setAuthenticator(customizedModularRealmAuthenticator());

        List<Realm> realms=new ArrayList<>();
        realms.add(myShiroRealm());
        realms.add(myShiroRealm2());
        securityManager.setRealms(realms);

        return securityManager;
    }

这个bean是SecurityManager 的设置,用来加载realm,很重要的一个东西(其实这里的bean每个都很重要o( ̄︶ ̄)o)。
这里我给出的代码是实现了多realm的,如果只有一个realm,可以去掉setAuthenticator,这里的realm设置也重新改成单realm(在这里只add一个realm应该也是没问题的,我没试过)。
单realm实现方式如下:

   @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        //设置realm.
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

很简单的一行代码。

    @Bean
    public MyShiroRealm myShiroRealm(){
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        //我自己实现的加密判断,这里被备注起来,仅供参考
//        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

realm在这里的加载,由于不能使用@Autowired,这里都是通过@Bean来加载的。备注的这行大家可以留一下,同时也说明了每个realm可以有自己的处理方式。

    /**
     * 自定义的Realm管理,主要针对多realm
     * */
    @Bean
    public MyModularRealmAuthenticator customizedModularRealmAuthenticator(){
        MyModularRealmAuthenticator customizedModularRealmAuthenticator=new MyModularRealmAuthenticator();
        //设置realm判断条件
        customizedModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());

        return customizedModularRealmAuthenticator;
    }

这里是针对多realm设置的,唯一要说的是setAuthenticationStrategy这里,用来设置realm的规则。我这里选用的是AtLeastOneSuccessfulStrategy,至少有一个生效。大家也可以选用AllSuccessfulStrategy()(所有realm都验证通过才能成功登陆)和FirstSuccessfulStrategy()(只要有一个生效就不会去其它realm验证)。

2.MyModularRealmAuthenticator

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException {

        // 判断getRealms()是否返回为空
        assertRealmsConfigured();
        MyUsernamePasswordToken token=(MyUsernamePasswordToken)authenticationToken;
        // 登录类型
        String loginType = token.getType();

        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 登录类型对应的所有Realm
        Collection<Realm> typeRealms = new ArrayList<>();
        HashMap<String,Realm> realmHashMap=new HashMap<>(realms.size());
        for (Realm realm : realms) {
            realmHashMap.put(realm.getName(),realm);
//            if (realm.getName().contains(loginType)){
//                typeRealms.add(realm);
//            }
        }

        if (realmHashMap.get(loginType)!=null){
            return doSingleRealmAuthentication(realmHashMap.get(loginType), token);
        }else {
            return doMultiRealmAuthentication(realms, token);
        }

        // 判断是单Realm还是多Realm
//        if (typeRealms.size() == 1)
//            return doSingleRealmAuthentication(typeRealms.iterator().next(), token);
//        else
//            return doMultiRealmAuthentication(typeRealms, token);
    }

使用多realm时自定义的管理器。参考的代码中给出的示例实现被我备注起来了,他是通过loginType来确定realm的验证类型。我这里该的是可以通过loginType来指定由某个特定的realm来执行这次的验证。
需要说明的是,验证(登陆)通过后在controller的方法中进行验证的时候它是按照setRealms中设定的顺序来找realm进行验证的,直到有一个通过为止。
其实验证的时候如果每个realm对应不同的token,可以通过supports来判断是否经过该realm来验证,这个我代码中的都被我删掉了,因为我只有一种token,当时也没有特别明白这个方法的用途,现在给出来。

    @Override
    public boolean supports(AuthenticationToken token) {
//      return super.supports(token);
        ////仅支持UsernamePasswordToken 类型的Token
        System.out.println("token supports");
        return token instanceof MyUsernamePasswordToken;
    }

这个方法放到realm中就好,进入realm的时候会第一个执行它。

    @Override
    public String getName() {
        return "myShiroRealm1";
    }

设定realm的名称,多realm中判断登陆类型来指定特定realm的时候我用到的。

3.realm

//认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        MyUsernamePasswordToken myToken=(MyUsernamePasswordToken)token;
        //获取用户的输入的账号.
        String username = (String)myToken.getPrincipal();
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        User user = userService.findByUsername(username);
        System.out.println(user.toString());
        if(user==null){
            throw new UnknownAccountException();
        }
//        if (0==user.getEnable()) {
//            // 帐号锁定
//            throw new LockedAccountException();
//        }

        //此处使用的是user对象,不是username
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                getName()
        );
        return authenticationInfo;
    }

这里是认证功能。

String username = (String)myToken.getPrincipal();
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        User user = userService.findByUsername(username);

第一行是从token中获取登陆的用户名,然后从数据库中取相应的信息。

 //此处使用的是user对象,不是username
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                getName()
        );

这里是对登陆密码做对比的,准确来说是将数据库中的密码user.getPassword()传入,SimpleAuthenticationInfo中有个位置会获取token中的密码与它做对比,如果一样就正常返回,不一样就抛出异常。

user这个字段很有意思,网上很多资料都是传的对象,但是后面备注的都是用户名。但是这里如果传username的时候会报string无法转user对象的异常。但是我在开涛大神那里参考他处理的时候,传username是OK的,传user也是OK的。如果传user,authenticationInfo 就是user的值;如果传username,authenticationInfo 就是一个字符串username的值。所以这里还有一些需要好好研究的地方,如果哪位大神看到了可以给我留言解惑。

//授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("doGetAuthorizationInfo1");
        //User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1}
        User user= (User) SecurityUtils.getSubject().getPrincipal();

        List<Permission> permissions=permissionService.findPermissionAndRoleNameByUserId(user.getUserId());
        System.out.println("permissions:"+permissions.size());
//        List<Role> roles=permissionService.findPermissionByUserId(user.getUserId());
        // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //这里赋给两个不存在的值,使controller中的权限验证失败,验证在此失败会继续进入myShiroRealm2验证权限
//        info.addRole("as");
//        info.addStringPermission("sdf");
        for(Permission permission: permissions){
            System.out.println("permission:"+permission.getPermission());
            System.out.println("permission.getRoleName():"+permission.getRoleName());
            info.addStringPermission(permission.getPermission());
            info.addRole(permission.getRoleName());
        }
        return info;
    }

这里备注的是授权,可是我感觉这里叫做权限验证更合适(或者调用这里之前有个叫做权限验证的方法,这里只是其中一个被用到的功能),因为这里是在做权限验证的时候用到的,就是那判断用户角色和权限的注解。

主要功能是通过用户信息从数据库中获取相应权限,设置进info中返回(但从返回info这点来看确实是授权的功能)。

4.controller权限验证

    @GetMapping("/users")
    @RequiresRoles(value={"admin","user"},logical = Logical.OR)
    @RequiresPermissions(value={"add","update"},logical = Logical.OR)
    public String getUserInfo(){

        log.info("into getUserInfo");
//        Subject subject= SecurityUtils.getSubject();
//        try {
//            subject.checkPermissions("add","update");
//        }catch (UnauthorizedException e){
//            log.info("错误信息:"+e.getMessage());
//            //TODO 定义错误处理页面
//            log.info("权限不足");
//        }

        log.info("...............................");

        return "Success to get user info";
    }
    @RequiresRoles(value={"admin","user"},logical = Logical.OR)
    @RequiresPermissions(value={"add","update"},logical = Logical.OR)

这两行在上面有说过,在进入方法之前判断权限的,很简洁明了。@RequiresRoles和@RequiresPermissions分别对角色和权限进行验证,会调用realm中的doGetAuthorizationInfo。

有几点需要注意,如果需要验证多个角色/权限的时候可以用逗号隔开,如上。默认是所有设置的角色/权限都具有才能验证通过,而且是每个角色/权限验证的时候都会单独调用一次doGetAuthorizationInfo,这是非常耗时的,对性能要求高的需要好好设定。

logical = Logical.OR这个是设置前面设定的角色/权限只要满足一个就可以了。

这篇文章就写到这里,后面可能会写几篇短的文章,来说明我集成过程中遇到的坑。由于我也是最近这几天刚接触,有什么理解不对的地方还请各位大神留言指正。如果有不同观点,也可以在这里探讨。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_35981283/article/details/78619750
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 17:54:59
  • 阅读 ( 1061 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢