SpringBoot整合Shiro实现权限管理

目录

概述

系统安全性是目前大大小小软件项目都会考虑的问题,这里我们来认识认识Apache的Shiro框架,了解如何用它来实现权限管理。

RBAC权限管理

问题:在企业OA系统中,部门主管和普通员工在系统中看到的菜单和操作的功能应该一样吗?

在企业中员工的职责不同,登录软件系统后不同的用户对于数据的操作和查询权限也肯定不应该相同,否则对于企业来说,内部管理和安全性都会出现问题。

RBAC(Role Based Access Control )权限管理,基于角色的访问控制。

主要对象:

1)用户

2)角色

3)权限

不同的用户登录系统后,拥有不同的角色,不同的角色可以对系统资源执行不同的操作。

用户和角色是多对多关系,角色和权限是多对多关系。

SpringBoot整合Shiro实现权限管理

Shiro介绍

Apache的Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

主要API

  • SecurityManager 安全管理器,完成核心业务

  • Subject 提供方法给开发者调用

  • Realm 提供用户登录和授权的数据

  • SecurityUtils 工具类,用于整合其他的组件

SpringBoot整合Shiro实现权限管理

Shiro入门

1、 添加依赖

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

2、在resources下添加shiro.ini文件,内容如下

[users]
zhang=123,role1,role2
wang=123,role2

[roles]
role1=user:create,user:update
role2=user:create,user:delete
role3=user:create

shiro.ini是Shiro的配置文件,[users]是用户配置,格式是:账号=密码,角色1,角色2…

[roles]是权限配置,格式是:角色=权限1,权限2…

3、测试

//创建基于Ini文件的安全管理器工厂
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//获得安全管理器
SecurityManager instance = factory.getInstance();
//配置安全管理器
SecurityUtils.setSecurityManager(instance);
//获得Subject对象
Subject subject = SecurityUtils.getSubject();
//创建账号密码token
UsernamePasswordToken user = new UsernamePasswordToken("wang", "123");
//登录验证
subject.login(user);
//权限判断
System.out.println("是否登录成功:" + subject.isAuthenticated());
System.out.println("是否拥有role1角色:" + subject.hasRole("role1"));
System.out.println("是否拥有delete权限:" + subject.isPermitted("user:delete"));

使用wang登录的效果:

SpringBoot整合Shiro实现权限管理

使用zhang登录的效果:

SpringBoot整合Shiro实现权限管理

密码错误会抛出异常:

SpringBoot整合Shiro实现权限管理

自定义Realm

上面的案例中使用ini文件配置用户、密码、角色和权限等,过于简单,不适合企业级项目的使用,真正项目中用户、角色、权限这些重要数据都保存在数据库中,需要我们通过自己编写的类和方法实现登录和授权。

AuthorizingRealm类实现Realm接口,提供两个方法:

  • doGetAuthenticationInfo 返回登录验证信息,该方法在subject执行login方法后调用
  • doGetAuthorizationInfo 返回用户授权信息,该方法在subject进行权限判断时调用

1) 定义Realm类,此案例为简单起见,账号密码是固定的zhang,123,添加角色role1,添加权限user:select、user:insert、user:delete

/**
 * 用户Realm
 */
public class MyRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获得登录用户
        String username = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("登录用户授权:" + username);
        //授权信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //添加角色
        info.addRole("role1");
        //添加权限
        info.addStringPermission("user:select");
        info.addStringPermission("user:insert");
        info.addStringPermission("user:delete");
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获得用户输入的账号
        String username = authenticationToken.getPrincipal().toString();
        if(!username.equals("zhang")){
            throw new UnknownAccountException("此用户不存在");
        }
        //返回验证信息,参数:1、用户名 2、正确密码 3、realm名称
        return new SimpleAuthenticationInfo(username,"123", getName());
    }
}

2) 使用自定义Realm进行登录和授权

