springboot集成shiro

shiro简介

Shiro是Apache旗下的一个开源项目,它是一个非常易用的安全框架,提供了包括认证、授权、加密、会话管理等功能。Shiro属于轻量级框架,相对于Spring Security简单很多,
并没有security那么复杂,很容易上手。(可以绕过简介,先看示例,回过头再看简介内容)

  • 主要功能
  • 验证用户身份
  • 用户访问权限控制
  • 支持单点登录(SSO)功能
  • 可以响应认证、访问控制,或Session事件
  • 支持提供“Remember Me”服务
  • 框架结构
    Shiro 的整体框架大致如下图所示(图片来自互联网)
  • spring boot集成sonarqube springboot集成sso_User

  • Authentication(认证):用户身份识别,也就是看用户名和密码是否正确。
  • Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
  • Session Management(会话管理):特定于用户的会话管理,甚至在非web 应用程序。
  • Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
  • Web支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
  • 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
  • 并发:Apache Shiro 支持多线程应用程序的并发特性。
  • 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
  • “Run As”:这个功能允许用户在许可的前提下假设另一个用户的身份。
  • “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录

::: tip 提示
本示例不讲springboot搭建和集成mybatis等基础知识,没有这方面基础的可以学习前面的文章5springboot集成mybatis :::

数据库设计

我们进行权限控制的时候,一般是希望一个用户可以有一个或者多个角色,一个角色又对应多个权限。我们创建5张表来控制权限

  1. 用户表sys_user
  2. 角色表sys_role
  3. 用户角色关系表sys_user_role
  4. 权限表sys_permission
  5. 角色权限关系表sys_role_permission

创建表

  1. 创建用户表sys_user并且插入两条数据
