(SpringBoot)Shiro安全框架深入解析

内容目录

  1. Shiro的整体架构介绍
  2. 框架验证流程与原理分析
  3. Url匹配模式
  4. 加密机制
  5. 缓存机制

1.Shiro的整体架构介绍

1.1从使用者角度看Shiro架构

(SpringBoot)Shiro安全框架深入解析

ApplicationCode为客户端,在Web环境中为登录的Controller,使用者只需要创建一个Subject对象,调用其上的login()方法,即可完成登录。在使用者角度只需要在SpringIOC容器中配置ShiroSecurityManager注入Realm即可简单使用,其中原理下面会提到。

Subject主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

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

Realm域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

在使用者的角度来说,只需要知道这三个东西即可完成简单的身份认证。

1.2从架构师角度看Shiro架构

(SpringBoot)Shiro安全框架深入解析

从上图可以看到,Subject可以是任何东西创建的,而其中认证的心脏其实是SecurityManager,不管是Subject的login方法还是Session操作还是判断是否有具体某个角色、权限的方法,其中都是调用底层的SecurityManager,SecurityManager里支持所有Shiro内置功能,包括认证器(可以在其中设置认证策略)、Reaml的设置和调用、SessionManager的管理、缓存机制的实现,所以SecurityManager被称为整个Shiro架构的心脏,大部分的功能全是在这里实现。而Reaml更像是一个数据源,在这里获取身份认证,授权信息,可以从各种方式获取,例如Oracle数据库Mysql数据库亦或是ini配置文件等等。Realm被配置在SecurityManager中,在登录授权等等操作时会调用配置的Realm(数据源)。

2.框架验证流程与原理分析

2.1身份认证流程

(SpringBoot)Shiro安全框架深入解析

其实在上面已经讲过了,结合上图更清楚的可以看出,整个流程就是在客户端调用Subject对象的login方法,里面传入一个参数token,这个token就是前端用户输入的账号密码,封装成token对象传入即可,底层还是SecurityManager调用认证器(第三步),在认证器中选择具体认证策略(第四步),最后去Realms(数据源)中验证用户是否存在等等。

在这里值得一提的是,上图Realms或许不止一个,这是因为Shiro可以支持多个Realms,即多个数据源,上面也提到,可以去Oracle找数据,也可以去Mysql找数据或者其他什么途径,用户数据可能存在各个地方,这个时候就可以配置多个Realm,这里就引申出认证策略的问题,也就是上图中的第四步,Shiro中默认的认证策略为至少一个Realm认证成功即视为成功,使用场景是用户信息可能被存放在多个地方,此时只需要找到一个地方匹配了用户的信息就算登录成功。这里认证策略还有AllSuccess即全部Realm认证成功才视为成功等等的认证策略。

2.2在SpringIOC需要配置的一些Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package com.shiro.shiroConfig;
 
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
 
import javax.servlet.Filter;
 
import net.sf.ehcache.hibernate.EhCacheRegionFactory;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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 com.shiro.filter.URLPathMatchingFilter;
import com.shiro.realm.DatabaseRealm;
 
