Spring Boot集成Shiro快速入门Demo

1.什么是shiro?

Shiro是一个功能强大、灵活的,开源的安全框架,主要可以帮助我们解决程序开发中认证和权限等问题。基于拦截器做的权限系统,权限控制的粒度有限,为了方便各种各样的常用的权限管理需求的实现,我们有必要使用比较好的安全框架。

早期Spring security 作为一个比较完善的安全框架比较火,但是Springsecurity学习成本比较高,于是就出现了shiro安全框架,学习成本降低了很多,而且基本的功能也比较完善。

Shiro的架构

  1. Subject:主题。被验证的对象,一般指的当前用户对象。但是不仅仅可以指当前用户对象,还可以是其他东西,线程等等。spring mvc中一个一个的用户的请求。
  2. SecurityManager:安全认证管理器。是shiro的核心,会在安全认证管理器中所做所有的认证操作。类似于之前Spring MVC中的前端控制器(DispacherServlet)。
  3. Realm:域的意思。负责访问安全认证数据。shiro框架并不存在安全认证数据,安全认证数据需要用户自己存储。shiro支持很多的Realm实现,也就是说安全认证数据我们可以放到数据库中,也可以放到文件中等等。可以把realm理解为以前web项目的dao层。

Shiro的功能

  1. Authentication:身份认证/登陆,验证用户是不是拥有相对应的身份,通常被称为用户"登录";
  2. Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者粒度的验证某个用户对某个资源是否具有权限;
  3. Session Manager:会话管理,即用户登陆后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是Web环境的;
  4. Cryptographt:加密,保护数据,如密码加密存储到数据库,而不是明文存储;
  5. Web Support:Web支持,可以保护 Web 应用程序的安全;
  6. Caching:缓存,比如用户登陆后,其用户信息、拥有的角色/权限不必每次去查,这样提高效率;
  7. Concurrency:多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  8. Testing:支持单元测试和集成测试,确保代码和预想的一样安全;
  9. Run As:允许一个用户假装另一个用户(如果我们允许)的身份进行访问;
  10. Remember Me:记住我,这个是非常常见的功能,即一次登陆后,下次再来的话不用登陆了。

2.环境搭建

mysql database

css 复制代码
docker run --name docker-mysql-5.7 -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:5.7

init data