CREATE TABLE `sys_user` (
  `id` varchar(64) NOT NULL COMMENT '主键id',
  `username` varchar(100) DEFAULT NULL COMMENT '登录账号',
  `password` varchar(100) DEFAULT NULL COMMENT '密码',
  `salt` varchar(100) DEFAULT NULL COMMENT '电子邮件',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';

INSERT INTO `sys_user` VALUES ('user01', 'test01', '111', '2');
INSERT INTO `sys_user` VALUES ('user02', 'test02', '111', '2');
  1. 创建角色表sys_role并且插入1条数据
CREATE TABLE `sys_role` (
  `id` varchar(64) NOT NULL,
  `role` varchar(255) DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `sys_role` VALUES ('role01', 'admin', '管理员');
  1. 创建用户角色关系表sys_user_role并且插入2条数据
CREATE TABLE `sys_user_role` (
  `user_id` varchar(64) DEFAULT NULL,
  `role_id` varchar(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `sys_user_role` VALUES ('user01', 'role01');
INSERT INTO `sys_user_role` VALUES ('user02', 'role01');
  1. 创建权限表sys_permission并且插入2条数据
CREATE TABLE `sys_permission` (
  `id` varchar(64) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `parent_id` varchar(64) DEFAULT NULL,
  `resource_type` varchar(255) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  `permission` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `sys_permission` VALUES ('p02', '新增用户', 'p1', 'button', 'sysUser/add', 'sysUser:add');
INSERT INTO `sys_permission` VALUES ('p03', '查询所有用户', 'p1', 'button', 'sysUser/listAll', 'sysUser:listAll');
INSERT INTO `sys_permission` VALUES ('p1', '用户管理', null, 'menu', 'sysUser/list', 'sysUser:list');
  1. 创建角色权限关系表sys_role_permission并且插入3条数据
CREATE TABLE `sys_role_permission` (
  `role_id` varchar(64) DEFAULT '',
  `permission_id` varchar(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `sys_role_permission` VALUES ('role01', 'p1');
INSERT INTO `sys_role_permission` VALUES ('role01', 'p02');
INSERT INTO `sys_role_permission` VALUES ('role01', 'p03');

::: tip 提示
这个表结构和关系只是举例子,大家理解原理后可以根据自己业务需求去创建表结构。如果一个用户只有一个角色的话,就不用用户角色关系表,直接在用户表里面加个role_id就可以了。
:::

添加依赖

在正常的springboot项目中,加入shiro依赖包

<!--shiro-->
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring-boot-starter</artifactId>
  <version>1.4.1</version>
</dependency>

创建ShiroConfig

添加一个Shiro配置类,主要配置路由的访问控制,以及注入自定义的认证器MyShiroRealm。

@Configuration
@Slf4j
public class ShiroConfig {
    /**
     * Filter Chain定义说明
     * 1、一个URL可以配置多个Filter,使用逗号分隔
     * 2、当设置多个过滤器时,全部验证通过,才视为通过
     * 3、部分过滤器可指定参数,如perms,roles
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        log.info("**********shiroFilter");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        filterChainDefinitionMap.put("/sysUser/login", "anon");
        // 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
        //authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
        filterChainDefinitionMap.put("/**", "authc");
        // 没有登录的用户请求需要登录的页面时自动跳转到的页面(或者url),如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/sysUser/unlogin");
        // 登录的用户访问了没有被授权的资源自动跳转到的页面(或者url)
        shiroFilterFactoryBean.setUnauthorizedUrl("/sysUser/url403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(MyShiroRealm myRealm) {
        log.info("**********securityManager");
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);
        return securityManager;
    }
}
  • 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
  • filterChainDefinitionMap:url的拦截器,可以指定哪些url是白名单,哪些url需要认证才能访问等
  • setLoginUrl:没有登录的用户请求需要登录的页面时自动跳转到的页面(或者url),如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
  • setUnauthorizedUrl:登录的用户访问了没有被授权的资源自动跳转到的页面(或者url)
    ::: tip 提示
    shiro的内置过滤器
  • anon:无需认证就可以访问 默认
  • authc:必须认证了才能访问
  • user:必须拥有记住我功能才能访问
  • perms:必须拥有对某个的权限才能访问
  • role:拥有某个角色权限才能访问
    如果不想深究这个,你只是想登录成功后才能访问一些url,那么只使用anon和authc就可以了,anon过滤白名单,authc过滤所有非白名单的url就可以了。
    :::

创建MyShiroRealm

添加一个MyShiroRealm并继承AuthorizingRealm,实现其中的两个方法。

  • doGetAuthenticationInfo:实现用户认证,通过服务加载用户信息并构造认证对象返回。(用户登录判断,用户名和密码正确才可以登录成功)
  • doGetAuthorizationInfo:实现权限认证,通过服务加载用户角色和权限信息设置进去。(登录成功后,用户请求的时候判断用户是否有该url权限)
@Component
@Slf4j
public class MyShiroRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService sysUserService;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //获取用户登录信息
        SysUser sysUser  = (SysUser)principals.getPrimaryPrincipal();
        // 循环添加用户角色信息
        for(SysRole sysRole:sysUser.getRoleList()){
            authorizationInfo.addRole(sysRole.getRole());
            // 循环添加所有权限信息(sysPermission.getPermission()取出来的其实就是一个标识,数据库里面怎么录入的,@RequiresPermissions验证的时候就用什么)
            for(SysPermission sysPermission:sysRole.getSysPermissionList()){
                authorizationInfo.addStringPermission(sysPermission.getPermission());
            }
        }
        return authorizationInfo;
    }

    /**
     * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。
     * @param authToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken)
            throws AuthenticationException {
        log.info("权限配置-->MyShiroRealm.doGetAuthenticationInfo()");
        // 获取用户的输入的账号.
        String userName = (String)authToken.getPrincipal();
        //实际项目中,这里可以根据实际情况做缓存,不用每次登录都查询数据库
        SysUser sysUser = sysUserService.getUserByName(userName);
        log.info("权限配置-->doGetAuthenticationInfo-->sysUser="+sysUser);
        if(sysUser == null){
            return null;
        }
        // 进行认证,将正确数据给shiro处理,密码不用自己比对,shiro会自己处理我们查询出来的sysUser的密码和登录传入的password
        /*	第一个参数可以放user对象,
         *  第二个参数必须放密码,
         *  第三个参数放 当前realm的名字,因为可能有多个realm*/
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                sysUser, //用户信息
                sysUser.getPassword(), //密码
                getName()  //realm name
        );
        //清除之前的授权信息
        super.clearCachedAuthorizationInfo(authenticationInfo.getPrincipals());
        // 存入用户对象
        SecurityUtils.getSubject().getSession().setAttribute("loginUser", sysUser);
        // 返回给安全管理器,securityManager,由securityManager比对数据库查询出的密码和页面提交的密码。如果有问题,向上抛异常,一直抛到控制器
        return authenticationInfo;
    }
}

创建测试controller

@RestController
@RequestMapping("sysUser")
public class SysUserController {

    @Autowired
    private SysUserService sysUserService;

    @RequestMapping("save")
    public String save(SysUser sysUser){
        sysUserService.save(sysUser);
        return "添加成功";
    }

    /**
     * 测试不加@RequiresPermissions的时候,只要登录成功就可以访问
     * @return
     */
    @RequestMapping("listAll")
    public Object listAll(){
        return sysUserService.findAll();
    }

    /**
     * 测试必须拥有sysUser:list权限才可以访问,数据库有该权限
     * @return
     */
    @RequiresPermissions("sysUser:list")
    @RequestMapping("list")
    public Object list(){
        //为了演示,内容与listAll一样
        return sysUserService.findAll();
    }

    /**
     * 测试接口,数据库里面没有添加sysUser:list2权限,测试登录成功后,执行该方法,会报一个没有权限的异常
     * @return
     */
    @RequiresPermissions("sysUser:list2")
    @RequestMapping("list2")
    public Object list2(){
        //为了演示,内容与listAll一样
        return sysUserService.findAll();
    }

    @RequestMapping("login")
    public Object login(SysUser sysUserLogin){
        Map<String, Object> result = new HashMap<>();
        // 不用自己验证,直接使用shiro验证用户名密码
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(sysUserLogin.getUsername(), sysUserLogin.getPassword());
        try {
            // 执行subject.login(token)后会执行MyShiroRealm的AuthenticationInfo进行身份认证
            subject.login(token);
            //登录成功后,会向前台返回一个sessionid
            result.put("token", subject.getSession().getId());
            result.put("msg", "登录成功");
        } catch (IncorrectCredentialsException e) {
            result.put("msg", "密码错误");
        } catch (LockedAccountException e) {
            result.put("msg", "登录失败,该用户已被冻结");
        } catch (AuthenticationException e) {
            result.put("msg", "该用户不存在");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 没有登录时返回前台的信息
     * @return
     */
    @RequestMapping("unlogin")
    public Object unlogin(){
        Map<String, Object> result = new HashMap<>();
        result.put("message","未登录,请您先登录");
        return result;
    }

    /**
     * 登录后没有权限时返回前台的信息
     * @return
     */
    @RequestMapping("url403")
    public Object url403(){
        Map<String, Object> result = new HashMap<>();
        result.put("message","未登录403,请您先登录");
        return result;
    }

}
  • 未登录时
    在页面输入:http://localhost:8088/moyundong/sysUser/list
    会在页面提示{"message":"未登录,请您先登录"}
  • 登录后(在页面用http://localhost:8088/moyundong/sysUser/login?username=test01&password=111进行登录,一般登录都是post请求,为了方便测试就用了get方式)
  • 在页面输入:http://localhost:8088/moyundong/sysUser/list,会查询出用户信息
  • 在页面输入:http://localhost:8088/moyundong/sysUser/listAll,会查询出用户信息
  • 在页面输入:http://localhost:8088/moyundong/sysUser/list2,控制台会报异常org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method ::: tip 提示
  • service、dao等代码就不列出来了,有需要的朋友可以在文末下载代码
  • 如果不是在浏览器测试,使用的是postman等工具测试的化,先调用登录接口,会返回一个32位的JSESSIONID,其它接口调用的时候必须在headers的cookie里面加上JSESSIONID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    这样shiro就会知道该用户是已经登录过的
    :::

执行顺序

  • 没有登录的时候,非白名单的url请求最终都会跳转到setLoginUrl设置的url
  • 登录时执行subject.login(token)后会执行MyShiroRealm的AuthenticationInfo进行身份认证,认证成功后会返回一个32位的JSESSIONID
  • 登录后执行没有@RequiresPermissions的请求的时候(cookie里面必须带上32位的JSESSIONID),会直接成功
  • 登录后执行有@RequiresPermissions的请求的时候(cookie里面必须带上32位的JSESSIONID),会进入MyShiroRealm的AuthorizationInfo方法进行权限认证
  • 在浏览器执行登录成功的时候会默认把JSESSIONID放到headers的cookie里面,其它接口直接调用就可以
  • 在测试工具执行登录成功的时候应手动把JSESSIONID放到headers的cookie里面,其它接口才可以调用成功

总结

这个是shiro最基本的用法,算是最基础的入门。我们没有使用缓存,没有对密码进行加密。