@Configuration
public class ShiroConfiguration {
        /**
	 * 这是管理Shiro生命周期的Bean
	 *
	 */
	@Bean
	public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
		return new LifecycleBeanPostProcessor();
	}
 
	/**
	 * ShiroFilterFactoryBean 处理拦截资源文件问题。
	 * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
	 * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
	 *
	 * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
	 * 3、部分过滤器可指定参数,如perms,roles
	 *
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
		System.out.println("ShiroConfiguration.shirFilter()");
//		SimpleHash sh = new SimpleHash("MD5", "123456", null, 3);
//		System.out.println(sh);
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
 
		// 必须设置 SecurityManager
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
		shiroFilterFactoryBean.setLoginUrl("/login");
		// 登录成功后要跳转的链接
		shiroFilterFactoryBean.setSuccessUrl("/index");
		// 未授权界面;
		shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
		// 拦截器.
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
		// 自定义拦截器
		Map<String, Filter> customisedFilter = new HashMap<>();
		customisedFilter.put("url", getURLPathMatchingFilter());
 
		// 配置映射关系
		filterChainDefinitionMap.put("/login", "anon");
		filterChainDefinitionMap.put("/index", "anon");
		filterChainDefinitionMap.put("/static/**", "anon");
		filterChainDefinitionMap.put("/config/**", "anon");
		filterChainDefinitionMap.put("/doLogout", "logout");
		filterChainDefinitionMap.put("/deleteOrder", "roles[admin]");
		//filterChainDefinitionMap.put("/**", "anon");
		filterChainDefinitionMap.put("/**", "url");
		shiroFilterFactoryBean.setFilters(customisedFilter);
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		return shiroFilterFactoryBean;
	}
 
	public URLPathMatchingFilter getURLPathMatchingFilter() {
		return new URLPathMatchingFilter();
	}
 
	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		// 设置realm.
		securityManager.setRealm(getDatabaseRealm());
		//设置缓存
		securityManager.setCacheManager(getCacheManager());
 
		return securityManager;
	}
 
	@Bean
	public DatabaseRealm getDatabaseRealm() {
		DatabaseRealm myShiroRealm = new DatabaseRealm();
		myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		return myShiroRealm;
	}
 
	/**
	 * 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
	 * 所以我们需要修改下doGetAuthenticationInfo中的代码; )
	 * 
	 * @return
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcher() {
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
 
		hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
		hashedCredentialsMatcher.setHashIterations(2);// 散列的次数,比如散列两次,相当于
														// md5(md5(""));
 
		return hashedCredentialsMatcher;
	}
 
	/**
	 *
	 * 缓存框架
	 * @return
	 */
	@Bean
	public EhCacheManager getCacheManager(){
		EhCacheManager ehCacheManager = new EhCacheManager();
		ehCacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml");
		return ehCacheManager;
	}
 
	/**
	 * 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
	 * 
	 * @param securityManager
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}
}

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.Filter;

import net.sf.ehcache.hibernate.EhCacheRegionFactory;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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 com.shiro.filter.URLPathMatchingFilter;
import com.shiro.realm.DatabaseRealm;

@Configuration
public class ShiroConfiguration {
/**
* 这是管理Shiro生命周期的Bean
*
*/
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}

/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*
* Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
// SimpleHash sh = new SimpleHash("MD5", "123456", null, 3);
// System.out.println(sh);
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 自定义拦截器
Map<String, Filter> customisedFilter = new HashMap<>();
customisedFilter.put("url", getURLPathMatchingFilter());

// 配置映射关系
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/index", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/config/**", "anon");
filterChainDefinitionMap.put("/doLogout", "logout");
filterChainDefinitionMap.put("/deleteOrder", "roles[admin]");
//filterChainDefinitionMap.put("/**", "anon");
filterChainDefinitionMap.put("/**", "url");
shiroFilterFactoryBean.setFilters(customisedFilter);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

public URLPathMatchingFilter getURLPathMatchingFilter() {
return new URLPathMatchingFilter();
}

@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(getDatabaseRealm());
//设置缓存
securityManager.setCacheManager(getCacheManager());

return securityManager;
}

@Bean
public DatabaseRealm getDatabaseRealm() {
DatabaseRealm myShiroRealm = new DatabaseRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}

/**
* 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码; )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);// 散列的次数,比如散列两次,相当于
// md5(md5(""));

return hashedCredentialsMatcher;
}

/**
*
* 缓存框架
* @return
*/
@Bean
public EhCacheManager getCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml");
return ehCacheManager;
}

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

