Shiro和Spring MVC、Mybatis整合教程

2,067 阅读13分钟

前言

Apache Shiro 是Java的安全框架,提供了认证(Authentication)、授权(Authorization)、会话(Session)管理、加密(Cryptography)等功能,且Shiro与Spring Security等安全框架相比具有简单性、灵活性、支持细粒度鉴权、支持一级缓存等,还有Shiro不跟任何容器(Tomcat等)和框架(Sping等)捆绑,可以独立运行,这也造就了Shiro不仅仅是可以用在Java EE上还可以用在Java SE上

Shiro四大功能

在开始之前,首先了解一下Shiro的四大功能,俗话说“知己知彼百战不殆”。

image

认证

认证就是用户访问系统的时候,系统要验证用户身份的合法性,比如我们通常所说的“登录”就是认证的一种方式,只有登录成功了之后我们才能访问相应的资源。在Shiro中,我们可以将用户理解为Subject主体,在用户身份认证的时候,用户需要提供能证明他身份的信息,如用户名、密码等,用户所提供的这些用户名、密码则对应Shiro中的Principal、 Credentials,即在Subject进行身份认证的时候,需要提供相应的Principal、 Credentials,对应的代码如下:

UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
subject.login(token); //提交认证

我们知道Http协议是无状态的,所以用户认证成功后怎么才能保持认证成功的状态呢?如果是我们开发的话一般都是登录成功后将Session储存在服务器,然后再将Session返回给用户,之后的请求用户都将这个Session带上,然后服务器根据用户请求携带的Session和服务器储存的Session进行比较来判断用户是否已认证。但是使用Shiro后,Shiro已经帮我们做好这个了(下面介绍的会话管理),是不是feel爽~

授权

授权可以理解为访问控制,在用户认证(登录)成功之后,系统对用户访问资源的权限进行控制,即确定什么用户能访问什么资源,如普通用户不能访问后台,但是管理员可以。在这里我们还需要认识几个概念,资源(Resource)、角色(Role)、权限(Permission),上面提到的Subject主体可以有多个角色,每个角色又对应多个资源的多个权限,这种基于资源的访问控制可以实现细粒度的权限。对主体设置角色、权限的代码如下:

SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//  添加用户的角色
authorizationInfo.addRoles(roleIdList);
//  添加用户的权限
authorizationInfo.addStringPermissions(resourceIdList);

如果要实现这样的授权功能,我们必定需要设计一个用户组、权限,给每个方法或者URL加上判断,是否当前登录的用户满足条件。但是使用Shiro后,Shiro也帮我们帮这些都做好了

会话管理

会话管理的会话即Session,所谓会话,即用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。在Shiro中,与用户有关的一切信息都可以通过Shiro的接口获得,和用户的会话Session也都由Shiro管理。如实现“记住我”或者“下次自动登录”的功能,如果要自己去开发的话,估计又得话不少时间。但是使用Shiro后,Shiro也帮我们帮这些都做好了

加密

用户密码明文保存是不是安全,应不应该MD5加密,是不是应该加盐,又要写密码加密的代码。 这些Shiro已经帮你做好了

Shiro三大核心概念

从整体概念上理解,Shiro的体系架构有三个主要的概念,Subject(主体),Security Manager (安全管理器)和 Realms (域)。

image

Subject主体

主体是当前正在操作的用户的特定数据集合。主体可以是一个人,也可以代表第三方服务,守护进程,定时任务或类似的东西,也就是几乎所有与该应用进行交互的事物。所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给 SecurityManager,可以把 Subject 认为是一个门面,SecurityManager 才是实际的执行者。

Security Manager安全管理器

安全管理器,即所有与安全有关的操作都会与SecurityManager交互,且它管理着所有Subject可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成DispatcherServlet前端控制器,一般来说,一个应用只会存在一个SecurityManager实例

Realms域

域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法,也需要从Realm得到用户相应的角色 / 权限进行验证用户是否能进行操作,即Realms作为Shiro与应用程序安全数据之间的“桥梁”。从这个意义上讲,Realm实质上是一个安全相关的DAO,它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。其中Realm有2个方法,doGetAuthenticationInfo用来认证,doGetAuthorizationInfo用来授权。

Spring、Spring MVC、Mybatis、Shiro集成

项目目录

image

添加依赖包

pox.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">
  <modelVersion>4.0.0</modelVersion>

  <groupId>shiro</groupId>
  <artifactId>shiro</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>shiro Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!--Sping核心依赖-->
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>5.1.3.RELEASE</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.1.3.RELEASE</version>
      <scope>test</scope>
    </dependency>

    <!--Mybatis依赖-->
    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.6</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.2</version>
    </dependency>

    <!--MySQL连接驱动-->
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.13</version>
    </dependency>


    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.4.0</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-web</artifactId>
      <version>1.4.0</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.4.0</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>


  </dependencies>

  <build>
    <finalName>shiro</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

