Shiro学习之路(二):万字讲解如何集成Shiro

本文详细介绍了如何在Spring Boot应用中整合Shiro框架进行权限管理,包括依赖导入、数据库表设计、自定义Realm、Shiro配置、登录接口定义、权限控制以及常见异常处理。通过自定义Realm实现用户认证和授权,使用Shiro注解进行细粒度的接口权限控制,确保系统的安全性。
摘要由CSDN通过智能技术生成

1、依赖导入

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

除了shiro-spring这种依赖,其实在springboot也有集成了的依赖:shiro-spring-boot-web-starter,差别不大,我们这里使用shiro-spring

2、数据库表的建立及初始化

Shiro是不会替我们定义角色和权限的,所以我们需要设计相应的表

下面是一些相关表的设计,大家可以参考

用户表

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(50) NOT NULL,
  `salt` varchar(128) DEFAULT NULL COMMENT '加密盐值',
  `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(50) DEFAULT NULL COMMENT '联系方式',
  `sex` int(255) DEFAULT NULL COMMENT '年龄:1男2女',
  `age` int(3) DEFAULT NULL COMMENT '年龄',
  `status` int(1) NOT NULL COMMENT '用户状态:1有效; 2删除',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  PRIMARY KEY (`id`,`user_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

角色表

CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色id',,
  `name` varchar(50) NOT NULL COMMENT '角色名称',
  `description` varchar(255) DEFAULT NULL COMMENT '角色描述',
  `status` int(1) NOT NULL COMMENT '状态:1有效;2删除',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

权限表(菜单和按钮)

