Shiro和Spring MVC、Mybatis整合教程
文章目录
前言
Apache Shiro 是Java的安全框架,提供了认证(Authentication)、授权(Authorization)、会话(Session)管理、加密(Cryptography)等功能,且Shiro与Spring Security等安全框架相比具有简单性、灵活性、支持细粒度鉴权、支持一级缓存等,还有Shiro不跟任何容器(Tomcat等)和框架(Sping等)捆绑,可以独立运行,这也造就了Shiro不仅仅是可以用在Java EE上还可以用在Java SE上。
Shiro四大功能
在开始之前,首先了解一下Shiro的四大功能,俗话说“知己知彼百战不殆”。
认证
认证就是用户访问系统的时候,系统要验证用户身份的合法性,比如我们通常所说的“登录”就是认证的一种方式,只有登录成功了之后我们才能访问相应的资源。在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 (域)。
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集成
项目目录
添加依赖包
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>
创建数据库和实体类
为了减少篇幅,只做简单介绍,详情可以查看源码,数据库文件在本项目根目录。
- resource表:资源表,有
id
,name
两个字段,分别对应资源id和权限。 - role表:角色表,有
id
,name
两个字段,分别对应角色id和角色名。 - role_resource表:角色资源权限表,有
id
,roleid
,resid
三个字段,分别对应自增id、角色id和资源id。 - user表:用户表,有
id
,username
,password
三个字段,分别对应自增id、用户名和密码。 - user_role表:有
id
,uid
,rid
三个字段,分别对应自增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
登录页面,填写正确用户名和密码登录
登录成功 转跳成功页面
清除浏览器cookie之后(未登录状态),打开http://localhost:8080/home
页面,自动转跳到了/login
登录页面(即没有权限访问),登录账户,再次打开http://localhost:8080/home
页面即可正常访问。
总结
这是我学习Shiro时候根据自己的情况记录下来的,希望对大家有所帮助,如果大家想对Shiro进一步研究的话,推荐大家看张开涛老师的《跟我学Shiro》,最后附上本项目的Github地址:https://github.com/xue8/Java-Demo/tree/master/shiro