大致讲一下需要在SpringIOC容器需要配置的一些bean。

  1. LifecycleBeanPostProcessor:没什么好说,它管理Shiro的生命周期。
  2. ShiroFilterFactoryBean:运用了工厂模式,这个Bean需要注入一系列依赖,有Shiro的心脏SecurityManager、登录的页面url设置、登录后要跳转的url设置、未授权的页面的url设置、拦截器链的设置。
  3. SecurityManager:Shiro的心脏,由上面架构介绍可以知道,认证器、缓存器、Realm数据源都在这里配置注入,这里我没有用到认证策略,使用默认认证器即可,这里我注入了缓存器和Realm,以便之后认证底层使用到。
  4. DatabaseRealm:这个是我自定义的Realm,new出自定义类,因为我这里还使用到了密码机制(在下面会详细介绍),所以注入一个Shiro自带的密码器。为什么在Realm中配置密码器呢,可以这样理解,Realm是我们获取身份信息的数据源,则每一个获取身份信息(例如账号密码)都是独立的一种策略,如Oracle是用MD5加密存储密码,MySQL又是用其他加密算法,而Realm负责提供数据,所以是在这里配置的密码算法。配置此Realm用于注入上面声明的“心脏”中。
  5. hashedCredentialsMatcher:Shiro自带的密码器,我这里使用其中一种,哈希加密算法,其中设置其属性为算法名称和散列次数,在上面注释中应该写的很清楚了。配置此密码器用在注入上面我们声明的自定义Realm中。
  6. CacheManager:缓存管理器,用于注入与“心脏”中。这里我使用的是Ehcache框架实现缓存,Shiro也有对应的JAR包,new一个管理器,设置属性为缓存配置文件用于读取缓存设置。

2.3具体底层的流程(部分源码分析) 

那么,具体的流程是怎么样的呢?我们可以启动项目看看控制台。

(SpringBoot)Shiro安全框架深入解析

可以看出,在tomcat启动时,自动给我们加上了一个过滤器---shiroFilter,拦截一切请求(/*),这个过滤器就是我们在SpringIOC容器中配置的过滤器链(在上面配置文件中注释可以看到filterChainDefinitions)具体拦截模式下面会介绍到。

(SpringBoot)Shiro安全框架深入解析由上图应该大致可以看出来了吧?浏览器在访问页面时,总会被我们配置好了的ShiroFilter拦截,在这个拦截器中调用配置好了的对应的拦截器链,根据拦截模式调用具体的拦截器(filterChainDefinitions中配置的各种比如anon、roles等等都是拦截器)。如果是没有经过认证的会重定向到我们配置好的登录URL。

登录控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.shiro.controller;
 
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
/**
 * @Author:linyh
 * @Date: 2018/8/11 19:24
 * @Modified By:
 */
@Controller
@RequestMapping("")
public class LoginControlller {
 
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public String login(Model model, String name, String password) {
		Subject subject = SecurityUtils.getSubject();
		UsernamePasswordToken token = new UsernamePasswordToken(name, password);
		try {
			subject.login(token);
			Session session = subject.getSession();
			session.setAttribute("subject", subject);
			return "redirect:index";
		} catch(IncorrectCredentialsException e){
			model.addAttribute("error", "密码错误");
			return "login";
		} catch(AuthenticationException e) {
			model.addAttribute("error", "验证失败");
			return "login";
		}
	}
 
}

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
* @Author:linyh
* @Date: 2018/8/11 19:24
* @Modified By:
*/
@Controller
@RequestMapping("")
public class LoginControlller {

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(Model model, String name, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
try {
subject.login(token);
Session session = subject.getSession();
session.setAttribute("subject", subject);
return "redirect:index";
} catch(IncorrectCredentialsException e){
model.addAttribute("error", "密码错误");
return "login";
} catch(AuthenticationException e) {
model.addAttribute("error", "验证失败");
return "login";
}
}

}

所有未经认证的路径都能请求到这个Controller中,也就是我们之前配置好了的过滤链中的/login,这个路径也是anon过滤器,其含义为可以匿名访问,不需要进行认证。

这里面的代码逻辑就是我们上面说过的,从Shiro的工具类调用静态方法获取一个Subject(这里的Subject为线程独有的),然后new一个UsernamePasswordToken,顾名思义就是存放用户名和密码的,在其构造器中传入用户名与密码即可。接着调用Subject的login方法,传入token完成登录。如果密码错误底层抛出IncorrectCredentialsException异常,可以捕捉异常然后重定向到你想要去的页面,可以去一个错误页面,也可以还在登录页面中,在model存放一个错误信息,在view中显示此信息即可。这里值得一提的是Shiro封装的一些异常类。如果身份验证失败请捕获AuthenticationException或其子类,常见的如: DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库;

底层login方法都干了什么?

我们进入login方法看一看~

(SpringBoot)Shiro安全框架深入解析

这里的login方法归根结底还是调用了我们配置的Shiro心脏嘛!方法传入两个参数一个为当先线程的Subject,一个为用户输入的账号密码(封装为token传入)。接着进去看看~

(SpringBoot)Shiro安全框架深入解析

这里心脏的某个实现类调用了其上父类的一个认证方法,继续进去看看~

(SpringBoot)Shiro安全框架深入解析

它调用了认证器的一个认证方法,这里认证器是new出来的一个认证器

(SpringBoot)Shiro安全框架深入解析

继续进去看看吧~

(SpringBoot)Shiro安全框架深入解析

在这里token如果是空则会抛出一个异常。

这是一个认证器抽象类,继续调用其实现了认证方法的子类的认证方法~进去看看

1
2
3
4
5
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        this.assertRealmsConfigured();
        Collection<Realm> realms = this.getRealms();
        return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
    }