//创建默认安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//安全管理器配置自定义Realm
securityManager.setRealm(new MyRealm());
//SecurityUtils配置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//获得Subject对象
Subject subject = SecurityUtils.getSubject();
//创建账号密码token
UsernamePasswordToken user = new UsernamePasswordToken("zhang", "123");
//登录验证
subject.login(user);
//权限判断
System.out.println("是否拥有role1角色:" + subject.hasRole("role1"));
System.out.println("是否拥有delete权限:" + subject.isPermitted("user:delete"));

密码加密加盐

用户密码一般不会以明文方式保存,这样无法保证安全性,所以一般都需要加密。

SimpleHash类可以实现基本的加密,几种创建方式:

new SimpleHash("加密算法","原始密码")
new SimpleHash("加密算法","原始密码",盐)
new SimpleHash("加密算法","原始密码",盐,迭代次数)

参数说明:

加密算法一般使用常用的md5算法

盐的作用是提高密码安全性,如两个用户的原始密码都是123,则加密后的密文都是相同的,如果破解了一个用户的密码,另一个用户的密码也一同破解了,如果给密码加盐,每个用户的盐不同,加密后密码就都会不同,增加了破解难度。

迭代次数是加密一次后,再对密文再次加密,也能提高安全性。

下面我们以md5算法对“123456”加密,盐是“007”,迭代次数为10。

SimpleHash md5 = new SimpleHash("md5", "123456",ByteSource.Util.bytes("007"), 10);
System.out.println(md5);
输出:44202d045439dc33a2e43d2828d08e19

修改MyRealm的doGetAuthenticationInfo方法,这里将密文和盐直接写在代码中,实际应用时密文和盐是通过用户名从数据库中查询出来的。

//返回验证信息,参数:1、用户名 2、正确密码 3、盐 4、realm名称
return new SimpleAuthenticationInfo(username,"44202d045439dc33a2e43d2828d08e19", ByteSource.Util.bytes("007"),getName());

需要给自定义Realm添加密码匹配器

//创建默认安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//安全管理器配置自定义Realm
MyRealm realm = new MyRealm();
//创建密码匹配器
HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("md5");
//设置迭代次数
md5.setHashIterations(10);
//配置匹配器
realm.setCredentialsMatcher(md5);
securityManager.setRealm(realm);
//SecurityUtils配置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//获得Subject对象
Subject subject = SecurityUtils.getSubject();
//创建账号密码token
UsernamePasswordToken user = new UsernamePasswordToken("zhang", "123456");
//登录验证
subject.login(user);
//权限判断
System.out.println("是否登录成功:" + subject.isAuthenticated());
System.out.println("是否拥有role1角色:" + subject.hasRole("role1"));
System.out.println("是否拥有delete权限:" + subject.isPermitted("user:delete"));

SpringBoot整合Shiro实现权限管理

SpringBoot+MyBatis+Shiro整合

1、表设计

SpringBoot整合Shiro实现权限管理

  • s_user 用户表

  • s_role 角色表

  • s_menu 菜单表(权限表)

  • s_user_menu 用户角色中间表

  • s_role_menu 角色菜单中间表

2、添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.16</version>
</dependency>

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.7.0</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

2、SpringBoot配置

# jdbc配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/erp_db?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
# mybatis配置
mybatis-plus.type-aliases-package=com.blb.blb_erp.entity
mybatis-plus.mapper-locations=classpath:mapper/*.xml
# shiro配置
# 登录页面
shiro.loginUrl=/pages/login.html
# 登录失败跳转页面
shiro.unauthorizedUrl=/pages/failed.html
# 登录成功跳转页面
shiro.successUrl=/pages/index.html

3、编写Mapper接口

需要三个方法:

  1. 按用户名查找用户
  2. 按用户id查询所有菜单
  3. 按用户id查询所有角色
/**
 *  用户接口
 */
public interface SUserMapper extends BaseMapper<SMenu>{

    /**
     * 通过用户名查询用户
     * @param username
     * @return
     */
    SUser selectUserByUsername(String username);
}
映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blb.blb_erp.mapper.SUserMapper">

    <select id="selectUserByUsername" resultType="SUser">
        select * from s_user where user_name = #{username}
    </select>
