企业中使用的主流的权限框架主要有 Apache Shiro或者Spring Security,两者有哪些区别呢?
-
Apache Shiro比Spring Security , 前者使用更简单
-
Shiro 功能强大、 简单、灵活, 不跟任何的框架或者容器绑定,可以独立运行
-
Spring Security 对Spring 体系支持比较好,脱离Spring体系则很难开发
-
SpringSecutiry 支持Oauth鉴权 Spring Security OAuth,Shiro需要自己实现
不过企业中使用shiro的会比较多,因为上手会比较容易
一、Shiro的简单介绍
Apache Shiro官网 Apache Shiro | Simple. Java. Security.
Apache Shiro的四大核心模块为:身份认证,授权,会话管理和加密
-
什么是身份认证
- Authentication,身份证认证,一般就是登录
-
什么是授权
- Authorization,给用户分配角色或者访问某些资源的权限
-
什么是会话管理
- Session Management, 用户的会话管理员,多数情况下是web session
-
什么是加密
- Cryptography, 数据加解密,比如密码加解密等
Shiro中一些常用的api
-
Subject
- 我们把用户或者程序称为主体(如用户,第三方服务,cron作业),主体去访问系统或者资源
-
SecurityManager
- 安全管理器,Subject的认证和授权都要在安全管理器下进行
-
Authenticator
- 认证器,主要负责Subject的认证
-
Realm
- 数据域,Shiro和安全数据的连接器,好比jdbc连接数据库; 通过realm获取认证授权相关信息
-
Authorizer
- 授权器,主要负责Subject的授权, 控制subject拥有的角色或者权限
-
Cryptography
- 加解密,Shiro的包含易于使用和理解的数据加解密方法,简化了很多复杂的api
-
Cache Manager
- 缓存管理器,比如认证或授权信息,通过缓存进行管理,提高性能
二、使用实践:Springboot2.x整合 Apache Shiro
-
整合Shiro相关jar包
-
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
在项目实际使用中,需要自定义realm
-
realm作用:Shiro 从 Realm 获取安全数据(存放在数据库中的用户及权限数据,查询只有封装到realm类中的认证对象信息和授权对象信息中)
-
默认自带的realm:idae查看realm继承关系,有默认实现和自定义继承的realm
-
两个概念
- principal : 主体的标示,可以有多个,但是需要具有唯一性,常见的有用户名,手机号,邮箱等
- credential:凭证, 一般就是密码
- 所以一般我们说 principal + credential 就账号 + 密码
-
开发中,往往是自定义realm , 即继承 AuthorizingRealm抽象类,需要覆写两个方法;分别是用于认证的方法和授权的方法
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 进行权限校验的时候回调用
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("授权 doGetAuthorizationInfo");
String username = (String)principals.getPrimaryPrincipal();
User user = userService.findAllUserInfoByUsername(username);
List<String> stringRoleList = new ArrayList<>();
List<String> stringPermissionList = new ArrayList<>();
List<Role> roleList = user.getRoleList();
for(Role role : roleList){
stringRoleList.add(role.getName());
List<Permission> permissionList = role.getPermissionList();
for(Permission p: permissionList){
if(p!=null){
stringPermissionList.add(p.getName());
}
}
}
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(stringRoleList);
simpleAuthorizationInfo.addStringPermissions(stringPermissionList);
return simpleAuthorizationInfo;
}
/**
* 用户登录的时候会调用
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证 doGetAuthenticationInfo");
//从token获取用户信息,token代表用户输入,获取到用户名,或者是账号之类的
String username = (String)token.getPrincipal();
User user = userService.findAllUserInfoByUsername(username);
//取密码
String pwd = user.getPassword();
if(pwd == null || "".equals(pwd)){
return null;
}
return new SimpleAuthenticationInfo(username, user.getPassword(), this.getClass().getName());
}
}
或者:
public class ShiroRealm extends AuthorizingRealm{
private static final Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
private static final String CURRENT_USER = "CURRENT_USER";
@Autowired
private IUserService userService;
//@Autowired
//private IMemberService memberService;
// 权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
String userId = "";
Subject currentUser = SecurityUtils.getSubject();
if (null != currentUser) {
try {
Session session = currentUser.getSession();
if (null != session) {
userId = (String) session.getAttribute(CURRENT_USER);
}
} catch (InvalidSessionException e) {
logger.error(e.toString());
}
}
long uId = 0;
uId = StringUtils.isEmpty(userId) ? uId :Long.parseLong(userId);
UserVO sysUser = userService.getUserRole(uId);
info.addRoles(sysUser.getRolesName());
Map<String,Object> map = new HashMap<String,Object>();
map.put("ids", sysUser.getRolesId());
List<Map<String,Object>> list = userService.getRolePrivilege(map);
Set<String> set = new HashSet<String>();
for(Map<String,Object> lt :list){
set.add(String.valueOf(lt.get("privilegeName")));
}
info.addStringPermissions(set);
info.addStringPermission("user");
return info;
}
// 登录验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
throws AuthenticationException {
UsernamePasswordToken token=(UsernamePasswordToken) authcToken;
logger.info("验证当前Subject时获取到token为:" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));
String loginName = token.getUsername();
long userId = 0;
userId = StringUtils.isEmpty(loginName) ? userId :Long.parseLong(loginName);
StringBuilder sb = new StringBuilder(100);
for (int i = 0; i < token.getPassword().length; i++) {
sb.append(token.getPassword()[i]);
}
try {
UserVO user = userService.getUserRole(userId);
if(user != null){
if (user.getPassword().equals(sb.toString())) {
Subject currentUser = SecurityUtils.getSubject();
logger.info("当前Subject为:"+currentUser);
if (null != currentUser) {
Session session = currentUser.getSession();
logger.info("当前session为:"+session);
if (null != session) {
session.setAttribute(CURRENT_USER, userId);
logger.info("登录验证成功:"+userId);
}
}
//saveSession(user.getUserId());
AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUserId(), user.getPassword(),
user.getUserName());
return authcInfo;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
以及shiro的其他相关配置,可以在配置类中进行配置:
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
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.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
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 ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
System.out.println("执行 ShiroFilterFactoryBean.shiroFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//必须设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//需要登录的接口,如果访问某个接口,需要登录却没登录,则调用此接口(如果不是前后端分离,则跳转页面)
shiroFilterFactoryBean.setLoginUrl("/pub/need_login");
//登录成功,跳转url,如果前后端分离,则没这个调用
shiroFilterFactoryBean.setSuccessUrl("/");
//没有权限,未授权就会调用此方法, 先验证登录-》再验证是否有权限
shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit");
//设置自定义filter
Map<String,Filter> filterMap = new LinkedHashMap<>();
filterMap.put("roleOrFilter",new CustomRolesOrAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//拦截器路径,坑一,部分路径无法进行拦截,时有时无;因为同学使用的是hashmap, 无序的,应该改为LinkedHashMap
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//退出过滤器
filterChainDefinitionMap.put("/logout","logout");
//匿名可以访问,也是就游客模式
filterChainDefinitionMap.put("/pub/**","anon");
//登录用户才可以访问
filterChainDefinitionMap.put("/authc/**","authc");
//管理员角色才可以访问
filterChainDefinitionMap.put("/admin/**","roleOrFilter[admin,root]");
//有编辑权限才可以访问
filterChainDefinitionMap.put("/video/update","perms[video_update]");
//坑二: 过滤链是顺序执行,从上而下,一般讲/** 放到最下面
//authc : url定义必须通过认证才可以访问
//anon : url可以匿名访问
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//如果不是前后端分离,则不必设置下面的sessionManager
securityManager.setSessionManager(sessionManager());
//使用自定义的cacheManager
securityManager.setCacheManager(cacheManager());
//设置realm(推荐放到最后,不然某些情况会不生效)
securityManager.setRealm(customRealm());
return securityManager;
}
/**
* 自定义realm
* @return
*/
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm = new CustomRealm();
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customRealm;
}
/**
* 密码加解密规则
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置散列算法:这里使用的MD5算法
credentialsMatcher.setHashAlgorithmName("md5");
//散列次数,好比散列2次,相当于md5(md5(xxxx))
credentialsMatcher.setHashIterations(2);
return credentialsMatcher;
}
//自定义sessionManager
@Bean
public SessionManager sessionManager(){
CustomSessionManager customSessionManager = new CustomSessionManager();
//超时时间,默认 30分钟,会话超时;方法里面的单位是毫秒
//customSessionManager.setGlobalSessionTimeout(20000);
//配置session持久化
customSessionManager.setSessionDAO(redisSessionDAO());
return customSessionManager;
}
/**
* 配置redisManager
*
*/
public RedisManager getRedisManager(){
RedisManager redisManager = new RedisManager();
redisManager.setHost("localhost");
redisManager.setPort(6379);
return redisManager;
}
/**
* 配置具体cache实现类
* @return
*/
public RedisCacheManager cacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(getRedisManager());
//设置过期时间,单位是秒,20s
redisCacheManager.setExpire(20);
return redisCacheManager;
}
/**
* 自定义session持久化
* @return
*/
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(getRedisManager());
//设置sessionid生成器
redisSessionDAO.setSessionIdGenerator(new CustomSessionIdGenerator());
return redisSessionDAO;
}
/**
* 管理shiro一些bean的生命周期 即bean初始化 与销毁
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* api controller 层面
* 加入注解的使用,不加入这个AOP,则shiro的注解不生效(shiro的注解 例如 @RequiresGuest,@RequiresRoles,@RequiresPermissions等)
*
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
/**
* 用来扫描上下文寻找所有的Advistor(通知器),
* 将符合条件的Advisor应用到切入点的Bean中,需要在LifecycleBeanPostProcessor创建后才可以创建
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
}
shiro整合redis做缓存时,需要加上相关依赖
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
或者也可以使用ehcache做本地缓存,则加入ehcache相关依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
因为现在基本上都是web开发,所以依赖要加上:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
在项目resources目录下添加一个shiro配置xml文件(ehcache-shiro.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!--
name:缓存名称。 maxElementsInMemory:缓存最大个数。 eternal:对象是否永久有效,一但设置了,timeout将不起作用。
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。 diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
maxElementsOnDisk:硬盘最大缓存个数。 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk
store persists between restarts of the Virtual Machine. The default value
is false. diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 memoryStoreEvictionPolicy:
当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除
-->
<ehcache xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache/ehcache.xsd"
updateCheck="false" name="shiroCache">
<diskStore path="java.io.tmpdir/jeecms/shiro" />
<defaultCache maxElementsInMemory="1000" eternal="false"
timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"
diskSpoolBufferSizeMB="30" maxElementsOnDisk="1000"
diskPersistent="false" diskExpiryThreadIntervalSeconds="120" />
</ehcache>
在shiro配置类中,配置缓存bean方法时,替换为:
@Bean(name = "shiroEhcacheManager")
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return em;
}
更详细的用法可以参考官网中与spring框架整合的案例:Apache Shiro | Simple. Java. Security.
至于shiro配置类中配置的密码加解密规则,配置了密码解密规则,算法,两次md5,那么在散列用户密码的时候,用法:
String hashName = "md5";
String pwd = "123";
SimpleHash simpleHash = new SimpleHash(hashName, pwd, null, 2);
// 原始密码散列之后的值:d022646351048ac0ba397d12dfafa304
String result = simpleHash.toString();
在用户表(user表)中插入散列之后的密码
还有一个自定义的filter
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.Set;
/**
* 自定义filter
*/
public class CustomRolesOrAuthorizationFilter extends AuthorizationFilter {
@Override
@SuppressWarnings({"unchecked"})
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
//获取当前访问路径所需要的角色集合
String[] rolesArray = (String[]) mappedValue;
//没有角色限制,可以直接访问
if (rolesArray == null || rolesArray.length == 0) {
//no roles specified, so nothing to check - allow access.
return true;
}
Set<String> roles = CollectionUtils.asSet(rolesArray);
//当前subject是roles 中的任意一个,则有权限访问
for(String role : roles){
if(subject.hasRole(role)){
return true;
}
}
return false;
}
}
也可以自定义生成sessionID
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import java.io.Serializable;
import java.util.UUID;
/**
* 自定义sesionid生成
*/
public class CustomSessionIdGenerator implements SessionIdGenerator {
@Override
public Serializable generateId(Session session) {
return UUID.randomUUID().toString().replace("-","");
}
}
更多推荐
shiro安全框架
发布评论