这就是上面new出的一个认证器中的认证方法,走了这么多步,这里才真正准备开始实现认证,这里先调用this.getRealms获取Realms,因为我们已经在心脏中依赖注入了一个Realms,所以这里会自动获取到我们的自定义Realm,接下来判断size是否为1,其实就是在判断是否是多Realm模式,我们这里是单Realm,所以会进入doSingleRealm方法中,传入两个参数,第一个调用集合的迭代器传入这个唯一的Realm, 第二个传入我们之前封装的token。继续进去看看~

(SpringBoot)Shiro安全框架深入解析

如果token不支持这个realm,会抛出一个异常。

在这里,调用realm中的获取认证类方法,如果获取不到具体认证类,也就是为null,抛出一个异常为找不到对应的Realm获取认证对象。下面继续进去看看~

(SpringBoot)Shiro安全框架深入解析

首先注意此类名,就是我们自定义Realm所继承的类,也就是说此抽象类将会调用它的子类也就是我们的自定义类的doGetAuthenticationInfo方法。

值得一提的是看第二个红框,这里认证对象会先从缓存中获取,如果获取不到才会去自定义Realm中获取。继续进去看看~

(SpringBoot)Shiro安全框架深入解析这里的DatabaseRealm就是我们自定义的Realm,最终调用了我们自己写的这个方法,返回一个认证的对象,此认证对象用SimpleAuthenticationInfo这个对象封装,里面放入数据库中的账号、密码(如有用到密码机制中的盐,也可传入盐参数)、最后一个参数为本Realm的名称,直接调用getName方法即可。

这里封装好了这个数据库的用户认证对象拿来干什么用的呢?回到我们上一个父类Realm中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
        if (info == null) {
            info = this.doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                this.cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }
 
        if (info != null) {
            this.assertCredentialsMatch(token, info);
        } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
        }
 
        return info;
    }

if (info != null) {
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}

return info;
}

在这里可以看到,调用完我们自己定义的Realm的方法返回的一个认证实例,将会与前端输入的token作为两个参数传入assertCredentialsMatch方法中,这个方法才是主要验证的方法,进去看看吧

(SpringBoot)Shiro安全框架深入解析

在这里可以看到,在验证信息是否正确的时候,先获取密码器,也就是我们之前就已经在Realm中注入好了的那个密码器(如果无需密码器也可以,后面就只是明文比对,没有加密的过程),接着调用这个密码器去验证账号是否正确,进入验证方法看看~

(SpringBoot)Shiro安全框架深入解析这里即为最终BOSS,最后底层去调用equals把这个token(前端用户输入的信息),和Realm数据库得到的账号信息进行比对,如果为false即为不匹配,在上面的上面也可以看到,如果为false就会抛出一个密码错误的异常,这个异常在上面也提到过,如果匹配,返回true,则程序继续运行下去,做一些必要的配置,我们只需要了解这一个过程即可了解Shiro底层实现。

源码总结

首先在客户端调用Subject的login方法,其实就是在调用心脏中的login方法,而这个方法历经千辛万苦最终调用到自定义的Realm中的认证方法,返回一个认证实例,在用这个认证实例与token作对比,在这个过程中,认证失败会直接抛出异常,而认证如果成功则不会抛出异常,程序正常走下去,所以可以在客户端捕获异常后重定向到index(你所想要登录之后跳转的首页)。

阅读源码,我们可以知道,只要配置了Realm,其实就大致完成了整个的认证过程,其底层无非就是调用里面的方法获取认证实例,其余工作都交给Shiro来完成。接着在SpringIOC容器中配置必要的bean,注入必要的依赖就可以说已经完成了认证。

