Springboot集成mybatis框架、Shiro权限管理框架

最近入职了第一份Java后端开发的工作,在正式干活之前,部门老大首先给我派了个小任务,给部门的员工进行信息登记。要求每个员工首先注册自己信息,而员工的任何删改查操作都需要登录,并且只能查询或者修改自己的信息,部门老大需要所有权限。

这个小需求我用Springboot、Mybatis、Thymeleaf、Shiro、MySql做了个小网站,连接池用的Druid,分页插件使用Pagehelper,日志使用的默认日志。

为了记录一下整个项目的过程,并且巩固一下自己的知识,在完成之后,重新建立了一个小demo,重点重现了该Web环境(Mybatis及逆向工程、Shiro权限管理框架集成)的搭建过程。

完成之后,项目的目录结构如下:

image-20200604154705364

image-20200604154734918

一 Springboot的web项目创建

这个就没有什么好讲的,我们用IDEA直接创建一个Springboot的Web环境即可。

application.yml文件如下:

server:
  port: 80
spring:
  datasource:
    name: demo
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/shiroexample?useUnicode=true&charactorEncoding=utf-8&serverTimeZone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  mapper-locations: classpath*:/mybatis/mapper/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
  level:
    root: info
    com.hebin.shiroexample.mapper: debug
  file: log/log.log

一般Springboot的版本无需使用高版本(本项目使用2.1.14),版本太高会有插件或者工具因为未更新导致未知原因错误。插件选择图中几个,也可以一个不选在最后POM中手动添加依赖坐标,因为本文不展示Thymeleaf的使用,所以无需勾选Thymeleaf。其中Lombok插件用于简化JavaBean的开发,省略Getter/Setter/toString等方法代码,Spring Web是必须使用的,Mybatis和Mysql用于持久层。

image-20200604122242265

POM文件,dependencies如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
       
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.17</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

二 Apache Shiro

关于Shiro的介绍和使用请看王诗林的这篇文章springboot整合shiro(完整版),因为我使用的也不是很熟练,我这里定性的讲一下Shiro权限验证和授权的大概原理。

首先,在一个项目中,会有用户,而用户除了用户名、密码等常规属性之外,还具有各种角色。比如论坛项目,角色有:管理员,版主,普通注册用户,游客等角色,同时应该注意到,一个用户可以同时拥有多个角色,是个一对多的关系。而一个角色会有各种权限,也是一对多的关系。例如管理员有封禁、修改用户角色、发帖、删帖等权限,而游客只有浏览权限。

因此,在Shiro框架下,至少有三个实体类:User,Role,Permission,对应有三张数据库表,同时也应该建立两张关联表,将用户与角色的关系User_Role,角色与权限Role_Permission的关系联系起来。

2.1 创建Shiro的实体类

  1. 在数据库中建立五张表

    image-20200604145958079

    建表语句如下:

    create table t_user
    (
        id 			int auto_increment 		comment '用户主键ID'	primary key,
        username    varchar(10)  not null 	comment '用户名,不能重复',
        nickname    varchar(10)  not null 	comment '昵称',
        password    varchar(100) not null 	comment '密码',
        description varchar(256) null 		comment '用户描述',
        constraint t_user_username_uindex	unique (username)
    );
    
    create table t_role
    (
        id			int auto_increment 		comment '角色主键ID'	primary key,
        role_name   varchar(20) not null 	comment '角色名称',
        description varchar(50) null 		comment '角色描述',
        constraint t_role_role_name_uindex	unique (role_name)
    );
    
    create table t_permission
    (
        id				int auto_increment 		comment '权限主键ID'	primary key,
        permission_name varchar(20) not null 	comment '权限名称',
        description     varchar(30) null 		comment '权限描述',
        constraint t_permission_permission_name_uindex	unique (permission_name)
    );
    
    create table t_user_role
    (
        id		int auto_increment 	comment '用户和角色关联表主键ID'	primary key,
        user_id int not null 		comment '用户ID',
        role_id int not null 		comment '角色ID'
    );
    
    create table t_role_permission
    (
        id				int auto_increment 	comment '角色和权限关联表主键ID'	primary key,
        role_id			int not null 		comment '角色ID',
        permission_id 	int not null 		comment '权限ID'
    );
    
  2. 利用逆向工程生成User、Role、Permission单表的Bean和Mapper接口以及Mapper映射文件

    逆向工程可以查看这篇文章:Springboot项目Web环境引入Mybatis逆向工程MBG

    在User、Role的类中添加字段,最终如下。

    @Data注解为Lombok插件注解,也可不用,但需要将后加属性添加getter/setter方法,以及toString方法。

    @Data
    public class User {
        private Integer id;
    
        private String username;
    
        private String nickname;
    
        private String password;
    
        private String description;
    
        // 用户对应有各种角色,后加。
        private Set<Role> roles;
    }
    
    @Data
    public class Role {
        private Integer id;
    
        private String roleName;
    
        private String description;
    
        // 每个角色都有一些权限,后加。
        private Set<Permission> permissions;
    }
    
    @Data
    public class Permission {
        private Integer id;
    
        private String permissionName;
    
        private String description;
    }
    