less 复制代码
create database demo;
CREATE TABLE `sys_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `available` tinyint(4) DEFAULT NULL,
  `name` varchar(100) DEFAULT NULL,
  `parent_id` int(11) DEFAULT NULL,
  `permission` varchar(100) DEFAULT NULL,
  `resource_type` varchar(100) DEFAULT NULL,
  `url` varchar(100) DEFAULT NULL,
  `parent_ids` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `available` tinyint(4) DEFAULT NULL,
  `description` varchar(100) DEFAULT NULL,
  `role` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `sys_role_permission` (
  `permission_id` int(11) DEFAULT NULL,
  `role_id` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `sys_user_role` (
  `uid` int(11) DEFAULT NULL,
  `role_id` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `user_info` (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) CHARACTER SET latin1 DEFAULT NULL,
  `name` varchar(200) DEFAULT NULL,
  `password` varchar(100) CHARACTER SET latin1 DEFAULT NULL,
  `salt` varchar(100) CHARACTER SET latin1 DEFAULT NULL,
  `state` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (1,0,'管理员','admin');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (2,0,'VIP会员','vip');
INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (3,1,'test','test');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'用户管理',0,'0/','userInfo:view','menu','userInfo/userList');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (2,0,'用户添加',1,'0/1','userInfo:add','button','userInfo/userAdd');
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (3,0,'用户删除',1,'0/1','userInfo:del','button','userInfo/userDel');
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3,2);

remark

css 复制代码
msyql account:root
mysql password:123456

3.代码工程

**

实验目的:使用shiro控制访问权限

**

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springboot-demo</artifactId>
        <groupId>com.et</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>shiro</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </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-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
    </dependencies>
</project>

shiro config

调用userservice读取用户信息

scala 复制代码
package com.et.shiro.config;

import com.et.shiro.entity.SysPermission;
import com.et.shiro.entity.SysRole;
import com.et.shiro.entity.UserInfo;
import com.et.shiro.service.UserInfoService;
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.apache.shiro.util.ByteSource;

import javax.annotation.Resource;

public class MyShiroRealm extends AuthorizingRealm {
    @Resource
    private UserInfoService userInfoService;

    //check username and password
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
        String username = (String)token.getPrincipal();
        System.out.println(token.getCredentials());
        //query user by username
        //in here ,you can cache some data for efficient
        UserInfo userInfo = userInfoService.findByUsername(username);
        System.out.println("----->>userInfo="+userInfo);
        if(userInfo == null){
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userInfo, //username
                userInfo.getPassword(), //password
                ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
                getName()  //realm name
        );
        return authenticationInfo;
    }

    //authority controller
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("authority config-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        UserInfo userInfo  = (UserInfo)principals.getPrimaryPrincipal();
        for(SysRole role:userInfo.getRoleList()){
            authorizationInfo.addRole(role.getRole());
            for(SysPermission p:role.getPermissions()){
                authorizationInfo.addStringPermission(p.getPermission());
            }
        }
        return authorizationInfo;
    }
}

配置过滤链

java 复制代码
package com.et.shiro.config;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

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

@Configuration
public class ShiroConfig {
   @Bean
   public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
      System.out.println("ShiroConfig.shirFilter()");
      ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
      shiroFilterFactoryBean.setSecurityManager(securityManager);
      // filter
      Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
      // config filterChain
      filterChainDefinitionMap.put("/static/**", "anon");
      // logout
      filterChainDefinitionMap.put("/logout", "logout");
      // filterChainDefinitio,from up to down,so put /** into the last
      // authc:have a permission anon:anonymous access
      filterChainDefinitionMap.put("/**", "authc");
      //default login
      shiroFilterFactoryBean.setLoginUrl("/login");
      //success
      shiroFilterFactoryBean.setSuccessUrl("/index");
      // Unauthorized;
      shiroFilterFactoryBean.setUnauthorizedUrl("/403");
      shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
      return shiroFilterFactoryBean;
   }

   /**
    * 凭证匹配器
    * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
    * @return
    */

   // 因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。
   // 同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。
   // 这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
   @Bean
   public HashedCredentialsMatcher hashedCredentialsMatcher(){
      HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
      hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
      hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
      return hashedCredentialsMatcher;
   }

   @Bean
   public MyShiroRealm myShiroRealm(){
      MyShiroRealm myShiroRealm = new MyShiroRealm();
      myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());// 设置解密规则
      return myShiroRealm;
   }

   // SecurityManager是Shiro架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
   @Bean
   public SecurityManager securityManager(){
      DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
      securityManager.setRealm(myShiroRealm());
      return securityManager;
   }

   /**
    * 开启shiro aop注解支持.
    * 使用代理方式;所以需要开启代码支持;
    * @param securityManager
    * @return
    */
   @Bean
   public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
      AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
      authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
      return authorizationAttributeSourceAdvisor;
   }

   @Bean(name="simpleMappingExceptionResolver")
   public SimpleMappingExceptionResolver
   createSimpleMappingExceptionResolver() {
      SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
      Properties mappings = new Properties();
      mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
      mappings.setProperty("UnauthorizedException","403");
      r.setExceptionMappings(mappings);  // None by default
      r.setDefaultErrorView("error");    // No default
      r.setExceptionAttribute("ex");     // Default is "exception"
      //r.setWarnLogCategory("example.MvcLogger");     // No default
      return r;
   }
}

controller

kotlin 复制代码
package com.et.shiro.controller;

import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@Controller
public class HomeController {

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

    @RequestMapping("/login")
    public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
        System.out.println("HomeController.login()");
        // shiroLoginFailure
        String exception = (String) request.getAttribute("shiroLoginFailure");
        System.out.println("exception=" + exception);
        String msg = "";
        if (exception != null) {
            if (UnknownAccountException.class.getName().equals(exception)) {
                System.out.println(" -- > account isn't exist");
                msg = " -- > account isn't exist";
            } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                System.out.println(" -- > password isn't right");
                msg = " -- > password isn't right";
            } else {
                msg = "else >> "+exception;
                System.out.println("else -- >" + exception);
            }
        }
        map.put("msg", msg);
        return "/login";
    }

    @RequestMapping("/403")
    public String unauthorizedRole(){
        System.out.println("------unauthorizedRole-------");
        return "403";
    }
}

设置不同链接访问权限

kotlin 复制代码
package com.et.shiro.controller;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/userInfo")
public class UserInfoController {

    /**
     * userList
     * @return
     */
    @RequestMapping("/userList")
    @RequiresPermissions("userInfo:view")
    public String userInfo(){
        return "userInfo";
    }

    /**
     * userAdd
     * @return
     */
    @RequestMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String userInfoAdd(){
        return "userInfoAdd";
    }

    /**
     * userDel
     * @return
     */
    @RequestMapping("/userDel")
    @RequiresPermissions("userInfo:del")
    public String userDel(){
        return "userInfoDel";
    }
}

jpa读取数据库数据

因为在entity里面设置及联关系,所以jpa会自动加载关联数据出来

less 复制代码
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
private List<SysRole> roleList;

继承公共crud操作类

java 复制代码
package com.et.shiro.dao;

import com.et.shiro.entity.UserInfo;
import org.springframework.data.repository.CrudRepository;

public interface UserInfoDao extends CrudRepository<UserInfo,Long> {

    public UserInfo findByUsername(String username);
}

teamplate

在resource目录下,你可以在代码仓库看到,这里不一个个列举出来

application.yaml

yaml 复制代码
server:
  port: 8088
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: 123456
    #schema: database/import.sql
    #sql-script-encoding: utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5Dialect

  thymeleaf:
    cache: false
    mode: HTML

以上只是一些关键代码,所有代码请参见下面代码仓库

代码仓库

4.测试

启动Spring Boot应用

测试未登录访问

访问[http://localhost:8088/userInfo/userList](http://localhost:8088/userInfo/userList)页面,由于没有登录就会跳转到[http://localhost:8088/login](http://localhost:8088/login)页面

登陆后访问

登录后,能看首页信息

测试不同用户登陆

访问[http://127.0.0.1:8088/userInfo/userDel](http://127.0.0.1:8088/userInfo/userDel)显示403没有权限,因为需要@RequiresPermissions("userInfo:del"),而admin没有给它设置相应的权限

5.参考引用

相关推荐
假装我不帅40 分钟前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
神仙别闹43 分钟前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
计算机-秋大田1 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
货拉拉技术2 小时前
货拉拉-实时对账系统(算盘平台)
后端
掘金酱2 小时前
✍【瓜分额外奖金】11月金石计划附加挑战赛-活动命题发布
人工智能·后端
代码之光_19802 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi2 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇3 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
戴眼镜的猴3 小时前
Spring Boot的过滤器与拦截器的区别
spring boot