从这个总结中就可以大致知道如何编写我们的自定义Realm类了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.shiro.realm;
 
import java.util.Set;
 
import org.apache.shiro.SecurityUtils;
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.authc.UsernamePasswordToken;
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.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
 
import com.shiro.pojo.User;
import com.shiro.service.PermissionService;
import com.shiro.service.RoleService;
import com.shiro.service.UserService;
 
/**
 * @Author:linyh
 * @Date: 2018/8/11 18:26
 * @Modified By:
 */
public class DatabaseRealm extends AuthorizingRealm {
 
	@Autowired
	private UserService userService;
	@Autowired
	private RoleService roleService;
	@Autowired
	private PermissionService permissionService;
 
	/**
	 * @Description:授权用户角色与权限
	 *
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
		String userName = (String) principalCollection.getPrimaryPrincipal();
		// 通过Service获取角色与权限集合
		Set<String> permissions = permissionService.listPermissions(userName);
		Set<String> roles = roleService.listRoleNames(userName);
 
		// 授权对象
		SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
		// 把通过Service获取到的角色和权限放进去
		authorizationInfo.setStringPermissions(permissions);
		authorizationInfo.setRoles(roles);
		return authorizationInfo;
	}
 
	/**
	 * @Description:
	 *
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
			throws AuthenticationException {
		// 获取账号密码
		UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
		//String userName = authenticationToken.getPrincipal().toString();
		String userName = token.getUsername();
		User user = userService.getByName(userName);
		String passwordInDB = user.getPassword();
		String salt = user.getSalt();
 
		//这里盐值可以是主键
		SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB,
				ByteSource.Util.bytes(salt), getName());
 
		return authenticationInfo;
	}
 
	/**
	* @Description:清除缓存
	* @Param:
	* @return:
	*/
	public void clearCache(){
		PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
		super.clearCache(principals);
	}
}

import java.util.Set;

import org.apache.shiro.SecurityUtils;
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.authc.UsernamePasswordToken;
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.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import com.shiro.pojo.User;
import com.shiro.service.PermissionService;
import com.shiro.service.RoleService;
import com.shiro.service.UserService;

/**
* @Author:linyh
* @Date: 2018/8/11 18:26
* @Modified By:
*/
public class DatabaseRealm extends AuthorizingRealm {

@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;

/**
* @Description:授权用户角色与权限
*
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String userName = (String) principalCollection.getPrimaryPrincipal();
// 通过Service获取角色与权限集合
Set<String> permissions = permissionService.listPermissions(userName);
Set<String> roles = roleService.listRoleNames(userName);

// 授权对象
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 把通过Service获取到的角色和权限放进去
authorizationInfo.setStringPermissions(permissions);
authorizationInfo.setRoles(roles);
return authorizationInfo;
}

/**
* @Description:
*
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 获取账号密码
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//String userName = authenticationToken.getPrincipal().toString();
String userName = token.getUsername();
User user = userService.getByName(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();

//这里盐值可以是主键
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB,
ByteSource.Util.bytes(salt), getName());

return authenticationInfo;
}

/**
* @Description:清除缓存
* @Param:
* @return:
*/
public void clearCache(){
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
}

理解了源码,就可以知道首先就是继承我们的AuthorizingRealm类(当然,如果你只想要做身份认证的话,可以继承另一个身份认证Realm,我这里需要做身份授权与身份认证),底层会调用这个抽象类的一系列方法,而我们只需要继承它,实现两个do方法即可,轻松简单。而两个do方法返回认证实例,为什么返回认证实例就可以完成认证的原理大家也都知道了把,无非就是把这个实例和token在抽象父类中的一个方法中拿着注入的密码器进行比较,返回比较结果。

3.Url匹配模式

3.1shiroFilter中过滤器链配置细节

[urls]部分的配置,格式为:“url=拦截器[参数],拦截器[参数]…

如果当前请求的url匹配[urls]部分的某个url模式,将会执行其配置的拦截器。

anonanonymous)拦截器  表示匿名访问,不需要登录即可访问

authcauthentication)拦截器表示需要身份认证通过后才能访问

还有一些shiro自带的拦截器

3.2shiro默认的过滤器