2.2 Shiro的Realm和Config

因为Shiro对外接口是Subject,外界与Shiro的交互首先交给Subject,其在内部交给SecurityManager,SecurityManager再根据相关的域(Realm)做出授权或者认证管理。

而这个域,需要进行自己的编写。

MyRealm:

package com.hebin.shiroexample.shiro;

import com.hebin.shiroexample.bean.Permission;
import com.hebin.shiroexample.bean.Role;
import com.hebin.shiroexample.bean.User;
import com.hebin.shiroexample.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 org.springframework.util.StringUtils;

import java.util.Set;

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 授权的方法,首先从系统中获取系统使用主体,从数据库中查出该主体拥有的角色信息和权限信息,并赋予给AuthorizationInfo,从而完成授权。
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1.根据主体获取PrimaryPrincipal(不一定非得是用户名,也可以是手机号,邮箱地址等能够唯一确定用户的信息)
        String username = (String) principalCollection.getPrimaryPrincipal();

        // 这一步先判断是否存在被授权用户。
        if (principalCollection == null || StringUtils.isEmpty(username)) {
            return null;
        }
        // 2.根据PrimaryPrincipal获取用户。
        User user = userService.getUserWithRolesAndPermissionsByUsername(username);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 3.将用户具有的角色和权限信息分别添加到AuthorizationInfo中
        Set<Role> roles = user.getRoles();
        for (Role r : roles) {
            // 添加角色
            simpleAuthorizationInfo.addRole(r.getRoleName());

            Set<Permission> permissions = r.getPermissions();
            // 添加权限
            for (Permission permission : permissions) {
                simpleAuthorizationInfo.addStringPermission(permission.getPermissionName());
            }
        }
        // 将该主体应该有的角色和权限全部添加到Shiro中返回,Controller接收到返回值后判断是否有授权放行。
        return simpleAuthorizationInfo;
    }

    /**
     * 认证的方法
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        // 从token中获取用户名、密码等信息
        String username = (String) authenticationToken.getPrincipal();
        // 如果获取不到,认证失败
        if (authenticationToken == null || StringUtils.isEmpty(username)) {
            return null;
        }

        // 根据获取到的信息,从数据库查询相关实体
        User user = userService.getUserWithRolesAndPermissionsByUsername(username);
        // 如果实体不存在,认证失败
        if (user == null) {
            return null;
        }

        // 将查询到的实体的相关信息交给Shiro进行判断是否通过认证。
        return new SimpleAuthenticationInfo(username,user.getPassword(),getName());
    }
}

上文中的service接口及其实现类

package com.hebin.shiroexample.service;

import com.hebin.shiroexample.bean.User;

public interface UserService {
    User getUserWithRolesAndPermissionsByUsername(String username);
}
package com.hebin.shiroexample.service.impl;

import com.hebin.shiroexample.bean.Role;
import com.hebin.shiroexample.bean.User;
import com.hebin.shiroexample.mapper.RoleExtendMapper;
import com.hebin.shiroexample.mapper.UserExtendMapper;
import com.hebin.shiroexample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Set;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserExtendMapper userExtendMapper;

    @Autowired
    private RoleExtendMapper roleExtendMapper;

    @Override
    public User getUserWithRolesAndPermissionsByUsername(String username) {

        // 根据用户名查出用户,其中用户的Set<Role> Roles属性中不包含Role的Set<Permission> permissions属性。
        User user = userExtendMapper.selectUserWithRolesByUsername(username);

        // 取出用户的Roles Set,根据每个role查询出对应的Set<Permission>,将role缺失的该权限属性集合用setter方法赋予。
        Set<Role> roles = user.getRoles();

        // 1.循环取出role
        for (Role r : roles) {
            // 2.每个role取出其id
            Integer roleId = r.getId();
            // 3.根据roleId取出对应的permissions完好的Role
            Role role = roleExtendMapper.selectRoleWithPermissionsByRoleId(roleId);
            // 4.调用role的setter方法赋值permissions。
            r.setPermissions(role.getPermissions());
        }

        // 返回User对象,该对象是完整对象,内包含全部的role和permission。
        return user;
    }
}

上述代码中用到的Mapper接口及其Sql映射文件:

UserExtendMapper:

package com.hebin.shiroexample.mapper;

import com.hebin.shiroexample.bean.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserExtendMapper extends UserMapper {
    User selectUserWithRolesByUsername(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.hebin.shiroexample.mapper.UserExtendMapper">
    <resultMap id="BaseResultMap" type="com.hebin.shiroexample.bean.User">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="username" property="username" jdbcType="VARCHAR"/>
        <result column="nickname" property="nickname" jdbcType="VARCHAR"/>
        <result column="password" property="password" jdbcType="VARCHAR"/>
        <result column="description" property="description" jdbcType="VARCHAR"/>
        <collection property="roles" resultMap="RoleSetMap"/>
    </resultMap>
    <resultMap id="RoleSetMap" type="com.hebin.shiroexample.bean.Role">
        <id column="rid" property="id" jdbcType="INTEGER"/>
        <result column="role_name" property="roleName" jdbcType="VARCHAR"/>
        <result column="rdesc" property="description" jdbcType="VARCHAR"/>
    </resultMap>

    <select id="selectUserWithRolesByUsername" resultMap="BaseResultMap">
      select u.*,r.id rid,r.role_name,r.description rdesc
      from t_user u
      left join t_user_role ur on u.id=ur.user_id
      left join t_role r on ur.role_id=r.id
      where  u.username=#{username}
  </select>
</mapper>

RoleExtendMapper:

package com.hebin.shiroexample.mapper;

import com.hebin.shiroexample.bean.Role;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleExtendMapper extends RoleMapper {
    Role selectRoleWithPermissionsByRoleId(Integer id);
}
<?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.hebin.shiroexample.mapper.RoleExtendMapper">
    <resultMap id="BaseResultMap" type="com.hebin.shiroexample.bean.Role">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="role_name" property="roleName" jdbcType="VARCHAR"/>
        <result column="description" property="description" jdbcType="VARCHAR"/>
        <collection property="permissions" resultMap="PermissionSetMap"/>
    </resultMap>
    <resultMap id="PermissionSetMap" type="com.hebin.shiroexample.bean.Permission">
        <id column="pid" property="id" jdbcType="INTEGER"/>
        <result column="permission_name" property="permissionName" jdbcType="VARCHAR"/>
        <result column="pdesc" property="description" jdbcType="VARCHAR"/>
    </resultMap>

    <select id="selectRoleWithPermissionsByRoleId" resultMap="BaseResultMap">
      select r.*,p.id pid,p.permission_name,p.description pdesc
      from t_role r
      left join t_role_permission rp on r.id=rp.role_id
      left join t_permission p on rp.permission_id=p.id
      where  r.id=#{id}
  </select>
</mapper>

当完成上面的代码后,需要进行Shiro的配置了:

package com.hebin.shiroexample.config;

import com.hebin.shiroexample.shiro.MyRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;

@Configuration
public class ShiroConfig {

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator creator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    // 将自定义的Realm加入到Spring容器
    @Bean
    public MyRealm myShiroRealm() {
        return new MyRealm();
    }

    // 权限管理,特别需要注意的是,SecurityManager是个接口,一定要使用org.apache.shiro.mgt.SecurityManager;
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(myShiroRealm());
        return defaultWebSecurityManager;
    }

    // filter设置过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        filterFactoryBean.setSecurityManager(securityManager);
        HashMap<String, String> map = new HashMap<>();

        map.put("/logout", "logout");

        // authc表示需要认证才能访问,即拦截认证;anon表示无需认证也能访问,即不拦截
        map.put("/admin/**", "authc");
        map.put("/user/**", "authc");
        map.put("/leader/**", "authc");
   		map.put("/visitor/**", "anon");
        
        filterFactoryBean.setLoginUrl("/login");

        filterFactoryBean.setUnauthorizedUrl("/error");

        filterFactoryBean.setFilterChainDefinitionMap(map);

        return filterFactoryBean;
    }

    // 开启Shiro的注解
    @Bean
    public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

三 测试

首先我们建立一个异常拦截类拦截授权或者认证异常

package com.hebin.shiroexample.exceptionHandler;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.naming.AuthenticationException;

@RestControllerAdvice
@Slf4j
public class MyExceptionHandler {

    @ExceptionHandler(AuthorizationException.class)
    public String authorizationExceptionHandler(AuthorizationException e) {
        log.error("没有通过权限验证", e);
        return "没有通过权限验证!";
    }

    @ExceptionHandler(AuthenticationException.class)
    public String authenticationExceptionHandler(AuthenticationException e) {
        log.error("授权异常", e);
        return "授权异常!";
    }
}

我们建立一些假数据:

User:

image-20200604161636350

Role:

image-20200604161735562

Permission:

image-20200604161804159

User_Role:

image-20200604161841036

Role_Permission:

image-20200604161906590

建立一个Controller测试权限情况

@RestController
public class IndexController {

    @RequestMapping("/index")
    public String index() {
        return "success!";
    }

    @RequiresRoles("admin")
    @RequestMapping("/admin/list")
    public String getUserList() {
        Subject subject = SecurityUtils.getSubject();
        Object principal = subject.getPrincipal();
        System.out.println(principal);
        return "userList";
    }

    @RequiresPermissions("delete")
    @RequestMapping("/admin/delete")
    public String deleteUser() {
        return "userDelete";
    }

    @GetMapping("/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password) {
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            subject.login(token);
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return "账号和密码错误!";
        } catch (AuthorizationException e) {
            e.printStackTrace();
            return "没有权限!";
        }

        return "登陆成功!";
    }
}

3.1 浏览器访问测试

  1. 访问:http://localhost/index,无需任何权限,成功!

    返回Success!

    image-20200604162326378

  2. 测试登录:

    1. 浏览器访问:http://localhost/login?username=aa&password=123

      image-20200604162639532

      控制台提示认证错误。

      org.apache.shiro.authc.AuthenticationException: Authentication failed for token submission [org.apache.shiro.authc.UsernamePasswordToken - aa, rememberMe=false]. 
      

      浏览器访问:http://localhost/login?username=a&password=123

    image-20200604162509394

  3. a用户为管理员,包含所有权限,访问:http://localhost/admin/list,成功

image-20200604162850759

  1. a用户为管理员,包含所有权限,访问:http://localhost/admin/delete,成功

    image-20200604163103173

  2. 我们访问:http://localhost/logout登出,对b用户登录:http://localhost/login?username=b&password=123

  3. b为leader、user用户,访问:http://localhost/admin/list,失败

    image-20200604163435081

  4. b用户不包含delete权限,访问:http://localhost/admin/delete,失败
    image-20200604163523819

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值