51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

springboot集成用户认证授权框架shiro基础教程

# springboot 集成用户认证授权框架 shiro 基础教程 {#springboot-集成用户认证授权框架-shiro-基础教程}

本文介绍 springboot 项目集成 shiro 框架的步骤,使系统支持用户认证和授权。java 生态中,常用的用户认证授权框架有 2 个:spring security、shiro。而 shiro 因为使用相对简单, 且能满足大部分需求而成为首选的权限框架。shiro 对资源的认证和授权配置非常灵活, 支持全局配置(通过配置类)和局部配置(通过注解)。
提示

本文只讲述最基础的配置,不说废话,带您快速入门。

# 1. 安装依赖 {#_1-安装依赖}

//    权限框架shiro
    compile 'org.apache.shiro:shiro-spring-boot-web-starter:1.6.0'

# 2. 创建 UserRealm 类 {#_2-创建-userrealm-类}

UserRealm 类做的工作只有 2 个:

  1. 认证
    在函数 doGetAuthenticationInfo 中实现用户认证逻辑。 何时执行: 执行后文提到的 ShiroServiceImpl.login 方法会触发认证逻辑。

  2. 授权
    在函数 doGetAuthorizationInfo 中实现授权逻辑。
    何时执行: 只有被访问的资源需要授权访问时, 才执行授权逻辑。

    package com.ruiboyun.facehr.permission.shiro;

    import cn.hutool.core.util.ObjectUtil; import com.ruiboyun.facehr.permission.service.IPermissionService; import com.ruiboyun.facehr.user.entity.User; import com.ruiboyun.facehr.user.service.IUserService; import lombok.extern.log4j.Log4j2; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired;

    import java.util.List;

    @Log4j2 public class UserRealm extends AuthorizingRealm { @Autowired private IUserService iUserService;

     @Autowired
     private IPermissionService iPermissionService;
    
     /**
      * shiro授权
      *
      * @param principals
      * @return
      */
     @Override
     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    

    // 从shiro取出当前用户对象 User user = (User) principals.getPrimaryPrincipal();

    // 从数据库查询出当前用户的角色列表和权限列表 List permissionStringList = iPermissionService.listPermissionStringByUsername(user.getUsername()); List roleNameList = iPermissionService.listRoleNameByUsername(user.getUsername());

    // 将当前用户的角色和权限列表赋给shiro SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addStringPermissions(permissionStringList); authorizationInfo.addRoles(roleNameList);

         log.info("shiro授权, 完成, user[{}], permissionStringList[{}]", user, permissionStringList);
         return authorizationInfo;
     }
    
     /**
      * shiro认证
      *
      * @param authenticationToken
      * @return
      * @throws AuthenticationException
      */
     @Override
     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
         UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
         User user = iUserService.getByUsername(token.getUsername());
         if (ObjectUtil.isNull(user)) {
             log.error("shiro认证, 失败, 用户不存在, username[{}]", token.getUsername());
             return null;
         }
         log.info("shiro认证, 完成, user[{}]", user);
         return new SimpleAuthenticationInfo(
                 user,
    

    // 注意是从数据库读取的密码,并不是从客户端传入的密码: 若对密码做了加密存储的话,这2个密码的值是不同的 user.getPassword().toCharArray(), getName() ); } }

您只需要根据具体需求, 改动如下逻辑: 根据用户名查询出用户的角色列表和权限列表。
这个逻辑您就可以任意发挥了,甚至不需要数据库,把数据写死都可以。

//        从数据库查询出当前用户的角色列表和权限列表
        List<String> permissionStringList = iPermissionService.listPermissionStringByUsername(user.getUsername());
        List<String> roleNameList = iPermissionService.listRoleNameByUsername(user.getUsername());

为了从简, 本教程假定密码为明文存储, 否则, UserRealm 类还需要做对应修改。

# 3. 创建 shiro 配置类 {#_3-创建-shiro-配置类}

shiro 配置类完成的工作包括:

  • 定义密码匹配器
    若用户是加密的,则需要定义密码匹配器,否则无需定义。本教程没有定义。

  • 以"url 配置"方式初始化认证权限规则
    需要依赖 shiro 提供的认证过滤器或权限过滤器,或者是用户自定义过滤器。

如前所述, 假定用户密码在数据库中没有加密存储。否则, 还需要再定义密码匹配器。

package com.ruiboyun.facehr.config;

import com.ruiboyun.facehr.permission.shiro.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 权限框架shiro的配置
 */
@Configuration
public class ShiroConfig {
    /**
     * 自定义realm
     *
     * @return
     */
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        return userRealm;
    }

    /**
     * 安全管理器
     *
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        return securityManager;
    }


    /**
     * 设置过滤规则
     *
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        /**
         * 资源需要认证, 且认证失败时, 跳转的url
         * 注意: 如果不设置,默认会自动寻找Web工程根目录下的"/login.jsp"页面或"/login"映射
         */
        shiroFilterFactoryBean.setLoginUrl("http://demo.com/login/");
//        资源需要授权, 且授权失败时, 跳转的url
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");

        //指定路径和过滤器的对应关系
        //注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
        //所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/api/user/user/loginByPhoneAndPassword", "anon");

//        其它请求都需要认证
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
}

提示

将如上配置类复制到您的工程中,需要对如下 3 处按实际情况修改:

  • shiroFilterFactoryBean.setLoginUrl("http://demo.com/login/")
  • shiroFilterFactoryBean.setUnauthorizedUrl("/unauth")
  • filterChainDefinitionMap.put("/api/user/user/loginByPhoneAndPassword", "anon")

若项目中集成了 swagger,则需要对 swagger api 文档的资源访问路径做"匿名访问"配置:

/************** start swagger接口文档支持匿名访问 ***************/
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
/************** end swagger接口文档支持匿名访问 ***************/

# 4. 创建 ShiroService {#_4-创建-shiroservice}

为了代码清晰, 我们创建一个独立的 ShiroService 类, 用于封装 shiro 的登录和登出逻辑。

package com.ruiboyun.facehr.permission.service.impl;

import com.ruiboyun.facehr.permission.service.IShiroService;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;

@Service
@Log4j2
public class ShiroServiceImpl implements IShiroService {
    /**
     * shiro登录
     *
     * @param username
     * @param password
     * @return
     */
    @Override
    public void login(String username, String password) {
        Subject user = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        user.login(token);
        log.info("shiro登录, 完成, username[{}], password[{}]", username, password);
    }

    /**
     * shiro登出
     */
    @Override
    public void logout() {
        Subject user = SecurityUtils.getSubject();
        user.logout();

        log.info("shiro登出, user[{}]", user);
    }
}

# 5. 修改业务逻辑 {#_5-修改业务逻辑}

修改登录接口, 补充逻辑: 调用 ShiroServiceImpl.login 方法。
修改登出接口, 补充逻辑: 调用 ShiroServiceImpl.logout 方法。

# 6. 定义请求的认证和授权规则 {#_6-定义请求的认证和授权规则}

请求的认证和授权规则, 有 2 种配置方式:

| 配置方式 | 实现方法 | 控制精度 | 灵活性 | 认证或授权失败的处理方式 | |------------|------------------------------------------------------|---------|--------------------|-------------------------------------------------------------| | 通过"url 配置" | 修改 ShiroConfig.shiroFilterFactoryBean 方法, 追加认证和授权规则。 | 粗粒度控制 | 支持动态配置 | 跳转到配置中指定的 url | | 通过注解 | 在接口定义方法上写注解 | 细粒度地控制。 | 因为注解是写死到代码中,无法动态配置 | 抛出异常。为了更友好的给用户提供错误信息,建议在全局异常捕获处理器中捕获认证或授权的所有异常,并以友好的方式返回给用户 |

为了能够捕获"注解方式"的认证或授权异常, 需要在全局异常处理类中补充如下异常处理函数:

    /**
     * shiro认证异常
     * Shiro在登录认证过程中,认证失败需要抛出的异常
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(value = AuthenticationException.class)
    public Result handleAuthenticationException(AuthenticationException ex) {
//        凭证异常
        if (ex instanceof CredentialsException) {
//            不正确的凭证
            if (ex instanceof IncorrectCredentialsException) {
                log.error("shiro认证异常, 凭证异常, 不正确的凭证");
                return Result.error("账号或密码错误");
            } else if (ex instanceof ExpiredCredentialsException) {
                log.error("shiro认证异常, 凭证异常, 凭证过期");
                return Result.error("账号或密码错误");
            }

            log.error("shiro认证异常, 凭证异常");
            return Result.error("账号或密码错误");
        }
        //账号异常
        else if (ex instanceof AccountException) {
//            并发访问异常: 多个用户同时登录时抛出
            if (ex instanceof ConcurrentAccessException) {
                log.error("shiro认证异常, 账号异常, 并发访问异常");
                return Result.error("不允许多个用户同时登录");
            } else if (ex instanceof UnknownAccountException) {
                log.error("shiro认证异常, 账号异常, 未知的账号");
                return Result.error("账号或密码错误");
            } else if (ex instanceof ExcessiveAttemptsException) {
                log.error("shiro认证异常, 账号异常, 认证次数超过限制");
                return Result.error("登录次数超过限制");
            } else if (ex instanceof DisabledAccountException) {
                log.error("shiro认证异常, 账号异常, 禁用的账号");
                return Result.error("账号已禁用");
            } else if (ex instanceof LockedAccountException) {
                log.error("shiro认证异常, 账号异常, 账号被锁定");
                return Result.error("账号被锁定");
            }

            log.error("shiro认证异常, 账号异常");
            return Result.error("账号异常");
        }
//        使用了不支持的Token
        else if (ex instanceof UnsupportedTokenException) {
            log.error("shiro认证异常, 使用了不支持的Token");
            return Result.error("认证失败");
        }

        log.error("shiro认证异常");
        return Result.error("您没有登录,无权执行此操作");
    }

    /**
     * shiro授权异常
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(value = AuthorizationException.class)
    public Result handleAuthorizationException(AuthorizationException ex) {
        if (ex instanceof UnauthorizedException) {
            log.error("shiro授权异常, 无权访问");
            return Result.error("无权访问");
        }
        //当尚未完成成功认证时, 尝试执行授权操作时引发该异常
        else if (ex instanceof UnauthenticatedException) {
            log.error("shiro授权异常, 没有通过认证无法执行授权操作");
            return Result.error("请先登录");
        }

        log.error("shiro授权异常");
        return Result.error("无权访问");
    }

# 7. 验证 {#_7-验证}

完成了如上步骤以后, 就完成了 shiro 的基本集成工作。可以进行功能验证了。

# 参考资料 {#参考资料}

Spring Boot2 整合 Shiro - 仅支持身份认证 (opens new window)
Spring Boot2 整合 Shiro - 密码加密存储 (opens new window)
https://blog.csdn.net/gnail_oug/article/details/80662553
https://segmentfault.com/a/1190000014479154
https://www.cnblogs.com/seve/p/12241197.html
原生和 starter 整合 shiro 的区别 (opens new window)

赞(4)
未经允许不得转载:工具盒子 » springboot集成用户认证授权框架shiro基础教程