Apache Shiro
Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理.
简介:
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
- Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的。
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查。
- Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去。
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
- Testing:提供测试支持。
- Web Support:Web支持,可以非常容易的集成到 web 环境。
- Subject:主体,可以看到主体可以是任何与应用交互的“用户”。
- SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher。它是 Shiro 的核心,所有具体的交互都通过 SecurityManager 进行控制。它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,我们可以自定义实现。其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了。
- Authrizer:授权器,或者访问控制器。它用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能。
- Realm:可以有1个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的。它可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等。
- SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 需要有人去管理它的生命周期,这个组件就是 SessionManager。而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境。
- SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD。我们可以自定义 SessionDAO 的实现,控制 session 存储的位置。如通过 JDBC 写到数据库或通过 jedis 写入 redis 中。另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能。
- CacheManager:缓存管理器。它来管理如用户、角色、权限等的缓存的。因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。
- Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。
运行流程:
- Subject:主体,代表了当前“用户”。这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等。所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager。我们可以把 Subject 认为是一个门面,SecurityManager 才是实际的执行者。
- SecurityManager:安全管理器。即所有与安全有关的操作都会与 SecurityManager 交互,且它管理着所有 Subject。可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,我们可以把它看成 DispatcherServlet 前端控制器。
- Realm:域。Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法,也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作。我们可以把 Realm 看成 DataSource,即安全数据源。
shiro 过滤器:
anon,authcBasic,auchc,user 是认证过滤器。
perms,roles,ssl,rest,port 是授权过滤器。
anon: 表示可以匿名使用。
java类:【org.apache.shiro.web.filter.authc.AnonymousFilter】
authc:表示需要认证(登录)才能使用,没有参数
java类:【org.apache.shiro.web.filter.authc.FormAuthenticationFilter】
roles:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,如admins/user/**=roles[“admin,guest”],每个参数通过才算通过,相当于hasAllRoles()方法。
【org.apache.shiro.web.filter.authz.RolesAuthorizationFilter】
perms:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms[“user:add:,user:modify:”],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
【org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter】
rest:根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
【org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter】
port:当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
【org.apache.shiro.web.filter.authz.PortFilter】
authcBasic:没有参数表示httpBasic认证
【org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter】
ssl: 表示安全的url请求,协议为https
【org.apache.shiro.web.filter.authc.LogoutFilter】
user: 当登入操作时不做检查
【org.apache.shiro.web.filter.authc.UserFilter】
logout:【org.apache.shiro.web.filter.authc.LogoutFilter】
noSessionCreation:【org.apache.shiro.web.filter.session.NoSessionCreationFilter】
/service/ ** =anon <!-- 该uri可以匿名访问-->
/service/ ** =auth <!-- 该uri需认证访问-->
/service/ ** =authcBasic <!-- 该uri需要 httpBasic 认证-->
/service/ ** =perms[user:sys:use]<!-- 该uri需要 用户拥有user:sys:use 权限才能访问-->
/service/ ** =port[8081] <!-- 该uri需要使用8081端口-->
/service/ ** =rest[user] <!-- /admins/ **=perms[user:method],其中,method 表示 get、post、delete 等-->
/service/ ** =roles[admin] <!-- uri 需要认证用户拥有 admin 角色才能访问-->
/logout=logou # 表示注销,可以当作固定配置
/service/ **=user # 表示该 uri 需要认证或通过记住我认证才能访问
POM依赖
<!-- shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- shiro-ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
- == Relam 实现 ==
public class UserRelam extends AuthorizingRealm {
private static final Logger Logger = LoggerFactory.getLogger(UserRelam.class);
@Autowired
private UserMapper userMapper;
/*执行授权(为当前登录的Subject授予角色和权限)
* 如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置
* 超过这个时间间隔再刷新页面,该方法会被执行
* 当访问到页面的时候,使用了相应的注解或者shiro标签才会执行此方法否则不会执行
* 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可
* (non-Javadoc)
* @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Logger.info("用户登陆成功开始执行授权操作");
// 获取当前用户
String username = (String)principals.getPrimaryPrincipal();
// TODO 授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 获取权限
Set<String> perms = userMapper.findPermsByUsername(username);
// 获取角色
Set<String> roles = userMapper.findRolesByUsername(username);
info.setStringPermissions(perms);
info.setRoles(roles);
Logger.info("{} 的权限和角色是:{}{}", username, perms, roles);
return info;
}
/*执行认证 查询数据库用户信息,
* (non-Javadoc)
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
Logger.info("用户开始执行shiro 认证操作");
// 获取用户的信息
UsernamePasswordToken userToken = (UsernamePasswordToken)token;
// 用户名
String username = userToken.getUsername();
// 查询数据库 返回信息
UserTo userTo = userMapper.findUserByUsername(username);
if (userTo == null) {
throw new AuthenticationException("用户名信息错误");
}
// SimpleHash sHash = new SimpleHash("MD5", passWord, ByteSource.Util.bytes("password"), 1024); 不访问数据库自己操作
Logger.info("usertO 基本信息是:{}", userTo);
// 传入:用户名,加密后的密码,盐值,该realm的名字,加密算法和加密次数在已经在配置文件中指定
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, userTo.getPassword(),
ByteSource.Util.bytes(userTo.getSalt()), getName());
return info;
}
// 权限修改生效后,立即刷新清空缓存,则可以实现用户不退出生效新的权限
// 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,调用clearCached方法
public void clearCache() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
}
- Shiro 核心配置类
@Configuration
public class ShiroConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(ShiroConfig.class);
/**
* 内置过滤器
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
LOGGER.info("shiro 过滤器开始执行");
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 配置shiro n内置过滤器
Map<String, String> filterMap = new HashMap<>();
/*
* anon:表示可以匿名使用。
authc:表示需要认证(登录)才能使用,没有参数
roles:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
port:当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
authcBasic:没有参数表示httpBasic认证
ssl:表示安全的url请求,协议为https
user:当登入操作时不做检查
*/
// 设置login url
factoryBean.setLoginUrl("/service/page/login");
// 设置成功后的跳转的连接
factoryBean.setSuccessUrl("/service/page/index");
factoryBean.setUnauthorizedUrl("/service/error-400");
filterMap.put("/web/**", "anon");
// 静态资源处理
filterMap.put("/js/**", "anon");
filterMap.put("/css/**", "anon");
filterMap.put("/images/**", "anon");
filterMap.put("/font/**", "anon");
// 当请求/shiro/logout时登
filterMap.put("/service/logout", "logout");
filterMap.put("/service/login", "anon");
/*
* 注意:这里的user是过滤器的一种,而下面roles[user]中的user是自定义的一种角色。
* 注意:user拦截器既允许通过Subject.login()认证进入的用户访问;又允许通过rememberMe缓存进入的用户访问
* 注意:authc拦截器既只允许通过Subject.login()认证进入的用户访问;不允许rememberMe缓存通过进入的用户访问
*/
filterMap.put("/introduce.html", "user");
filterMap.put("/rememberMe.html", "user");
// 注意roles[user]这里的话,角色不要再用引号引起来了,直接写即可
filterMap.put("/user.html", "authc,roles[user]");
filterMap.put("/admin.html", "authc,roles[admin]");
filterMap.put("/superadmin.html", "authc,roles[host,admin]");
// 由于权限由上而下“就近”选择,所以一般将"/**"配置在最下面
filterMap.put("/**", "anon");// 必须授权才能访问
factoryBean.setFilterChainDefinitionMap(filterMap);
LOGGER.info("shiro 过滤器结束执行");
return factoryBean;
}
@Bean
public DefaultWebSecurityManager getSecurityManager(UserRelam userRelam, SessionManager sessionManager,
EhCacheManager cacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置Relam
securityManager.setRealm(userRelam);
// 自定义缓存实现,
securityManager.setCacheManager(cacheManager);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public HashedCredentialsMatcher getHashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");// MD5加密
hashedCredentialsMatcher.setHashIterations(1024);// 迭代1024次
return hashedCredentialsMatcher;
}
/**
* 配置relam 以及凭证匹配器,也可以在里面重写CredentialsMatcher 方法
*
* @param hashedCredentialsMatcher
* @return
*/
@Bean
public UserRelam getUserRelam(HashedCredentialsMatcher hashedCredentialsMatcher) {
UserRelam userRelam = new UserRelam();
userRelam.setCredentialsMatcher(hashedCredentialsMatcher);
return userRelam;
}
/** 配置shiro框架组件的生命周期管理对象 */
@Bean
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 配置授权属性应用对象(在执行授权操作时需要用到此对象) 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/** 配置负责为Bean对象(需要授权访问的方法所在的对象) 创建代理对象的Bean组件 */
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
/** 使用缓存ehcachae */
@Bean
public EhCacheManager getEhCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
// 将ehcacheManager转换成shiro包装后的ehcacheManager对象
// ehCacheManager.setCacheManager(cacheManager);
// eheache 配置文件目录
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return ehCacheManager;
}
/**
* shiro session的管理
*
* @return
*/
@Bean
public SessionManager sessionManager(SimpleCookie simpleCookie, SessionDAO sessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(100000);// ms
// 设置sessionDao对session查询,在查询在线用户service中用到了
// sessionManager.setSessionDAO(sessionDAO);
// 配置session 的监听
// Collection<SessionListener> listeners = new ArrayList<SessionListener>();
// listeners.add(new BDsess)
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdCookieEnabled(true);
// 设置cookie 中sessionid 的名称
sessionManager.setSessionIdCookie(simpleCookie);
return sessionManager;
}
@Bean
public SessionDAO getSessionDAO() {
return new MemorySessionDAO();
}
@Bean
public SimpleCookie getSimpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("jeesite.session.id");
return simpleCookie;
}
}
1.LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。主要是AuthorizingRealm类的子类,以及EhCacheManager类。
2.HashedCredentialsMatcher,这个类是为了对密码进行编码的,防止密码在数据库里明码保存,当然在登陆认证的生活,这个类也负责对form里输入的密码进行编码。
3.ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
4.EhCacheManager,缓存管理,用户登陆成功后,把用户信息和权限信息缓存起来,然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库。
5.SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
6.ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
7.DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
8.AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
- 测试类
@Controller
@RequestMapping
public class UserController {
private static final Logger LOGGER = LoggerFactory.getLogger(GloableExecption.class);
@Autowired
private UserService userService;
@Autowired
private UserMapper userMapper;
@RequestMapping("/login")
public String login(UserTo userTo) {
LOGGER.info("开始执行登陆操作 username:{},password:{}", userTo.getUsername(), userTo.getPassword());
Subject subject = SecurityUtils.getSubject();// 创建用户实体
System.err.println(subject.isRemembered() + "" + subject.isAuthenticated());
// if (!subject.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(userTo.getUsername(), userTo.getPassword());
try {
// 执行登陆验证
subject.login(token);
LOGGER.info("恭喜登陆成功");
} catch (UnknownAccountException e) {
throw new ServiceException("用户名不存在");
} catch (IncorrectCredentialsException e) {
throw new ServiceException("用户名或者密码错误");
}
// }
return "index";
}
@RequestMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
LOGGER.info("用户登出成功");
return "index";
}
@RequestMapping("/select")
@ResponseBody
@RequiresPermissions(value = {"system:user:delete"})
public Object selectUser() {
// userService.insertUserList(new ArrayList<>());
return userMapper.findUserByUsername("wang3");
}
@RequestMapping("/insert")
@ResponseBody
@RequiresPermissions(value = {"system:user:insert"})
public Object insertUser(UserTo userTo) {
Subject subject = SecurityUtils.getSubject();
System.err.println("进来了");
return userMapper.findUserByUsername("wang3");
}
/**
* 尝试通过异常让事务回滚
*
* @return
*/
@RequestMapping("trans")
@ResponseBody
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) // 开启事务,异常事务回滚
public Object transTest() {
UserTo user = new UserTo();
user.setAge(18);
user.setBrithday(new Date());
user.setCreteTime(new Date());
user.setModifyTime(new Date());
user.setLastLoginTime(new Date());
String salt = String.valueOf(System.currentTimeMillis());
SimpleHash sHash = new SimpleHash("MD5", "password", ByteSource.Util.bytes(salt), 1024);
user.setSalt(salt);
user.setPassword(sHash.toString());
user.setUsername("wang");
user.setGender("women");
// 获取自增的主键
userMapper.insertUser(user);
LOGGER.info("新增后主键是:{}", user.getId());
// int i = 0 / 0;// 制造异常 看事务回滚
return null;
}
// 配置错误界面
@RequestMapping("error-400")
public String toPage400() {
return "err";
}
@Autowired
private UserRelam userRelam;
// 登陆界面
@RequestMapping("page/login")
public String log() {
userRelam.clearCache();// 刷新权限不许要重新登录
return "login";
}
// 主界面
@RequestMapping("page/index")
public String index() {
return "index";
}
}
缓存 文件
<ehcache>
<!-- Sets the path to the directory where cache .data files are created.
If the path is a Java System Property it is replaced by
its value in the running VM.
The following properties are translated:
user.home - User's home directory
user.dir - User's current working directory
java.io.tmpdir - Default temp file path -->
<diskStore path="java.io.tmpdir"/>
<cache name="authorizationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authenticationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<!--Default Cache configuration. These will applied to caches programmatically created through
the CacheManager.
The following attributes are required for defaultCache:
maxInMemory - Sets the maximum number of objects that will be created in memory
eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the element
is never expired.
timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
if the element is not eternal. Idle time is now - last accessed time
timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
if the element is not eternal. TTL is now - creation time
overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache
has reached the maxInMemory limit.
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"/>
<!--Predefined caches. Add your cache configuration settings here.
If you do not have a configuration for your cache a WARNING will be issued when the
CacheManager starts
The following attributes are required for defaultCache:
name - Sets the name of the cache. This is used to identify the cache. It must be unique.
maxInMemory - Sets the maximum number of objects that will be created in memory
eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the element
is never expired.
timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
if the element is not eternal. Idle time is now - last accessed time
timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
if the element is not eternal. TTL is now - creation time
overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache
has reached the maxInMemory limit.-->
<!-- Place configuration for your caches following -->
<cache name="sampleCache1"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<cache name="sampleCache2"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/>
</ehcache>
参靠考项目地址: