1. Shiro概述
Shiro是apache旗下一个开源安全框架(http://shiro.apache.org/),它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
2. Shiro流程
3. Shrio架构
3.1 主要架构
在概念层面,Shiro 架构包含三个主要的理念
其中:
1)Subject :主体对象,负责提交用户认证和授权信息。
2)SecurityManager:安全管理器,负责认证,授权等业务实现。
3)Realm:领域对象,负责从数据层获取业务数据。
3.2 详细架构
Shiro框架进行权限管理时,要涉及到的一些核心对象,主要包括:认证管理对象,授权管理对象,会话管理对象,缓存管理对象,加密管理对象以及Realm管理对象(领域对象:负责处理认证和授权领域的数据访问题)等
其中:
1)Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
2)SecurityManager(安全管理器) :Shiro 的核心,用来协调管理组件工作。
3)Authenticator(认证管理器):负责执行认证操作。
4)Authorizer(授权管理器):负责授权检测。
5)SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一个强有力的 Session 体验。
6)SessionDAO:代表 SessionManager 执行 Session 持久(CRUD)动作,它允许任何存储的数据挂接到 session 管理基础上。
7)CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
8)Cryptography(加密管理器):提供了加密方式的设计及管理。
Realms(领域对象):是shiro和你的应用程序安全数据之间的桥梁。
4. Shiro拦截实现
4.1 添加shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.0</version>
</dependency>
4.2 Shiro核心对象配置
基于SpringBoot 实现的项目中,我们的shiro应用基本配置如下:。
第一步:创建SpringShiroConfig类
package com.cy.pj.common.config;
/**@Configuration 注解描述的类为一个配置对象,
* 此对象也会交给spring管理
*/
@Configuration
public class SpringShiroConfig {
}
第二步:在Shiro配置类中添加SecurityManager配置(这里一定要使用org.apache.shiro.mgt.SecurityManager这个接口对象)
/**
* 构建并初始化SecurityManager对象,然后将此对象交给spring管理.
* 说明:@Bean注解应用于@Configuration注解描述的类的内部,通过此注解描述的方法,
* 方法的返回值会交给spring管理,默认bean的名字为方法名.
* @return 返回值shiro中的安全管理器对象,是shiro框架的核心,此对象中实现了
* 认证,授权,会话,缓存,加密等一列功能的实现.
*/
@Bean //bean的名字默认为方法名
//@Scope("singleton")//默认
public SecurityManager securityManager(){
DefaultWebSecurityManager sManager=new DefaultWebSecurityManager();
return sManager;
}
第三步: 在Shiro配置类中添加ShiroFilterFactoryBean对象的配置。通过此对象设置资源匿名访问、认证访问
/**构建并初始化ShiroFilterFactoryBean对象通过此对象,
* 创建过滤器工厂,进而通过过滤器工厂创建过滤器(filter),
* 并通过过滤器对请求信息进行过滤,例如检测此请求是否需要
* 认证或此请求是否已认证.
* */
@Bean
//@Autowired //可以省略
public ShiroFilterFactoryBean shiroFilterFactoryBean(
SecurityManager securityManager){
ShiroFilterFactoryBean fBean=new ShiroFilterFactoryBean();
//设置securityManager,基于此对象进行认证检测
fBean.setSecurityManager(securityManager);
sfBean.setLoginUrl("/doLoginUI");//拦截后,跳转到登录页面
//定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
LinkedHashMap<String,String> map= new LinkedHashMap<>();
//静态资源允许匿名访问:"anon"
map.put("/bower_components/**","anon");//AnonFilter
map.put("/build/**","anon");
map.put("/dist/**","anon");
map.put("/plugins/**","anon");
//除了匿名访问的资源,其它都要认证("authc")后访问
map.put("/**","authc");//这句话要写在匿名访问的后面(有顺序要求)
fBean.setFilterChainDefinitionMap(map);
return fBean;
}
其配置关系图:
5. Shiro认证实现
5.1 认证流程分析
身份认证即判定用户是否是系统的合法用户,用户访问系统资源时的认证(对用户身份信息的认证)流程图
其中认证流程分析如下:
1)系统调用subject的login方法将用户信息提交给SecurityManager
2)SecurityManager将认证操作委托给认证器对象Authenticator
3)Authenticator将用户输入的身份信息传递给Realm。
4)Realm访问数据库获取用户信息然后对信息进行封装并返回。
5)Authenticator 对realm返回的信息进行身份认证。
5.2 认证业务实现
1)在SysUserDao接口中,添加根据用户名获取用户对象的方法
@Select("select * from sys_users where username=#{username}")
SysUser findUserByUserName(String username)
- Realm业务
我们编写realm时,要继承
AuthorizingRealm并重写相关方法,完成认证及授权业务数据的获取及封装
@Component
public class ShiroUserRealm extends AuthorizingRealm {
@Autowired
private SysUserDao sysUserDao;
/**负责用户权限信息的获取和封装*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
return null;
}
/**负责认证信息的获取和封装*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取登录用户信息(用户名,密码)
UsernamePasswordToken token=
(UsernamePasswordToken)authenticationToken;
String username=token.getUsername();
//2.基于用户名查询数据库中的用户对象
SysUser user=sysUserDao.findUserByUserName(username);
//3.检测用户是否存在
if(user==null)
throw new UnknownAccountException();
//4.检测用户是否被禁用
if(user.getValid()==0)
throw new LockedAccountException();
//5.封装用户信息并返回
ByteSource byteSource=ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo info=
new SimpleAuthenticationInfo(
user,//principal (用户身份)
user.getPassword(),//credentials (凭证-已加密密码)
byteSource,//credentialsSalt(凭证盐)
getName());
return info;
}
/**设置加密匹配器**/
// @Override
// public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
// HashedCredentialsMatcher cMatcher=new HashedCredentialsMatcher();
// cMatcher.setHashAlgorithmName("MD5");
// cMatcher.setHashIterations(1);
// super.setCredentialsMatcher(cMatcher);
// }
@Override
public CredentialsMatcher getCredentialsMatcher() {
HashedCredentialsMatcher cMatcher=new HashedCredentialsMatcher();
cMatcher.setHashAlgorithmName("MD5");
cMatcher.setHashIterations(1);
//cMatcher.setStoredCredentialsHexEncoded(true);
return cMatcher;
}
}
3)对此realm,需要在SpringShiroConfig配置类中,注入给SecurityManager对象,修改securityManager方法
@Bean
public SecurityManager securityManager(Realm realm) {
DefaultWebSecurityManager sManager=new DefaultWebSecurityManager();
sManager.setRealm(realm);
return sManager;
}
4)处理客户端的登陆请求,例如获取用户名,密码等然后提交该shiro框架进行认证
@RequestMapping("doLogin")
public JsonResult doLogin(String username,String password){
//1.获取Subject对象
Subject subject= SecurityUtils.getSubject();
//2.通过Subject提交用户信息,交给shiro框架进行认证操作
//2.1对用户进行封装
UsernamePasswordToken token=new UsernamePasswordToken();
token.setUsername(username);
token.setPassword(password.toCharArray());
//2.2对用户信息进行身份认证
subject.login(token);//提交给securityManager
return new JsonResult("login ok");
}
5)修改shiroFilterFactory的配置,对/user/doLogin这个路径进行匿名访问的配置
map.put("/user/doLogin",“anon”);
@Bean
//@Autowired //可以省略
public ShiroFilterFactoryBean shiroFilterFactoryBean(
SecurityManager securityManager){
ShiroFilterFactoryBean fBean=new ShiroFilterFactoryBean();
//设置securityManager,基于此对象进行认证检测
fBean.setSecurityManager(securityManager);
sfBean.setLoginUrl("/doLoginUI");//拦截后,跳转到登录页面
//定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
LinkedHashMap<String,String> map= new LinkedHashMap<>();
//静态资源允许匿名访问:"anon"
map.put("/bower_components/**","anon");//AnonFilter
map.put("/build/**","anon");
map.put("/dist/**","anon");
map.put("/plugins/**","anon");
map.put("/user/doLogin","anon");
//除了匿名访问的资源,其它都要认证("authc")后访问
map.put("/**","authc");//这句话要写在匿名访问的后面(有顺序要求)
fBean.setFilterChainDefinitionMap(map);
return fBean;
}
6)当我们在执行登录操作时,为了提高用户体验,可对系统中的异常信息进行处理,在统一异常处理类中添加
@ExceptionHandler(ShiroException.class)
@ResponseBody
public JsonResult doHandleShiroException(
ShiroException e) {
JsonResult r=new JsonResult();
r.setState(0);
if(e instanceof UnknownAccountException) {
r.setMessage("账户不存在");
}else if(e instanceof LockedAccountException) {
r.setMessage("账户已被禁用");
}else if(e instanceof IncorrectCredentialsException) {
r.setMessage("密码不正确");
}else if(e instanceof AuthorizationException) {
r.setMessage("没有此操作权限");
}else {
r.setMessage("系统维护中");
}
e.printStackTrace();
return r;
}
6. Shiro退出实现
在shiroFilterFactory配置类中,修改过滤规则
添加 map.put("/doLogout",“logout”);
@Bean
//@Autowired //可以省略
public ShiroFilterFactoryBean shiroFilterFactoryBean(
SecurityManager securityManager){
ShiroFilterFactoryBean fBean=new ShiroFilterFactoryBean();
//设置securityManager,基于此对象进行认证检测
fBean.setSecurityManager(securityManager);
sfBean.setLoginUrl("/doLoginUI");//拦截后,跳转到登录页面
//定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
LinkedHashMap<String,String> map= new LinkedHashMap<>();
//静态资源允许匿名访问:"anon"
map.put("/bower_components/**","anon");//AnonFilter
map.put("/build/**","anon");
map.put("/dist/**","anon");
map.put("/plugins/**","anon");
map.put("/user/doLogin","anon");
map.put("/doLogout","logout");
//除了匿名访问的资源,其它都要认证("authc")后访问
map.put("/**","authc");//这句话要写在匿名访问的后面(有顺序要求)
fBean.setFilterChainDefinitionMap(map);
return fBean;
}
7. Shiro授权实现
7.1 授权流程
其中授权流程分析如下:
1)系统调用subject相关方法将用户信息(例如isPermitted)递交给SecurityManager。
2)SecurityManager将权限检测操作委托给Authorizer对象。
3)Authorizer将用户信息委托给realm。
4)Realm访问数据库获取用户权限信息并封装。
5)Authorizer对用户授权信息进行判定。
7.2 添加授权配置
/** 配置授权对应的Advisor对象,此对象会在spring启动时加载,并且通过此
* 对象可以找到@RequiresPermissions注解描述的方法,然后这些方法在运
* 行时,由此Advisor对象,调用SecurityManager中的checkPermissions方法
* 检查用户权限,并为访问目标切入点方法的用户做授权操作.
* */
@Bean
public AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
7.3 授权业务实现
@Component
public class ShiroUserRealm extends AuthorizingRealm {
@Autowired
private SysUserDao sysUserDao;
@Autowired
private SysUserRoleDao sysUserRoleDao;
@Autowired
private SysRoleMenuDao sysRoleMenuDao;
@Autowired
private SysMenuDao sysMenuDao;
/**负责用户权限信息的获取和封装*/
//基于多表查询
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principalCollection) {
System.out.println("==doGetAuthorizationInfo==");
//1.获取登录用户
SysUser user=(SysUser) principalCollection.getPrimaryPrincipal();
//2.基于登录用户查询用户对应的权限标识
List<String> perimssionList=
sysMenuDao.findUserPermissions(user.getId());
if(perimssionList==null||perimssionList.size()==0)
throw new AuthorizationException();
//3.封装用户权限信息并返回(交给SecurityManager对象)
Set<String> set=new HashSet<>();
for(String per:perimssionList){
if(!StringUtils.isEmpty(per)){
set.add(per);
}
}
System.out.println("set="+set);
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.setStringPermissions(set);
return info;
}
//基于单表查询
// @Override
// protected AuthorizationInfo doGetAuthorizationInfo(
// PrincipalCollection principalCollection) {
// //1.获取登录用户
// SysUser user=(SysUser) principalCollection.getPrimaryPrincipal();
// //2.基于登录用户查询用户对应的角色id并校验.
// List<Integer> roleIds=sysUserRoleDao.findRoleIdsByUserId(user.getId());
// if(roleIds==null||roleIds.size()==0)
// throw new AuthorizationException();
// //3.基于角色id查找菜单id并校验
// List<Integer> menuIds=sysRoleMenuDao.findMenuIdsByRoleIds(roleIds);
// if(menuIds==null||menuIds.size()==0)
// throw new AuthorizationException();
// //4.基于菜单id找到授权标识并校验
// List<String> perimssionList=sysMenuDao.findPermissions(menuIds);
// if(perimssionList==null||perimssionList.size()==0)
// throw new AuthorizationException();
// //5.封装用户权限信息并返回(交给SecurityManager对象)
// Set<String> set=new HashSet<>();
// for(String per:perimssionList){
// if(!StringUtils.isEmpty(per)){
// set.add(per);
// }
// }
// System.out.println("set="+set);
// SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
// info.setStringPermissions(set);
// return info;
// }
}
多表sql
<!--基于用户id进行多表查询获取用户权限-->
<select id="findUserPermissions" resultType="string">
select m.permission
from sys_user_roles ur join sys_role_menus rm join sys_menus m
on ur.role_id=rm.role_id and rm.menu_id=m.id
where ur.user_id=#{userId}
</select>
单表sql
<!--基于多个菜单id找到对应的权限标识-->
<select id="findPermissions" resultType="string">
select permission
from sys_menus
where id in <!--(1,2,3,4)-->
<foreach collection="menuIds" open="(" close=")" separator="," item="menuId">
#{menuId}
</foreach>
</select>
7.4 授权注解
@RequiresPermissions(“sys:user:update”)//注解中的字符串为一个权限标识
8.Shiro缓存配置
当进行授权操作时,每次都会从数据库查询用户权限信息,为了提高授权性能,可以将用户权限信息查询出来以后进行缓存,下次授权时从缓存取数据即可
/**
* 配置CacheManager对象,此对象中管理着一个Cache对象,此cache可以
* 存储授权时获取的用户权限信息,下次在授权时可以直接从缓存取用户权限,
* 这样可以减少对数据库的访问压力,并提高其授权性能.
* FAQ?我们知道此管理器内置一个Cache对象,那请问谁来调用此管理器并获取cache呢?
* SecurityManager (因为此对象负责授权,授权就需要获取用户权限.)
* @return
*/
@Bean
public CacheManager shiroCacheManager(){
return new MemoryConstrainedCacheManager();
}
将缓存对象注入给SecurityManager对象
@Bean //bean的名字默认为方法名
//@Scope("singleton")//默认
public SecurityManager securityManager(Realm realm,
CacheManager cacheManager){
DefaultWebSecurityManager securityManager=
new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setCacheManager(cacheManager);
return securityManager;
}
9.Shiro记住我功能
记住我功能是要在用户登录成功以后,假如关闭浏览器,下次再访问系统资源时,无需再执行登录操作
在SysUserController中的doLogin方法中基于是否选中记住我,设置token的setRememberMe方法
if(isRememberMe) {
token.setRememberMe(true);
}
@RequestMapping("doLogin")
public JsonResult doLogin(String username,String password,boolean isRememberMe){
Subject subject= SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken();
token.setUsername(username);
token.setPassword(password.toCharArray());
if(isRememberMe)
token.setRememberMe(true);
subject.login(token);//提交给securityManager
return new JsonResult("login ok");
}
添加记住我配置
@Bean
public RememberMeManager rememberMeManager(){
CookieRememberMeManager rManager=new CookieRememberMeManager();
SimpleCookie cookie=new SimpleCookie("rememberMe");
cookie.setMaxAge(7*24*60*60);
//假如没有设置maxAge,此cookie对象为会话cookie,此cookie会在浏览器关闭时生命周期结束
rManager.setCookie(cookie);
return rManager;
}
将记住我对象注入给SecurityManager对象
@Bean //bean的名字默认为方法名
//@Scope("singleton")//默认
public SecurityManager securityManager(Realm realm,
CacheManager cacheManager,
RememberMeManager rememberMeManager,
SessionManager sessionManager){
DefaultWebSecurityManager securityManager=
new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setCacheManager(cacheManager);
securityManager.setRememberMeManager(rememberMeManager);
return securityManager;
}
修改shiro的过滤认证级别
//除了匿名访问的资源,其它都要认证("authc")后访问
//map.put("/**","authc");//这句话要写在匿名访问的后面(有顺序要求)
map.put("/**","user");//可以从用户浏览器cookie中读取账号信息进行身份认证
10.Shiro会话管理
使用shiro框架实现认证操作,用户登录成功会将用户信息写入到会话对象中,其默认时长为30分钟
添加会话管理配置
/**
* Session 是什么?
* 此对象是在服务端记录客户端与服务端会话状态的一个对象,
* 这个对象一般是一个会话创建一个,并且会有一个唯一标识(JSESSIONID).
* 可以通过这样的对象来记录登录用户信息,记录购物车信息,记录验证码信息
* SessionManager 是什么?管理session的一个对象
* @return
*/
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(60*60*1000);//设置超时时间为1个小时
//session对象由谁服务端创建,
//但这个对象创建好以后,会将其jsessionid以会话cookie的形式写到客户端
//客户端再访问服务器时,会携带jsessionid到服务端,然后基于这个id找到session.
sessionManager.setSessionIdUrlRewritingEnabled(false);//关闭url重写.
//一般假如浏览器禁用了cookie,我们可重写url,
//此时会在url的后面添加一个jsessionid,服务端可以基于这个id为客户端找到对应session
return sessionManager;
}
将会话对象注入给SecurityManager对象
@Bean //bean的名字默认为方法名
//@Scope("singleton")//默认
public SecurityManager securityManager(Realm realm,
CacheManager cacheManager,
RememberMeManager rememberMeManager,
SessionManager sessionManager){
DefaultWebSecurityManager securityManager=
new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setCacheManager(cacheManager);
securityManager.setRememberMeManager(rememberMeManager);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
11.shiro在springboot中的改进
11.1 配置类编写
@Configuration //此注解描述的类为spring中的配置类
public class SpringShiroConfig {
@Bean
public Realm realm() {
return new ShiroUserRealm();//自己实现的Realm类
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
LinkedHashMap<String,String> map=new LinkedHashMap<>();
map.put("/bower_components/**","anon");
map.put("/build/**","anon");
map.put("/dist/**","anon");
map.put("/plugins/**","anon");
map.put("/user/doLogin","anon");
map.put("/doLogout","logout");
map.put("/**","user");
chainDefinition.addPathDefinitions(map);
return chainDefinition;
}
@Bean
protected CacheManager shiroCacheManager() {
return new MemoryConstrainedCacheManager();
}
}
其余跳转路径、记住我等功能在配置文件中配置
例如:
shiro:
loginUrl: /doLoginUI
rememberMeManager:
cookie:
name: rememberMe
domain: null
path: null
secure: false
具体配置参见官方文档配置说明
http://shiro.apache.org/spring-boot.html