前言
Apache Shiro 是 Java 的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。
功能介绍
资源-角色-权限
登录认证,密码加密(Authentication, Authorization, Cryptography)
用户角色和权限放入缓存(Caching)
会话管理(Session Management)
功能实现
实现说明
基于Spring开发Shiro的话,我们只需要实现ShiroFilterFactoryBean即可。
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setLoginUrl("/login");// 未登录时候跳转URL
filterFactoryBean.setSuccessUrl("/index");// 成功后欢迎页面
filterFactoryBean.setUnauthorizedUrl("/unAuthorized");// 未认证页面
filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
上面我们看到:
SecurityManager是登录认证,缓存管理和会话管理等的具体实现;filterChainDefinitionMap是资源对应的各种Filter的实现;
资源-角色-权限
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// /user/下面的需要ROLE_USER角色
filterChainDefinitionMap.put("/user/**", "roles[ROLE_USER]");
// /admin/下面的所有需要ROLE_ADMIN的角色才能访问
filterChainDefinitionMap.put("/admin/**", "roles[ROLE_ADMIN]");
//登录注册不需要认证
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/register", "anon");
// 其他资源地址全部需要用户认证才能访问
filterChainDefinitionMap.put("/**", "authc");
上面是设置当用户请求某个api的时候,使用对应的filter进行拦截处理,从而判断是否有对应的权限;默认的filter有以下几种:
/**
* Enum representing all of the default Shiro Filter instances available to web applications. Each filter instance is
* typically accessible in configuration the {@link #name() name} of the enum constant.
*
* @since 1.0
*/
public enum DefaultFilter {
anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);
登录认证,密码加密
因为用户信息和对应的角色权限信息,都是由应用方提供,所以Shiro抽象了一个接口,由应用方去实现这个接口(AuthorizingRealm),有两个方法需要应用方自己去实现:
//用户登录认证
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
//用户权限认证
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
以下是自己抽象了一个类:
@Slf4j
public abstract class AbstractAuthorizingRealm extends AuthorizingRealm {
public AbstractAuthorizingRealm() {
HashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher(EhCacheManagerFactory.getCacheManager());
credentialsMatcher.setHashAlgorithmName(ShiroConstant.hashAlgorithmName);
credentialsMatcher.setHashIterations(ShiroConstant.hashIterations);//加密次数
credentialsMatcher.setStoredCredentialsHexEncoded(true);
setCredentialsMatcher(credentialsMatcher);
}
/**
* 获取当前用户的角色和权限
*/
public abstract RoleAndPermissions getRoleAndPermissionsFromUsername(String username);
/**
* 获取认证信息
* @param username 用户名
*/
public abstract UserAuthInfo getAuthInfoFromUsername(String username);
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("##################执行Shiro权限认证(默认)##################");
// 获取用户名
String loginName = (String) principals.fromRealm(getName()).iterator().next();
// 判断用户名是否存在
if (loginName == null || loginName.length() == 0) {
return null;
}
// 创建一个授权对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//角色和权限设置
RoleAndPermissions rps = getRoleAndPermissionsFromUsername(loginName);
if (rps.getPermissions() != null) {
info.addStringPermissions(rps.getPermissions());
}
if (rps.getRoles() != null) {
info.addRoles(rps.getRoles());
}
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("##################执行Shiro登陆认证(默认)##################");
UsernamePasswordToken authenticationToken = (UsernamePasswordToken) token;
// 用户名
String username = authenticationToken.getUsername();
if (username != null && !"".equals(username)) {
UserAuthInfo userAuthInfo = getAuthInfoFromUsername(username);
if (userAuthInfo != null) {
Object principal = token.getPrincipal();
// shiro的用户认证对象
return new SimpleAuthenticationInfo(principal, userAuthInfo.getPassword(), ByteSource.Util.bytes(userAuthInfo.getSalt()), getName());
}
}
return null;
}
}
业务方只需要继承AbstractAuthorizingRealm,并且实现获取用户信息和角色权限的方法即可。而且还对密码进行了加密处理:
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
// 这里使用的是Ehcache对密码重试次数进行缓存
private Cache<String, AtomicInteger> passwordRetryCache;
public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == null) {
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
// 当用户连续输入密码错误5次以上禁止用户登录一段时间
if (retryCount.incrementAndGet() > 5) {
throw new ExcessiveAttemptsException();
}
boolean match = super.doCredentialsMatch(token, info);
if (match) {
passwordRetryCache.remove(username);
}
return match;
}
}
用户角色和权限放入缓存
用户登录成功后,没访问一个资源的时候不可能都去查一次角色和权限信息,可以将这些信息放入缓存中。
//缓存可以有多种实现,可以是EhCacheManager或RedisCacheManager
securityManager.setCacheManager(cm);
会话管理
如果使用DefaultWebSecurityManager, 默认会有一个SessionManager
public DefaultWebSecurityManager() {
super();
((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
this.sessionMode = HTTP_SESSION_MODE;
setSubjectFactory(new DefaultWebSubjectFactory());
setRememberMeManager(new CookieRememberMeManager());
setSessionManager(new ServletContainerSessionManager());
}
默认是使用ServletContainerSessionManager和我们平时的Session管理是一致的,客户端通过JSESSIONID和后台的session进行匹配,如果后台服务关闭或挂掉,session信息就丢失了,我们可以自己实现SessionManager,自己去管理Session的创建,销毁等。
@Slf4j
public class ShiroRedisSessionDao extends AbstractSessionDAO {
private RedisCache<String, Object> cache;
public ShiroRedisSessionDao(RedisCacheManager redisCacheManager) {
this.cache = (RedisCache)redisCacheManager.getCache("session-cache-manager");
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
log.error("session id is null");
return null;
}
return (Session)cache.get(sessionId.toString());
}
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
log.error("session or session id is null");
return;
}
cache.remove(session.getId().toString());
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<>();
/** 这里最好别用keys,可能会阻塞
Set<String> keys = cache.keys();
if(keys != null && keys.size()>0){
for(String key : keys){
Session s = (Session)cache.get(key);
sessions.add(s);
}
}
**/
return sessions;
}
private void saveSession(Session session) throws UnknownSessionException{
if (session == null || session.getId() == null) {
log.error("session or session id is null");
return;
}
// //设置过期时间
// long expireTime = 1800000l;
// session.setTimeout(expireTime);
cache.put(session.getId().toString(), session);
}
}
上面是继承了AbstractSessionDAO, 使用Redis对Session进行缓存,这样即使后台服务重启了,Session依然可以匹配。
封装
把上面的各部分实现封装到一起就构成了shiro权限管理的核心,业务方只需要实现简单的接口即可。
public abstract class AbstractShiroConfig {
/**
* 此方法由业务方重写
* @return 具体的Realm
*/
public abstract AbstractAuthorizingRealm getRealm();
/**
* 动态获取角色-资源信息
* @return filterChainDefinitions
*/
public abstract Map<String, String> loadFilterChainDefinitions() throws Exception;
/**
* 创建ShiroFilterFactoryBean
* 默认使用Ehcache缓存
* @return
*/
public ShiroFilterFactoryBean createShiroFilterBean() {
return createShiroFilterBean(defaultWebSecurityManager(CacheType.EHCACHE), defaultFilterChainMap());
}
public ShiroFilterFactoryBean createShiroFilterBean(DefaultWebSecurityManager securityManager, Map<String, String> filterChainDefinitionMap) {
return ShiroFilterFactory.create(securityManager, filterChainDefinitionMap, this);
}
public ShiroFilterFactoryBean createShiroFilterBean(Map<String, String> filterChainDefinitionMap) {
return ShiroFilterFactory.create(defaultWebSecurityManager(CacheType.EHCACHE), filterChainDefinitionMap, this);
}
// public ShiroFilterFactoryBean createShiroFilterBean(CacheType cacheType) {
// return createShiroFilterBean(defaultWebSecurityManager(cacheType), defaultFilterChainMap());
// }
//
// public ShiroFilterFactoryBean createShiroFilterBean(DefaultWebSecurityManager securityManager) {
// return ShiroFilterFactory.create(securityManager, defaultFilterChainMap());
// }
//
//
// public ShiroFilterFactoryBean createShiroFilterBean(CacheType cacheType, Map<String, String> filterChainDefinitionMap) {
// return ShiroFilterFactory.create(defaultWebSecurityManager(cacheType), filterChainDefinitionMap);
// }
//
// public ShiroFilterFactoryBean createShiroFilterBean(CacheManager cacheManager) {
// return ShiroFilterFactory.create(defaultWebSecurityManager(cacheManager), defaultFilterChainMap());
// }
//
// public ShiroFilterFactoryBean createShiroFilterBean(CacheManager cacheManager, Map<String, String> filterChainDefinitionMap) {
// return ShiroFilterFactory.create(defaultWebSecurityManager(cacheManager), filterChainDefinitionMap);
// }
/***
* 默认的安全管理配置
*/
public DefaultWebSecurityManager defaultWebSecurityManager(CacheType cacheType) {
return defaultWebSecurityManager(cacheType, null);
}
public DefaultWebSecurityManager defaultWebSecurityManager(CacheManager cacheManager) {
return defaultWebSecurityManager(null, cacheManager);
}
public DefaultWebSecurityManager defaultWebSecurityManager(CacheType cacheType, CacheManager cacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm
securityManager.setRealm(getRealm());
// 配置securityManager
SecurityUtils.setSecurityManager(securityManager);
// 根据情况选择缓存器
CacheManager cm = cacheManager == null ? (cacheType == null ? defaultShiroCacheManager() : getCacheManager(cacheType)) : cacheManager;
securityManager.setCacheManager(cm);
//设置session manager
securityManager.setSessionManager(new ShiroSessionManager());
return securityManager;
}
/**
* shiro缓存:ehcache缓存 (用户认证信息和权限信息等)
*/
public CacheManager defaultShiroCacheManager() {
return EhCacheManagerFactory.getCacheManager();
}
/**
* redis缓存:redis缓存 (用户认证信息和权限信息等)
*/
public CacheManager defaultShiroRedisCacheManager() {
return new RedisCacheManager(new RedisManager(), "auth");
}
private Map<String, String> defaultFilterChainMap() {
// 配置拦截地址和拦截器, 使用LinkedHashMap,因为拦截有先后顺序
// authc:所有url都必须认证通过才可以访问;
// anon:所有url都都可以匿名访问
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 以下配置同样可以通过注解@RequiresPermissions("user:edit")来配置访问权限和角色注解@RequiresRoles(value={"ROLE_USER"})方式定义
// filterChainDefinitionMap.put("/user/**", "roles[ROLE_USER]");// /user/下面的需要ROLE_USER角色或者query权限才能访问
// filterChainDefinitionMap.put("/admin/**", "roles[ROLE_ADMIN]");// /admin/下面的所有需要ROLE_ADMIN的角色才能访问
//登录注册不需要认证
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/register", "anon");
// 其他资源地址全部需要用户认证才能访问
filterChainDefinitionMap.put("/**", "authc");
return filterChainDefinitionMap;
}
/**
* 根据缓存类型选择对应的缓存管理器
* @param cacheType 缓存类型
* @return
*/
private CacheManager getCacheManager(CacheType cacheType) {
CacheManager cacheManager = defaultShiroCacheManager();
if (cacheType == CacheType.REDIS) {
cacheManager = defaultShiroRedisCacheManager();
}
return cacheManager;
}
}
业务方只需要继承AbstractShiroConfig即可
@Configuration
public class ShiroConfig extends AbstractShiroConfig {
@Bean
public ShiroFilterFactoryBean filterFactoryBean() throws Exception {
return createShiroFilterBean();
}
@Override
public AbstractAuthorizingRealm getRealm() {
return new AbstractAuthorizingRealm() {
@Override
public RoleAndPermissions getRoleAndPermissionsFromUsername(String s) {
User user = null;
try {
user = userService.findByUsername(s);
if (user != null) {
List<String> roles = new ArrayList<>();
List<String> permissions = new ArrayList<>();
if (user.getRoleIds()!=null) {
user.getRoleIds().forEach(roleId -> {
try {
Role role = roleService.findByRoleId(roleId);
if (role != null) {
roles.add(role.getRoleName());
if (role.getResIds() != null) {
role.getResIds().forEach(resId -> {
try {
permissions.add(resMapper.findById(resId).getUrl());
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
return new RoleAndPermissions(roles, permissions);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public UserAuthInfo getAuthInfoFromUsername(String s) {
try {
User user = userService.findByUsername(s);
if (user != null) {
return new UserAuthInfo(user.getLoginname(), user.getPassword(), user.getSalt());
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
};
}
}
createShiroFilterBean的时候可以自己传securityManager和filterChainDefinitionMap,服务启动后即可实现权限的管理了。
动态权限
上面自己对shiro进行了封装,业务方只需要实现几个简单的方法即可享受到Shiro权限的管理,但是我们可以看到上面的权限资源等是初始化的时候加载了一次,后面如果更新了就不起作用了,那怎么实现动态的权限呢,看代码:
public void updatePermission() throws Exception {
Map<String, String> filterChainMap = null;
try {
filterChainMap = shiroConfig.loadFilterChainDefinitions();
} catch (Exception e) {
log.error("loadFilterChainDefinitions error:", e);
}
if (filterChainMap != null && filterChainMap.size() > 0) {
synchronized (filterFactoryBean) {
AbstractShiroFilter shiroFilter = (AbstractShiroFilter) filterFactoryBean.getObject();
PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter
.getFilterChainResolver();
DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver
.getFilterChainManager();
// 清空老的权限控制
manager.getFilterChains().clear();
filterFactoryBean.getFilterChainDefinitionMap().clear();
for (Map.Entry<String, Filter> filterEntry : manager.getFilters().entrySet()) {
if (("roles".equals(filterEntry.getKey()) || "perms".equals(filterEntry.getKey())) && PathMatchingFilter.class.isInstance(filterEntry.getValue())) {
PathMatchingFilter filter = PathMatchingFilter.class.cast(filterEntry.getValue());
Field f = ReflectionUtils.findField(PathMatchingFilter.class, "appliedPaths");
f.setAccessible(true);
Map<String, Object> appliedPaths = (Map<String, Object>) ReflectionUtils.getField(f, filter);
appliedPaths.clear();
}
}
//创建新的权限
filterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
Map<String, String> chains = filterFactoryBean
.getFilterChainDefinitionMap();
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue().trim()
.replace(" ", "");
manager.createChain(url, chainDefinition);
}
}
}
}