SHiro学习笔记
1.简介
Apache Shiro是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理。借助 Shiro 易于理解的 API,您可以快速轻松地保护任何应用程序,从最小的移动应用程序到最大的 Web 和企业应用程序。
-
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
- Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
2.结构
2.1外部结构
Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
2.2 内部结构
- 基础知识:
principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个Primary principals,一般是用户名/密码/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的principals和credentials组合就是用户名/密码了。
3. 简单Demo
- 这是一个简单的身份凭证对比例子,不涉及权限
- 身份凭证等信息是存储在shiro.ini文件中的,实际开发中是从数据库中取出。
@Test
public void test3(){
// 1.创建SecurityManger
DefaultSecurityManager manager = new DefaultSecurityManager();
// 2.为SecurityManager设置Realm
manager.setRealm(new IniRealm("classpath:shiro.ini"));
// 3.设置全局安全工具类
SecurityUtils.setSecurityManager(manager);
// 4.获取 关键对象Subject主体
Subject subject = SecurityUtils.getSubject();
// 5.创建令牌token
UsernamePasswordToken token = new UsernamePasswordToken("zs","123");
// 6.进行身份认证
try{
System.out.println("认证状态:"+subject.isAuthenticated());
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
}catch (Exception e){
e.printStackTrace();
}
// 7.注销认证
subject.logout();
}
- shiro.ini
;用户身份、凭证及各自的角色
[users]
zs = 123, admin
ls = 123, schwartz
ww = 123, goodguy
;用户角色
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
;配置应用程序的实例及其任何依赖项(如Realms)的地方
[main]
4.认证流程
- login流程
4.1 调用SecurityManager
- Subject的实现类的身份认证,其实质上也是调用SecurityManager模块的login方法。
- SecurityManager的实现类在login方法中进行身份认证。
- 返回验证信息
4.2 Realm中获取信息
- 区分多域与单域
- 从域中查找信息
- Realm中获取数据
- 先从缓存中拉取,(当设置数据库数据为Realm时,缓存可以缓解数据库压力)
- 如未找到,才从Realm中查找数据
- doGetAuthenticationInfo返回信息
4.3 匹配凭证
4.4 匹配成功
- 在匹配成功后,对设置的监听器进行唤醒。
4.5 配置RememberMe
- RememberMe管理器
4.6 结束
5.自定义Realm
5.1 观察Realm关系
- 发现在Authentication中有一个doGetAuthenticationInfo 方法:用于认证
- AuthorizingRealm中有一个doGetAuthorizationInfo方法:用于授权
5.2 简单自定义Realm认证
- 自定义的Realm
public class CustomerRealm extends AuthorizingRealm {
//授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if ("admin".equals(principal)){
// 1.正确的身份信息、2.正确的凭证信息、3.返回当前realm名
SimpleAuthenticationInfo admin = new SimpleAuthenticationInfo("admin", "123", this.getName());
return admin;
}
return null;
}
}
- 调用方式与demo差不多
public static void main(String[] args) {
DefaultSecurityManager manager = new DefaultSecurityManager();
manager.setRealm(new CustomerRealm());
SecurityUtils.setSecurityManager(manager);
Subject subject = SecurityUtils.getSubject();
// 获取口令/令牌
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123");
try{
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}catch (Exception e){
e.printStackTrace();
}
subject.logout();
}
5.3 MD5
- MD5主要用于加密和签名,还有文件检验
5.3.1 MD5加密
- MD5具有不可逆的特性,对内容相同的数据进行MD5算法处理后,结果也相同
demo:
Md5Hash md5Hash = new Md5Hash("12345678");
System.out.println(md5Hash.toHex()); // 25d55ad283aa400af464c76d713c07ad
5.3.2 盐Salt
- 为了防止穷举法破解MD5算法,需要在MD5加密前,给数据源进行加盐(Salt)
Md5Hash md5Hash = new Md5Hash("12345678","AF)(0*");
System.out.println(md5Hash.toHex()); //a1917c0170fc8b4e477e10fce3425ed1
5.3.3 Hash散列
- 为了提高数据安全性,Shiro中加入了Hash散列,可自定义散列次数。
Md5Hash md5Hash = new Md5Hash("12345678","AF)(0*",1024);
System.out.println(md5Hash.toHex()); //1e391e1c442c9a828e54d589fd6320fb
5.4 MD5加密Realm认证
- 一般登录需要从数据库中取出用户名、数据库密码、随机噪盐(在用户注册时,会随机产生一个随机字串作为盐)
public class CustomerMD5Realm extends AuthorizingRealm {
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if ("admin".equals(token.getPrincipal())){
// 1.数据库用户名、2.数据库密码、3.数据库噪盐
return new SimpleAuthenticationInfo("admin","1e391e1c442c9a828e54d589fd6320fb",
ByteSource.Util.bytes("AF)(0*"),this.getName());
}
return null;
}
}
- 在加上随机盐和散列次数后,那么其在使用上与demo有所不同
- 需要自行设置凭证匹配器
public static void main(String[] args) {
DefaultSecurityManager manager = new DefaultSecurityManager();
// 自定义的Realm
CustomerMD5Realm realm = new CustomerMD5Realm();
//创建要使用的凭证匹配器,不然会默认进行equals匹配
HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("md5");
// 设置散列次数
md5.setHashIterations(1024);
//设置凭证匹配器
realm.setCredentialsMatcher(md5);
manager.setRealm(realm);
SecurityUtils.setSecurityManager(manager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin","12345678");
try{
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}catch (Exception e){
e.printStackTrace();
}
subject.logout();
}
5.5 授权
-
授权,即访问控制。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是不能访问的。
-
授权可通过授权标识符来实现
[roles]
role1 = permission
6. Web
6.1 配置
[urls]
需要在[main
、users]
、[roles]
之后配置。
等号(=)右侧的标记是逗号分隔的过滤器列表,可针对与该路径匹配的请求执行这些过滤器。它必须与以下格式匹配:
url = filter1[optional_config1], filter2[optional_config2], ..., filterN[optional_configN]
-
[参数],中括号里的参数可以根据实际情况可以省略。
-
filterN是
[main]
中定义的过滤器bean的名称 -
= 号后面定义的过滤器列表组成过滤链,因此它们的顺序很重要。
6.2 Shiro自带过滤器
- Shiro自带了一些常用的过滤器,可以不在
[main]
中定义,可以直接使用
Filter Name | Class | 功能 |
---|---|---|
anon | AnonymousFilter | 指定Url可以匿名访问 |
authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username 、password 、rememberMe 等参数并尝试登录,如果登陆不了就会跳转到loginUrl配置得路径。我们也可以用这个过滤器做默认得登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
authcBearer | BearerHttpAuthenticationFilter | |
logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
port | PortFilter | 指定端口才可以访问 |
rest | HttpMethodPermissionFilter | 将HTTP请求方法转化成相应的动词来构造一个权限字符串,有兴趣看源码注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https才能访问 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
6.3 自定义过滤器
- 创建自定义过滤器后,也可如下操作,但需保证:
- 请确保您的过滤器子类为org.apache.shiro.web.filter.PathMatchingFilter
[main]
...
myFilter = com.company.web.some.FilterImplementation
myFilter.property1 = value1
...
[urls]
...
/some/path/** = myFilter
6.4 与Web Integration
- 在这里,我在Shiro的自带容器里实例化Realm和SecurityManager
[main]
myRealm = MyRealm
securityManager.realm = $myRealm
[urls]
/login = anon
/index.jsp = authc
- 在LoginServlet里进行验证
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject currentUser = SecurityUtils.getSubject();
try{
currentUser.login(token);
}catch (UnknownAccountException e){
System.out.println("账户输入有误!");
}catch (IncorrectCredentialsException e){
System.out.println("密码错误!");
}
if (currentUser.isAuthenticated()){
resp.sendRedirect("index.jsp");
}
}
- Web.xml里只需配置几项
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>loginservlet</servlet-name>
<servlet-class>com.gu_ppy.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>loginservlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<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>ASYNC</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
7. 配置
7.1 字符配置
- 凭证匹配器的编码转换
md5 = org.apache.shiro.authc.credential.HashedCredentialsMatcher
md5.StoredCredentialsHexEncoded = false ;false为Base64,默认为true十六进制编码
7.2 权限配置
- 权限与角色相关联
rolename
= 权限定义1,权限定义2,…,权限定义 N
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
- 注意!
[roles]
role1 = "printer:5thFloor:print,info" ;当需要在实例区域放多个时需要用""包含,否则容易产生解析错误
role2 = "printer:5thFloor:print,info",find
2*,…,权限定义 N
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
- 注意!
[roles]
role1 = "printer:5thFloor:print,info" ;当需要在实例区域放多个时需要用""包含,否则容易产生解析错误
role2 = "printer:5thFloor:print,info",find