</mapper>
/**
 *  菜单接口
 */
public interface SMenuMapper extends BaseMapper<SMenu>{
    /**
     * 根据userId查询所有权限
     * @param userId
     * @return
     */
    List<SMenu> selectMenusByUserId(String userId);
}
映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blb.blb_erp.mapper.SMenuMapper">

    <select id="selectMenusByUserId" resultType="SMenu">
        select m.* from s_user u,s_role r,s_user_role ur,s_menu m,s_role_menu rm
        where ur.role_id = r.id and ur.user_id = u.id and rm.role_id = r.id and rm.menu_id = m.id
        and u.id = #{userId}
    </select>
</mapper>
/**
 *  角色接口
 */
public interface SRoleMapper extends BaseMapper<SMenu>{

    /**
     * 根据用户id查询所有角色
     * @param userId
     * @return
     */
    List<SRole> selectRolesByUserId(String userId);
}
映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blb.blb_erp.mapper.SRoleMapper">

    <select id="selectRolesByUserId" resultType="SRole">
        select r.* from s_user_role ur
        join s_user u on ur.user_id = u.id
        join s_role r on ur.role_id = r.id
        where  ur.user_id = #{userId}
    </select>
</mapper>

4、自定义Realm

/**
 * 用户Realm
 */
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private SUserMapper userMapper;
    @Autowired
    private SRoleMapper roleMapper;
    @Autowired
    private SMenuMapper menuMapper;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获得用户对象
        SUser user = (SUser) principalCollection.getPrimaryPrincipal();
        //查询权限和角色
        List<SMenu> menus = menuMapper.selectMenusByUserId(user.getId());
        List<SRole> roles = roleMapper.selectRolesByUserId(user.getId());
        //保存权限和角色名称的集合
        List<String> strRoles = new ArrayList<>();
        roles.forEach(r -> strRoles.add(r.getRoleName()));
        List<String> strMenus = new ArrayList<>();
        menus.forEach(m -> strMenus.add(m.getMenuName()));
        //返回带有角色和权限名称的授权信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(strRoles);
        info.addStringPermissions(strMenus);
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获得账号
        String username = authenticationToken.getPrincipal().toString();
        //通过账号查询用户
        SUser user = userMapper.selectUserByUsername(username);
        if(user == null){
            throw new UnknownAccountException("此用户不存在");
        }
        //返回验证信息  参数:1、用户对象 2、正确密码 3、盐 4、realm名称
        return new SimpleAuthenticationInfo(user,user.getPassWord(), ByteSource.Util.bytes(user.getSalt()),getName());
    }
}

5、Shiro配置类

/**
 * Shiro配置
 */
@Configuration
public class ShiroConfig {

    //返回Realm
    @Bean
    public UserRealm myRealm(){
        UserRealm myRealm = new UserRealm();
        //设置密码匹配器
        HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("md5");
        md5.setHashIterations(10);
        myRealm.setCredentialsMatcher(md5);
        //关闭缓存
        myRealm.setCachingEnabled(false);
        return myRealm;
    }

    //返回面向Web开发的安全管理器
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(){
        DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
        //设置自定义Realm
        sm.setRealm(myRealm());
        return sm;
    }

    //返回Shiro过滤器链定义
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        //定义过滤器链key为url,value为anon不验证,authc验证
        //anon在前,authc在后,需要使用LinkedHashMap保留顺序
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("/pages/login.html","anon");
        map.put("/user/login","anon");
        map.put("/**","authc");
        chainDefinition.addPathDefinitions(map);
        return chainDefinition;
    }

    //启动thymeleaf的shiro标签
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }

}

6、启动类

@MapperScan("com.blb.blb_erp.mapper")
@SpringBootApplication
public class BlbErpApplication {

    public static void main(String[] args) {
        SpringApplication.run(BlbErpApplication.class, args);
    }

}

7、控制器

@Data
@AllArgsConstructor
@NoArgsConstructor
public class JsonResult {
    private int code;
    private Object data;

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

