Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。
image.pngSubject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的。
1、添加依赖
<?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>com.qianfeng</groupId>
<artifactId>413Shiro</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
</dependencies>
</project>
shiro的依赖非常多,由于我们只测试基本功能,所以这里只需要引入shiro-core这个依赖,与spring整合的时候还需要shiro-spring这个依赖,slf4j这两个依赖提供了日志相关的类,由于我还要连接数据库进行操作,所以引入了druid与mysql这两个依赖。
2、身份验证
TestShiro.java
package com.qainfeng;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class TestShiro {
@Test
public void shiroTest(){
//创建一个默认的安全管理器
DefaultSecurityManager manager = new DefaultSecurityManager();
//读取配置文件创建一个配置域
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
//设置域
manager.setRealm(iniRealm);
//将新创建的安全管理器设置为安全管理器
SecurityUtils.setSecurityManager(manager);
//获得subject(主体)
Subject subject = SecurityUtils.getSubject();
//根据前台发送过来的用户名与密码创建一个token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123456");
try {
//subject登录
subject.login(token);
} catch (UnknownAccountException e){
//如果出现了这个异常
System.out.println("未知账户");
e.printStackTrace();
} catch (IncorrectCredentialsException e){
System.out.println("密码错误");
e.printStackTrace();
}
catch (AuthenticationException e) {
//这个异常类是上面两个异常类的父类,此外还有很多异常子类
System.out.println("其他错误");
e.printStackTrace();
}
subject.logout();
}
@Test
public void shiroTest3(){
DefaultSecurityManager manager = new DefaultSecurityManager();
JdbcRealm jdbcRealm = new JdbcRealm();
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/market?useSSL=true&serverTimezone=UTC&characterEncoding=UTF-8");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("huwenlong");
jdbcRealm.setDataSource(dataSource);
jdbcRealm.setAuthenticationQuery("select password from users where user_name = ?");
manager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(manager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("汪淼","wangmiao123");
subject.login(token);
System.out.println("success");
}
@Test
public void shiroTest4(){
/*DefaultSecurityManager manager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro2.ini");
manager.setRealm(iniRealm);*/
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro2.ini");
// 通过工厂创建SecurityManager
SecurityManager manager = factory.getInstance();
SecurityUtils.setSecurityManager(manager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("汪淼","wangmiao123");
subject.login(token);
System.out.println("success");
}
}
有两种方式认证身份,一种方式是使用配置文件,直接在配置文件中写好用户名与密码,另外一种方式是连接数据库。
shiro.ini
[users]
zhangsan=123456,admin,guest
lisi=654321,guest
[roles]
admin=select,delete,update,insert
guest=select,update,insert
这个文件中配置了用户、角色、权限信息
[main]
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.cj.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3306/market?useSSL=true&serverTimezone=UTC&characterEncoding=UTF-8
dataSource.username=root
dataSource.password=huwenlong
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource=$dataSource
jdbcRealm.authenticationQuery=select password from users where user_name = ?
securityManager.realms=$jdbcRealm
在第一种方式中,由于IniSecurityManagerFactory这个类已经被废弃了,所以我就使用DefaultSecurityManager来取代,不过这种做法不能使用ini文件来配置JdbcRealm,还是要采用传统的方式。连接数据库也有两种方式,第一种方式使用纯Java代码,第二种方式使用ini文件,两种方式的写法有很大的区别,这里需要注意。还有就是一个JdbcReal实例的默认认证查询是"select password from users where username=?",如果你的数据库表名与字段名与默认的不一样可以修改这条语句。
3、授权
package com.qainfeng;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class TestShiro {
@Test
public void shiroTest2(){
DefaultSecurityManager manager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
manager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(manager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("lisi","654321");
subject.login(token);
System.out.println(subject.hasRole("guest"));
System.out.println(subject.hasRole("admin"));
System.out.println(subject.isPermittedAll("select"));
System.out.println(subject.isPermittedAll("select","delete","update","insert"));
}
}
4、加密
加密与解密主要使用Base64与MD5
package com.qainfeng;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.junit.Test;
public class TestEncryption {
@Test
public void encryptionBase64(){
//Base64编码可逆,编码后的长度与原字符串的长度是正比,可用于对图片进行编码
String str = "i am huwenlong,i love java";
byte[] encode = Base64.encode(str.getBytes());
System.out.println(new String(encode));
}
@Test
public void encrytionMD5(){
//MD5不可逆,长度固定32位
String str = "i am huwenlong,i love java";
String s = new Md5Hash(str, "hello", 3).toString();
System.out.println(s);
}
}
5、自定义Realm
shiro的iniRealm与jdbcRealm中有一些默认的设置,如果我们想创建一个与默认设置不一样的项目就需要自定义Realm,自定义Realm需要继承AuthorizingRealm并重写两个方法,一个方法用于认证,一个方法用于授权
MyRealm.java
package com.qainfeng.shiro;
import com.qainfeng.entity.Employee;
import com.qainfeng.entity.Permission;
import com.qainfeng.entity.Roles;
import com.qainfeng.service.EmployeeService;
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.subject.PrincipalCollection;
import java.util.List;
/**
* @author huwen
*/
public class MyRealm extends AuthorizingRealm {
private final EmployeeService service = new EmployeeService();
/**
*
* @param principalCollection principal的集合,可以理解为各种用户身份的集合,比如用户名、邮箱、手机号等
* @return 返回的是授权信息,包括角色与权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//SimpleAuthorizationInfo是AuthorizationInfo的子类
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
String empName = getAvailablePrincipal(principalCollection).toString();
List<Roles> roles = service.getAllRolesByEmpName(empName);
for (Roles role : roles) {
info.addRole(role.getRoleName());
}
List<Permission> permissions = service.getAllPermissionsByEmpName(empName);
for (Permission permission : permissions) {
info.addStringPermission(permission.getPermName());
}
return info;
}
/**
* 这个方法用于认证
* @param authenticationToken 用户名与密码
* @return 认证信息
* @throws AuthenticationException 可能引发的异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
AuthenticationInfo info = null;
//将authenticationToken强转为usernamePasswordToken,向下转型能够成功因为我们知道用户输入的是用户名与密码
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//从token中获取用户名与密码传给service,最后交给dao从数据库中查询
String username = token.getUsername();
//使用字节数组存储密码更为安全,因为字节数组是可变的,字符串是不可变的存在常量池中
char[] password = token.getPassword();
String pass = new String(password);
Employee emp = service.getEmpByNameAndPassword(username, pass);
//如果查询到的数据不为空,就构造一个SimpleAuthenticationInfo对象,将用户名与密码放在里里面
if(emp!=null && emp.getEmpId()!=0){
//getName()获取到的是当前Realm的标识,因为可以自定义多个Realm,不同的Realm需要区分
info = new SimpleAuthenticationInfo(username,pass,getName());
}
return info;
}
}
在这个类中我们创建了EmployeeService类型的对象,然后调用dao从数据库中获取数据。
Employee.java
package com.qainfeng.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/empServlet")
public class EmployeeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
String empName = req.getParameter("empName");
String password = req.getParameter("password");
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro3.ini");
SecurityManager manager = factory.getInstance();
SecurityUtils.setSecurityManager(manager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(empName,password);
try {
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
}
if (subject.isAuthenticated()){
resp.sendRedirect("main.jsp");
}else {
resp.getOutputStream().write("fail".getBytes());
}
}
}
这个servlet的作用是接收前台提交的登录表单数据然后登录。
shiro3.ini
[main]
myRealm=com.qainfeng.shiro.MyRealm
securityManager.realm=$myRealm
authc=org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authc.loginUrl=/index.html
[urls]
/index.html=anon
/main.jsp=authc
/manager.jsp=authc,roles[manager]
/guest.jsp=authc,roles[guest]
/select.jsp=perms[select]
/delete.jsp=perms[delete]
这个ini文件指定了自定义的Realm。
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
</web-app>
这个web.xml文件定义了一个监听器和过滤器,用于web项目的过滤。
main.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>这是主页</h1>
<shiro:authenticated>登录成功</shiro:authenticated> <br>
<shiro:hasRole name="manager">我是经理</shiro:hasRole> <br>
<shiro:hasRole name="guest">我是顾客</shiro:hasRole> <br>
<shiro:user>
欢迎用户 点击 <a href="index.html">此处</a>登录
</shiro:user> <br>
<shiro:hasPermission name="select">我可以查询</shiro:hasPermission> <br>
<shiro:hasPermission name="delete">我可以删除</shiro:hasPermission> <br>
</body>
</html>
这个jsp页面使用的shiro标签,这是一个新的标签,专门用于shiro与jsp页面结合配置权限。
网友评论