创建数据库和实体类

为了减少篇幅,只做简单介绍,详情可以查看源码,数据库文件在本项目根目录。

image

  • resource表:资源表,有idname两个字段,分别对应资源id和权限。
  • role表:角色表,有idname两个字段,分别对应角色id和角色名。
  • role_resource表:角色资源权限表,有idroleidresid三个字段,分别对应自增id、角色id和资源id。
  • user表:用户表,有idusernamepassword三个字段,分别对应自增id、用户名和密码。
  • user_role表:有iduidrid三个字段,分别对应自增id、用户id、和角色id。

Dao层

AccountDao.java:

public interface AccountDao {
    User findUserByUsername(String username);
    List<Role> findRoleByUserId(int id);
    List<Resource> findResourceByUserId(int id);
}

service层

AccountService.java:

public interface AccountService {
    User findUserByUsername(String username);
    List<Role> findRoleByUserId(int id);
    List<Resource> findResourceByUserId(int id);
    boolean login(User user);
}

AccountServiceImpl.java:

package com.shiro.service.impl;

import com.shiro.dao.AccountDao;
import com.shiro.entity.Role;
import com.shiro.entity.User;
import com.shiro.service.AccountService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * @program: shiro
 * @description:
 * @author: Xue 8
 * @create: 2019-02-01 15:37
 **/
@Service
public class AccountServiceImpl implements AccountService {
    @Resource
    AccountDao accountDao;

    /**
     * @description: 根据用户名查找用户信息
     * @param: [username]
     * @return: com.shiro.entity.User
     * @author: Xue 8
     * @date: 2019/2/1
     */
    @Override
    public User findUserByUsername(String username) {
        return accountDao.findUserByUsername(username);
    }

    @Override
    public List<Role> findRoleByUserId(int id) {
        return accountDao.findRoleByUserId(id);
    }

    @Override
    public List<com.shiro.entity.Resource> findResourceByUserId(int id) {
        return accountDao.findResourceByUserId(id);
    }

    public boolean login(User user){
//        获取当前用户对象subject
        Subject subject = SecurityUtils.getSubject();
        System.out.println("subject:" + subject.toString());
//        创建用户名/密码身份证验证Token
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        System.out.println("token" + token);
        try {
            subject.login(token);
            System.out.println("登录成功");
            return true;
        } catch (Exception e) {
            System.out.println("登录失败" + e);
            return false;
        }
    }
}

MyRealm.java

package com.shiro.service.impl;

import com.shiro.entity.Role;
import com.shiro.entity.User;
import com.shiro.service.AccountService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @program: shiro
 * @description:
 * @author: Xue 8
 * @create: 2019-02-01 15:16
 **/

public class MyRealm extends AuthorizingRealm {
    @Resource
    AccountService accountService;

    /**
      * 身份认证的方法 认证成功获取身份验证信息
     * 这里最主要的是user.login(token);这里有一个参数token,这个token就是用户输入的用户密码,
     * 我们平时可能会用一个对象user来封装用户名和密码,shiro用的是token,这个是控制层的代码,还没到shiro,
     * 当调用user.login(token)后,就交给shiro去处理了,接下shiro应该是去token中取出用户名,然后根据用户去查数据库,
     * 把数据库中的密码查出来。这部分代码一般都是要求我们自定义实现,自定义一个realm,重写doGetAuthenticationInfo方法
    **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//        获取用户输入的用户名和密码
//        实际上这个token是从UserResource面currentUser.login(token)传过来的
//        两个token的引用都是一样的
        String username = (String) authenticationToken.getPrincipal();
//        密码要用字符数组来接受 因为UsernamePasswordToken(username, password) 储存密码的时候是将字符串类型转成字符数组的 查看源码可以看出
        String password = new String((char[]) authenticationToken.getCredentials());
//        调用service 根据用户名查询用户信息
        User user = accountService.findUserByUsername(username);
//        String password = user.getPassword();
//        判断用户是否存在 不存在则抛出异常
        if (user != null) {
//            判断用户密码是否匹配 匹配则不匹配则抛出异常
            if (user.getPassword().equals(password)) {
//                登录成功 把用户信息储存在Session中
                Session session = SecurityUtils.getSubject().getSession();
                session.setAttribute("userSession", user);
                session.setAttribute("userSessionId", user.getId());
//                认证成功 返回一个AuthenticationInfo的实现
                return new SimpleAuthenticationInfo(username, password, getName());
            } else {
                System.out.println("密码不正确");
                throw new IncorrectCredentialsException();
            }
        } else {
            System.out.println("账号不存在");
            throw new UnknownAccountException();
        }
    }

    /**
      * 授权的方法
     * 1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
     *
     * 2、@RequiresRoles("admin") :在方法上加注解的时候;
     *
     * 3、[@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。
     * 4、xml配置权限的时候也会走
    **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("授权");
//        从principalCollection获取用户信息
//        如果doGetAuthenticationInfo(user,password,getName()); 传入的是user类型的数据 那这里getPrimaryPrincipal获取到的也是user类型的数据
        String username = (String) principalCollection.getPrimaryPrincipal();
        User user = accountService.findUserByUsername(username);
//        获取该用户的所有角色
        List<Role> roleList = accountService.findRoleByUserId(user.getId());
//        将角色的id放到一个String列表中 因为authorizationInfo.addRoles()方法只支持角色的String列表或者单个角色String
        List<String> roleIdList = new ArrayList<String>();
        for (Role role:roleList) {
            roleIdList.add(role.getName());
        }
//        获取该用户的所有权限
        List<com.shiro.entity.Resource> resourceList = accountService.findResourceByUserId(user.getId());
        List<String> resourceIdList = new ArrayList<String>();
//        将权限id放到一个String列表中 因为authorizationInfo.addRoles()方法只支持角色的String列表或者单个角色String
        for (com.shiro.entity.Resource resource:resourceList) {
            resourceIdList.add(resource.getName());
        }
        System.out.println("授权11");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//        添加用户的角色
        authorizationInfo.addRoles(roleIdList);
//        添加用户的权限
        authorizationInfo.addStringPermissions(resourceIdList);
        return authorizationInfo;
    }
}

controller层

AccountController.java

package com.shiro.controller;

import com.shiro.entity.User;
import com.shiro.service.AccountService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
 * @program: shiro
 * @description:
 * @author: Xue 8
 * @create: 2019-02-01 13:14
 **/
@Controller
public class AccountController {
    @Resource
    AccountService accountService;
    @Resource
    HttpServletRequest servletRequest;