    @PostMapping("/login")
    public JsonResult login(String username,String password){
        //创建Token
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //获得subject
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return new JsonResult(1,"登录成功");
        }catch (AuthenticationException ex){
            ex.printStackTrace();
        }
        return new JsonResult(0,"账号或密码错误");
    }
    
    @RequiresRoles("管理员")
    @GetMapping("/role-admin")
    public String testRole(){
        return "有管理员角色";
    }

    @RequiresPermissions("部门管理")
    @GetMapping("/menu-dept")
    public String testMenu(){
        return "有部门管理权限";
    }
}

@RequiresRoles、@RequiresPermissions写在控制器的方法上,登录用户有对应的角色和权限才能访问。

RememberMe

可以在登录页面上添加记住我功能,勾选后下次不用登录直接进去系统了

1、页面上添加RememberMe复选框

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link rel="stylesheet" href="/elementui/index.css">
    <style>
        .box-card{
            margin:200px auto;
            width: 480px;
        }
        .clearfix{
            text-align: center;
            color:#303133;
            font-size: 18px;
        }
        .login-form{
            width: 400px;
        }
    </style>
</head>
<body>
<div id="app">
    <el-card class="box-card">
        <div slot="header" class="clearfix">
            <span>系统登录</span>
        </div>
       <el-form class="login-form" ref="form"  :model="form" label-width="80px">
            <el-form-item label="账号">
           <el-input v-model="form.username"></el-input>
           </el-form-item>
           <el-form-item label="密码">
               <el-input type="password" v-model="form.password"></el-input>
           </el-form-item>
           <el-form-item >
               <el-checkbox v-model="form.rememberMe">记住我</el-checkbox>
           </el-form-item>
           <el-form-item>
               <el-button type="primary" @click="login">登 录</el-button>
               <el-button>取 消</el-button>
           </el-form-item>
        </el-form>
    </el-card>
</div>
<script src="/vue/vue.js"></script>
<script src="/elementui/index.js"></script>
<script src="/axios/axios.min.js"></script>
<script src="/qs/qs.min.js"></script>
<script>
    new Vue({
       el:"#app",
        data:{
           form:{username:"",password:"",rememberMe:false}
        },
        methods:{
           login:function () {
               //Qs.stringify(this.form) 将form由{xx:值} 转为 xx=值&xx=值
               axios.post("/user/login",Qs.stringify(this.form))
                   .then(res=>{
                      if(res.data.code == 1){
                          location.href = "/pages/index.html";
                      }
                   });
           }
        }
    });
</script>
</body>
</html>

2、修改登录方法

@PostMapping("/login")
public JsonResult login(String username,String password,Boolean rememberMe){
    //创建Token
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //设置记住我
    token.setRememberMe(rememberMe);
    //获得subject
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.login(token);
        return new JsonResult(1,"登录成功");
    }catch (AuthenticationException ex){
        ex.printStackTrace();
    }
    return new JsonResult(0,"账号或密码错误");
}

3、修改Shiro配置类

//创建RememberMe管理器
public CookieRememberMeManager rememberMeManager(){
    CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
    SimpleCookie rememberMe = new SimpleCookie("rememberMe");
    //单位是秒 过期时间
    rememberMe.setMaxAge(60 * 10);
    rememberMeManager.setCookie(rememberMe);
    return rememberMeManager;
}

//返回面向Web开发的安全管理器
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
    DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
    //设置自定义Realm
    sm.setRealm(myRealm());
    //设置RememberMe
    sm.setRememberMeManager(rememberMeManager());
    return sm;
}

//返回Shiro过滤器链定义
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    ...
    //这是使用user,和authc的区别是:authc必须经过验证,user经过验证或RememberMe都可以登录
    map.put("/**","user");
    chainDefinition.addPathDefinitions(map);
    return chainDefinition;
}

SpringBoot整合Shiro实现权限管理

选择记住我登录系统,关闭浏览器再次打开系统页面后,不需要登录直接进入系统。

结束

本文如果对你有帮助,请点个赞哦: )

上一篇:中小型企业可参考的类MySQL双主架构方案


下一篇:MySQL存储引擎5(笔记)