(SpringBoot)Shiro安全框架深入解析

介绍比较常用的几个把。

  1. anon:这个过滤器可以使对应url无需认证直接访问。
  2. authc:这个过滤器表示需要进行认证(即登录之后才可以访问的页面)。
  3. logout:这个过滤器可以使你的账号退出(如有缓存清除缓存)。
  4. roles:这个过滤器可以有参数,为具体角色名,而角色名在Realm中获取,主要比较是否有此角色,拥有此角色才可访问此url。
  5. perms:与roles大同小异,比较是否有此权限,拥有此权限才可访问此url。

3.3Url匹配模式(Ant表达式)

Ant路径通配符支持?、*、**,注意通配符匹配不包括目录分隔符“/”:

-?:匹配一个字符,如/admin?可以匹配/admin1,但不匹配/admin/admin/1

-*:匹配零个或多个字符串,如/admin将匹配/admin/admin12,但不匹配/admin/1

-**:匹配路径中的零个或多个路径,如/admin/**将匹配/admin/a/admin/a/b

3.4Url匹配顺序

Url权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的url模式对应的拦截器链。

如:

-/bb/**=filter1

-/bb/aa=filter2

-/**=filter3

-如果请求的url是“/bb/aa”,因为按照声明顺序进行匹配。那么将使用filter1进行拦截。

4.加密机制

在上文已经有提到,Shiro自带一些加密的工具以供我们对密码进行加密,我们只需要把加密工具配置在数据源(Realm)中即可完成加密验证。

1
2
3
4
5
6
    @Bean
	public DatabaseRealm getDatabaseRealm() {
		DatabaseRealm myShiroRealm = new DatabaseRealm();
		myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		return myShiroRealm;
	}

其将在认证时自动使用此密码器将前端收到的密码进行加密,而后与数据库密码进行比对,故此可以理解是配置在每个Realm中的原因了。Realm是获取认证信息的地方,所以每个Realm将会有不同的加密策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码; )
* 
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
 
     hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
    hashedCredentialsMatcher.setHashIterations(2);// 散列的次数,比如散列两次,相当于
														// md5(md5(""));
    return hashedCredentialsMatcher;
}

hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);// 散列的次数,比如散列两次,相当于
// md5(md5(""));
return hashedCredentialsMatcher;
}

在这里配置具体的加密工具,然后注入到对应的Realm中即可。这里只需要选择对应的工具类,设置属性如加密算法名称、加密次数等。

4.1盐的概念 

为什么要引入盐的概念呢?盐是什么?假设我们用的加密工具没有盐,那么在加密之后,相同的密码在加密后是相同的,这会有什么后果呢?比如密码12345加密后为abcde,在数据库存放的时候,每个12345的密码他都是abcde,这是不合理的,这样破解者就会知道,abcde就是对应12345,反加密变得透明。那么盐是什么呢?它就像炒菜,同一个人用同一个食材做出来的菜其实味道是一样的,关键就在调料放了多少,不同的调料(盐)可以让菜的味道就算是由同一个人做出的味道也可以是不同。就像我们的加密过程,同一个密码(12345),但是它的盐不同,就算加密算法相同,加密出来的结果也会是不同,由盐和算法两个决定,这样反加密就变得不太好做了。

我们通常会用主键作为我们的盐值,因为主键唯一且能标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
			throws AuthenticationException {
		// 获取账号密码
		UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
		//String userName = authenticationToken.getPrincipal().toString();
		String userName = token.getUsername();
		User user = userService.getByName(userName);
		String passwordInDB = user.getPassword();
		String salt = user.getSalt();
 
		//这里盐值可以是主键
		SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB,
				ByteSource.Util.bytes(salt), getName());
 
		return authenticationInfo;
	}

//这里盐值可以是主键
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB,
ByteSource.Util.bytes(salt), getName());

return authenticationInfo;
}

在Realm中,原本直接返回用户名和密码构造的验证实体,现在可以传入一个盐值,利用ByteSource的工具类来处理盐值。因为我们上面已经在Realm中配置过加密工具了,所以这里不加盐值的话会默认使用"MD5"算法(上面配置加密工具时设置)进行密码加密,如果加了盐值,在算法加密之后会再进行盐值加密过程。

4.2注册

注册也是加密的一个过程,因为解密加密一定是用的同一个算法才可以得到要的相同结果。

1
2
3
4
5
6
SimpleHash sh1 = new SimpleHash("MD5", "123456", ByteSource.Util.bytes("1"), 3);
SimpleHash sh2 = new SimpleHash("MD5", "123456", ByteSource.Util.bytes("1"), 3);
SimpleHash sh3 = new SimpleHash("MD5", "123456", ByteSource.Util.bytes("2"), 3);
System.out.println(sh1);
System.out.println(sh2);
System.out.println(sh3);

加密123456,第三个参数为盐值(可以传入主键作为盐值),这里是哈希加密算法,所以3为散列次数。

(SpringBoot)Shiro安全框架深入解析

从结果可以看出,在使用相同盐值1的情况下,123456加密的结果都是相同的,而在使用不同的盐值2的情况下,123456加密的结果就不同了。

由此可以知道,只要在注册的Service层使用SimpleHash这个类来加密密码来进行注册即可。在验证账号时Realm会将前端用户输入的密码进行相同的加密算法,加入相同的盐(在注册时加密的盐,如为主键则在数据库中取,所以也是相同的),匹配两个加密过的结果是否相同来完成解密过程。

5.缓存机制

为什么要用缓存呢?有兴趣的可以在Realm中doGetAuthorizationInfo授权方法也就是给用户配置拥有的角色与权限的授权方法打一个断点,然后在页面上访问一些需要角色或权限才能访问的url,你会发现每次访问都会进入断点,这个授权方法中调用的Service层的方法,去数据库拿对应用户拥有的角色权限,试想一下,我每访问一个url,都要去数据库重复拿这些数据,显然是一个不成熟的做法,如果有了缓存,我们只需要查询一次数据库,之后访问url都将从缓存中拿数据而不是数据库,这样数据库压力明显降低,看起来也相对成熟。

其次,用户拥有的角色和权限这部分数据是极少更改的,可能在配置时就已经定下来了,所以用缓存是一个明智且必须的选择。

5.1缓存配置

1
2
3
4
5
6
7
8
9
10
11
        <!-- shiro-ehcache -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.2.2</version>
        </dependency>
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache-core</artifactId>
            <version>2.6.8</version>
        </dependency>

ehcache与Shiro整合的依赖

1
2
3
4
5
6
7
8
9
10
	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		// 设置realm.
		securityManager.setRealm(getDatabaseRealm());
		//设置缓存
		securityManager.setCacheManager(getCacheManager());
 
		return securityManager;
	}

return securityManager;
}

在上文也有提到,缓存是配置在SecurityManager中的。这里我使用了最简单的用ehcache缓存框架来实现缓存。

1
2
3
4
5
6
7
8
9
10
	/**
	 * 缓存框架
	 * @return
	 */
	@Bean
	public EhCacheManager getCacheManager(){
		EhCacheManager ehCacheManager = new EhCacheManager();
		ehCacheManager.setCacheManagerConfigFile("src/main/java/com/shiro/shiroConfig/shiro-ehcache.xml");
		return ehCacheManager;
	}

在配置缓存框架Bean中new一个EhCacheManager,设置配置文件路径。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache>
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

配置完成后,可以再在Realm的授权方法中打上断点,实验缓存是否有效。可以发现,用户只在第一次访问权限url时才会进入断点,之后访问url都不会进入断点。

5.2清除缓存

在用户正常退出(logout)时缓存会自动清理,或是缓存框架设置的缓存时间过了之后缓存也会清理。

如需手动清除缓存,例如你修改了一个用户的权限,你想要他马上不能访问某某关键的地址,但有缓存的存在即使你修改了权限也不能立即生效,则可在Realm中定义方法clearCached(),在修改权限或角色的Service层中的delete、update等等方法中调用清除缓存方法即可实现修改权限立即生效。由于角色和权限的数据不常修改,所以也不会影响多少性能。

1
2
3
4
5
6
7
8
9
	/**
	* @Description:清除缓存
	* @Param:
	* @return:
	*/
	public void clearCache(){
		PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
		super.clearCache(principals);
	}

根本为调用父类的清除缓存方法,传入当前线程的用户凭证,清除缓存。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

关注我们