    @RequestMapping(value = "/home")
    public String  home(){
        return "home";
    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String  getLogin(){
        return "login";
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String doLogin(@RequestParam(value = "username") String username,
                        @RequestParam(value = "password") String password){
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        if (accountService.login(user)) {
            return "/home";
        }
        return "/login";
    }
}

GET方法访问/login的时候,会出现登录页面,输入账号密码点击登录数据将以POST方式提交给/login,如果账号密码匹配返回/home的页面,否则返回/login的页面。/home页面只有在登录且有权限的情况下才可以访问,未登录情况下访问会转跳/login页面,这个在Shiro的配置文件里面配置。

配置文件

applicationContext.xml:配置Spring

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启扫描注册-->
    <context:component-scan base-package="com.shiro"></context:component-scan>

    <!--读取properties配置-->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:jdbcConfig.properties"></property>
    </bean>

    <!--配置数据源-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${driverClassName}"></property>
        <property name="username" value="${username}"></property>
        <property name="password" value="${password}"></property>
        <property name="url" value="${url}"></property>
    </bean>

    <!--配置session工厂-->
    <bean id="sessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
        <property name="mapperLocations" value="classpath:mapping/*.xml"></property>
    </bean>

    <!--配置扫描mapping-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.shiro.dao"></property>
        <property name="sqlSessionFactoryBeanName" value="sessionFactoryBean"></property>
    </bean>
</beans>

spring-shiro.xml:配置Shiro

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"></property>
    </bean>

    <bean id="myRealm" class="com.shiro.service.impl.MyRealm">
        <!--关闭权限缓存 不然doGetAuthorizationInfo授权方法不执行-->
        <property name="authorizationCachingEnabled" value="false"/>
    </bean>

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"></property>
        <property name="successUrl" value="/success"></property>
        <!--登录页面-->
        <property name="loginUrl" value="/login"></property> 
        <property name="filterChainDefinitions">
            <value>
                <!--配置`/home`只有拥有`admin`角色的用户才可以访问-->
                /home = authc,roles[admin]
            </value>
        </property>
    </bean>

</beans>

这里需要注意的是 在配置Realm的时候,如果没用上缓存功能的话,需要将缓存关掉,不然进不到doGetAuthorizationInfo授权方法。

测试

打开http://localhost:8080/login登录页面,填写正确用户名和密码登录

image
登录成功 转跳成功页面
image
清除浏览器cookie之后(未登录状态),打开http://localhost:8080/home页面,自动转跳到了/login登录页面(即没有权限访问),登录账户,再次打开http://localhost:8080/home页面即可正常访问。

总结

这是我学习Shiro时候根据自己的情况记录下来的,希望对大家有所帮助,如果大家想对Shiro进一步研究的话,推荐大家看张开涛老师的《跟我学Shiro》,最后附上本项目的Github地址:github.com/xue8/Java-D…

原文地址:ddnd.cn/2019/02/02/…