CREATE TABLE `permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '权限id',
  `permission_code` varchar(20) NOT NULL  COMMENT '权限编码',
  `name` varchar(100) NOT NULL COMMENT '权限名称',
  `description` varchar(255) DEFAULT NULL COMMENT '权限描述',
  `url` varchar(255) DEFAULT NULL COMMENT '权限访问路径',
  `parent_id` int(11) DEFAULT NULL COMMENT '父级权限id',
  `type` int(1) DEFAULT NULL COMMENT '类型   0:目录   1:菜单   2:按钮',
  `order_num` int(3) DEFAULT '0' COMMENT '排序',
  `icon` varchar(50) DEFAULT NULL COMMENT '图标',
  `status` int(1) NOT NULL COMMENT '状态:1有效;2删除',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

说明:parent_id用于组成一个树状的菜单结构,比如系统设置下有用户管理和角色管理两个子模块,而角色管理下有角色添加、角色修改等子模块

用户角色关系表

CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(20) NOT NULL COMMENT '用户id',
  `role_id` varchar(20) NOT NULL COMMENT '角色id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

角色权限关系表

CREATE TABLE `role_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` varchar(20) NOT NULL COMMENT '角色id',
  `permission_id` varchar(20) NOT NULL COMMENT '权限id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

注:修改关系表,我们只需要将原来的数据删除,然后插入新的数据即可。

权限设计

当我们需要将权限控制到某个菜单下的时候,可以类似如下

message表示一个消息模块,我们有一个权限是控制用户是否能访问这个模块,我们在权限表设计时,访问消息模块的权限url都为message:xxx,所以当用户拥有message:权限的时候都可以访问这个模块

例:

map.put(/message/**”,perms[message:*])

消息模块下的某个接口如下,使用@RequiresPermissions注解进行控制

   //设置权限
    @RequiresPermissions("message:getMessage")
    @PostMapping("/getMessage")
    public String getMessage(){
        return "";
    }

注:一般一个模块对应一个控制器。

shiro常用的权限控制注解,可以在控制器类上使用

注解						功能
@RequiresGuest			只有游客可以访问
@RequiresAuthentication	需要登录才能访问
@RequiresUser			已登录的用户或“记住我”的用户能访问
@RequiresRoles			已登录的用户需具有指定的角色才能访问
@RequiresPermissions	已登录的用户需具有指定的权限才能访问

想要使用shiro的注解,我们需要在配置类添加以下配置

    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

3、自定义Realm

在设计好角色和权限之后,我们就可以自定义Realm。Realm的作用是什么呢?我们再复习下身份认证的流程

1:应用程序代码调用 Subject.login(token) 方法后,传入代表最终用户身份的 AuthenticationToken 实例 Token。

2:将 Subject 实例委托给应用程序的 SecurityManager(Shiro 的安全管理)并开始实际的认证工作。

3:SecurityManager 根据具体的 Realm 进行安全认证。

自定义Realm只需要继承 AuthorizingRealm 类,然后实现下面两个方法即可

doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。也就是先先根据用户名从数据库中获取其对应的角色和权限,将其封装到 authorizationInfo 并返回给 Shiro。

doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息,doGetAuthenticationInfo主要作用是获取用户输入的用户名、密码等信息并从数据库中取出保存的密码交给shiro,由shiro的密码匹配器进行匹配。

下面是一个自定义的realm,实际项目根据自己的需求进行定义

/**
 * 自定义realm
 */
public class MyRealm extends AuthorizingRealm {

    @Resource
    private EmployeeMapper employeeMapper;

    /**
     * 该方法主要用于获取授权信息,PrincipalCollection是一个用于标识属性的安全术语,如用户名或用户id或号码
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //创建一个SimpleAuthorizationInfo对象用于存储授权信息
        SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
        //获取用户凭证,就是用户名、帐号
        String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();
        //获取用户对应的角色
        List<String> userRoles = employeeMapper.getUserRoles(primaryPrincipal);
        //将用户角色信息设置进simpleAuthorizationInfo
        simpleAuthorizationInfo.setRoles(new HashSet<>(userRoles));

        //获取用户对应的权限
        List<String> userPermissions = employeeMapper.getUserPermissions(primaryPrincipal);
        simpleAuthorizationInfo.setStringPermissions(new HashSet<>(userPermissions));
        return simpleAuthorizationInfo;
    }

    /**
     * 该方法用于认证当前用户,根据用户输入的用户名,在数据库中查找到用户记录,并用查到的用户对象、数据库中存储的密码、密码盐(没有则不传)和Realm对象名字构建一个认证信息对象(SimpleAuthenticationInfo)交给系统进行密码验证。
     * @param authenticationToken AuthenticationToken包含了用户在身份验证时提交的帐户和凭证
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取用户名/帐号
        String username = (String) authenticationToken.getPrincipal();
        // 根据用户名从数据库中查询该用户
        Employee employee = employeeMapper.selectEmployeeByName(username);
        if (employee!=null &&employee.getId()!=null){
            // 把当前用户存到 Session 中
            SecurityUtils.getSubject().getSession().setAttribute("employee", employee);
            //传入用户名和密码进行身份认证,并返回认证信息。第一个参数是用户名,第二个是密码,第三个是realm
            SimpleAuthenticationInfo myRealm = new SimpleAuthenticationInfo(employee.getName(), employee.getPassword(), "myRealm");
            return myRealm;
        }else {
            //如果用户不存在,则抛出UnknownAccountException异常
            throw new UnknownAccountException("账号不存在");
        }
    }
}

4、Shiro 配置

Shiro需要配置后才可以生效,Shiro配置主要包括三个东西:自定义 Realm、安全管理器 SecurityManager 和 Shiro 过滤器。

自定义Realm我们已经定义定义好了,只需要注入即可

SecurityManager是Shiro核心,主要协调Shiro内部的各种安全组件

ShiroFilterFactoryBean :用于在基于spring的web应用中定义Shiro过滤器。

ShiroFilterFactoryBean 中的一些配置

  • loginUrl:没有登录的用户请求需要登录的页面时自动跳转到登录页面。

  • unauthorizedUrl:没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面。

  • successUrl:登录成功默认跳转页面,不配置则跳转至”/”,可以不配置,直接通过代码进行处理。

  • securityManager:这个属性是必须的,配置为我们定义的securityManager就好了。

  • filterChainDefinitions或者filterChainDefinitionMap:配置过滤规则,从上到下的顺序匹配。

下面再介绍下shiro过滤器,方便我们配置过滤规则

配置缩写		对应的过滤器					功能
anon		AnonymousFilter				指定url可以匿名访问
authc		FormAuthenticationFilter	指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到				loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返				回的信息都可以定制嘛。
authcBasic	BasicHttpAuthenticationFilter	指定url需要basic登录
logout		LogoutFilter				登出过滤器,配置指定url(即ShiroFilterFactoryBean.setLoginUrl设置的url)就可以实现退出功能,非常方便
noSessionCreation	NoSessionCreationFilter	禁止创建会话
perms		PermissionsAuthorizationFilter	需要指定权限才能访问
port		PortFilter					需要指定端口才能访问
rest		HttpMethodPermissionFilter	将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
roles		RolesAuthorizationFilter	需要指定角色才能访问
ssl			SslFilter					需要https请求才能访问
user		UserFilter					需要已登录或“记住我”的用户才能访问

下面是一个简单的shiro配置类,大家可以参考

/**
 * shiro配置类
 */
@Configuration
public class ShiroConfig {

    /**
     * 注入自定义的realm
     */
    @Bean
    public MyRealm myRealm(){
        return new MyRealm();
    }


    /**
     * 配置SecurityManager并设置Reaml
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
        defaultSecurityManager.setRealm(myRealm());
        return defaultSecurityManager;
    }


    /**
     * 配置过滤器
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //没有登录的用户请求需要登录的页面时自动跳转到登录页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //没有权限默认跳转的页面,登录的用户访问了没有被授权的资源自动跳转到的页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
        //配置过滤规则,使用LinkedHashMap保持规则的顺序,因为过滤规则是从上到下匹配的
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        filterChainDefinitionMap.put("/login", "anon");
        //配置logout过滤器
        filterChainDefinitionMap.put("/logout", "logout");

        //这行代码必须放在所有权限设置的最后,不然会导致所有url都被拦截,都需要认证
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;

    }


}

5、定义登录接口

在后台登录接口中,接收用户名密码,据此创建一个usernamePasswordToken令牌,交由Shiro并调用login()方法进行登录,然后就会调用自定义 Realm的doGetAuthenticationInfo 方法,如果不抛出任何异常表明登录成功,如果抛出异常,我们可以根据异常种类返回提示出错信息给用户。

下面是一个简单的登录接口例子

@RestController
public class LoginController {

    @PostMapping("/login")
    public String login(@RequestParam("username")String username,@RequestParam("password")String password){
        //从SecurityUtils中创建一个subject
        Subject subject = SecurityUtils.getSubject();
        //创建用于认证的token
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
        try {
            // 执行认证登陆
            subject.login(usernamePasswordToken);
        } catch (UnknownAccountException uae) {
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
            return "密码不正确";
        } catch (LockedAccountException lae) {
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
            return "用户名或密码不正确!";
        }catch (Exception e){
            return "登录失败!";
        }
        if (subject.isAuthenticated()){
            return "登录成功!";
        }else {
            return "登录失败!";
        }
    }
}

注:参数使用表单传输

6、权限控制

shiro可以使用url配置控制权限,也可以在控制器类上使用注解控制权限。一般我们是同时使用两种配置方式,用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制。url控制权限是在shiro配置类里进行配置,我们上面已经简单的配置了,下面我们使用shiro注解进行接口的权限控制

@RequiresPermissions注解标识接口需要用户拥有哪些权限才可以访问

例:

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private EmployeeMapper employeeMapper;

    @PostMapping("list")
    @RequiresPermissions({"employee:update"})
    public List<Employee>list(){
        return employeeMapper.selectEmployeeList();
    }
}

shiro注解中多个权限和角色之间默认是“与”关系,如果想实现的是“或”关系,那么将logical配置成Logical.OR即可,如下

    @PostMapping("list")
    @RequiresPermissions(value = {"employee:update","employee:list"},logical = Logical.OR)
    public List<Employee>list(){
        return employeeMapper.selectEmployeeList();
    }

当我们进行接口访问的时候,shiro就会校验登录的用户是否拥有对应的角色或者权限,如果没有那么就会进入某个页面或者抛出异常,跳转的页面是由setUnauthorizedUrl决定的

7、Shiro常见异常

1、AuthencationException: AuthenticationException 异常是Shiro在登录认证过程中,认证失败需要抛出的异常。

AuthenticationException包含以下子类:

CredentitalsException :凭证异常

IncorrectCredentialsException 不正确的凭证
ExpiredCredentialsException 凭证过期

AccountException: 账号异常

ConcurrentAccessException 并发访问异常(多个用户同时登录时抛出)
UnknownAccountException 未知的账号
ExcessiveAttemptsException 认证次数超过限制
DisabledAccountException 禁用的账号
LockedAccountException 账号被锁定

UnsupportedTokenException: 使用了不支持的Token

2、AuthorizationException:授权相关的异常

子类:

UnauthorizedException:抛出以指示请求的操作或对请求的资源的访问是不允许的。

UnanthenticatedException:当尚未完成成功认证时,尝试执行授权操作时引发异常。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值