Apache Shiro是Java的一个安全框架。相比于Spring Security,功能没那么强大,但是简单许多,下面我们了解的是在SpringBoot中集成Shiro框架。
一.Shiro框架介绍:
本次我pom.xml用的Shiro版本是
1.认证与授权相关基本概念
两个基本的概念
安全实体:系统需要保护的具体对象数据
权限:系统相关的功能操作,例如基本的CRUD
Authentication(认证):身份认证/登录,验证用户是不是拥有相应的身份-即登录;
Authorization(授权):授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
二.核心组件核心类
1.Subject:当前用户,SUbject可以是一个人,也可以是第三方服务,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
2.SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件,你可以把它看成DispatcherServlet前端控制器。
3.principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
4.credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的principals和credentials组合就是用户名/密码了。
5.Realms:用于进行权限信息的验证,需要自己实现。
6.Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。
域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
7.SimpleHash,可以通过特定算法(比如md5)配合盐值salt,对密码进行多次加密。
三、Shiro配置
1.SpringBoot集成Shiro一般通过java代码配合@Configuration和@Bean配置。
2.Shiro的核心是通过Filter实现的,Shiro中的Filter是通过URL规则来进行过滤和权限校验,因此我们需要定义一些关于URL的规则喝访问权限。
3.SpringBoot集成Shiro,我们需要写来给你个类,ShiroConfiguration-用来配置Shiro,注入各种Bean(包括过滤器(shiroFilter)、安全事务管理器(SecurityManager)、密码凭证(CredentialsMatcher)、aop注解支持(authorizationAttributeSourceAdvisor)等等) 和继承了AuthorizingRealm的Realm类,包括登陆认证(doGetAuthenticationInfo)、授权认证(doGetAuthorizationInfo)。
四.代码实现
ShiroConfiguration类:
package com.example.demo.config;
import cn.licoy.wdog.core.config.mybatis.UserRealm;
import cn.licoy.wdog.core.config.shiro.CredentialsMatcher;
import org.apache.log4j.Logger;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
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.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
private Logger logger=Logger.getLogger(ShiroConfiguration.class);
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//默认跳转到登陆页面
shiroFilterFactoryBean.setLoginUrl("/login");
//登陆成功后的页面
shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//自定义过滤器
Map filterMap=new LinkedHashMap<>();
shiroFilterFactoryBean.setFilters(filterMap);
//权限控制map
Map filterChainDefinitionMap=new LinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
// //:这是一个坑呢,一不小心代码就不好使了;
// //
// filterChainDefinitionMap.put("/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 核心的安全事务管理器
* 设置realm、cacheManager等
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager ();
//设置realm
securityManager.setRealm( myShiroRealm( ) );
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager( ehCacheManager() );
return securityManager;
}
/**
* 身份认证Realm,此处的注入不可以缺少。否则会在UserRealm中注入对象会报空指针.
* @return
*/
@Bean
public UserRealm myShiroRealm( ){
UserRealm myShiroRealm = new UserRealm();
myShiroRealm.setCredentialsMatcher( hashedCredentialsMatcher() );
return myShiroRealm;
}
/**
* 配置自定义的密码比较器
* @return
*/
@Bean
public CredentialsMatcher credentialsMatcher(){
return new CredentialsMatcher();
}
/**
* 哈希密码比较器。在myShiroRealm中作用参数使用
* 登陆时会比较用户输入的密码,跟数据库密码配合盐值salt解密后是否一致。
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用md5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5( md5(""));
return hashedCredentialsMatcher;
}
/**
* shiro缓存管理器;
* 需要注入对应的其它的实体类中: 安全管理器:securityManager
* 可见securityManager是整个shiro的核心;
* @return
*/
@Bean
public EhCacheManager ehCacheManager(){
logger.info("------------->ShiroConfiguration.getEhCacheManager()执行");
EhCacheManager cacheManager=new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return cacheManager;
}
/**
* 记住我管理器
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密钥 默认AES算法
// cookieRememberMeManager.setCipherKey();
return cookieRememberMeManager;
}
/**
* cookie对象
* @return
*/
@Bean
public Cookie rememberMeCookie() {
SimpleCookie simpleCookie=new SimpleCookie("rememberMe");
//记住我cookie生效时间,单位秒
simpleCookie.setMaxAge(3600);
return simpleCookie;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;否则@RequiresRoles等注解无法生效
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro生命周期处理器
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* 自动创建代理
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
}
自定义UserRealm-(MyRealm)类:
package cn.licoy.wdog.core.config.mybatis;
import com.example.demo.pojo.SysUserRole;
import com.example.demo.pojo.User;
import com.example.demo.service.UserSerevice;
import com.example.demo.utils.State;
import org.apache.log4j.Logger;
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 org.apache.shiro.util.ByteSource;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;
public class UserRealm extends AuthorizingRealm {
@Resource(name = "userServiceImpl")
private UserSerevice userService;
private Logger logger=Logger.getLogger(UserRealm.class);
/**
* 提供用户信息,返回权限信息
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("---------------------------->授权认证:");
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
String userName=(String) principals.getPrimaryPrincipal();
String userId=userService.findUserIdByName(userName);
Set roleIdSet=userService.findRoleIdByUid( Integer.parseInt(userId) );
Set roleSet=new HashSet<>();
Set pemissionIdSet=new HashSet<>();
Set pemissionSet=new HashSet<>();
for(SysUserRole roleInfo : roleIdSet) {
int roleId=roleInfo.getRoleId();
roleSet.add( userService.findRoleByRoleId( roleId ) );
//将拥有角色的所有权限放进Set里面,也就是求Set集合的并集
pemissionIdSet.addAll( userService.findPermissionIdByRoleId( roleId ));
}
for(int permissionId : pemissionIdSet) {
String permission= userService.findPermissionById( permissionId ).getPermission() ;
pemissionSet.add( permission );
}
// 将角色名称提供给授权info
authorizationInfo.setRoles( roleSet );
// 将权限名称提供给info
authorizationInfo.setStringPermissions(pemissionSet);
return authorizationInfo;
}
/**
* 提供帐户信息,返回认证信息
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("---------------------------->登陆验证:");
String userName=(String)authenticationToken.getPrincipal();
User user=userService.findUserByName(userName);
if(user==null) {
//用户不存在就抛出异常
throw new UnknownAccountException();
}
//State.LOCKED="2" 用户被锁定状态,自定义
if( State.LOCKED.equals( user.getState() ) ) {
//用户被锁定就抛异常
throw new LockedAccountException();
}
//密码可以通过SimpleHash加密,然后保存进数据库。
//此处是获取数据库内的账号、密码、盐值,保存到登陆信息info中
SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(user.getUserName(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()) ,
getName()); //realm name
return authenticationInfo;
}
}
更具体的代码,参见码云:
https://gitee.com/lufff1458/Shiro-Demo
可以参考watchdog整个框架中的shiro部分:
https://gitee.com/licoy/watchdog-framework/tree/master/src/main/java/cn/licoy/wdog/core/config
参考博客:
http://www.ityouknow.com/springboot/2017/06/26/springboot-shiro.html