思维导图
什么是Shiro
shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。
spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。
shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。
使用shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。
用户通过subject登陆,形成一个UsernamePasswordToken,令牌,在域realm里完成认证、授权,成功后加入缓存。(realm可以写、也可以用默认的,也可以写很多个域)
Shiro架构
subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
securityManager:安全管理器,主体进行认证和授权都是通过securityManager进行。
authenticator:认证器,主体进行认证最终通过authenticator进行的。
authorizer:授权器,主体进行授权最终通过authorizer进行的。
sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。
在realm中存储授权和认证的逻辑。
认证过程
认证执行流程
1、通过ini配置文件创建securityManager
2、调用subject.login方法主体提交认证,提交的token
3、securityManager进行认证,securityManager最终由ModularRealmAuthenticator进行认证。
4、ModularRealmAuthenticator调用IniRealm(给realm传入token) 去ini配置文件中查询用户信息
5、IniRealm根据输入的token(UsernamePasswordToken)从 shiro.ini查询用户信息,根据账号查询用户信息(账号和密码)
如果查询到用户信息,就给ModularRealmAuthenticator返回用户信息(账号和密码)
如果查询不到,就给ModularRealmAuthenticator返回null
6、ModularRealmAuthenticator接收IniRealm返回Authentication认证信息
如果返回的认证信息是null,ModularRealmAuthenticator抛出异常(org.apache.shiro.authc.UnknownAccountException)
如果返回的认证信息不是null(说明inirealm找到了用户),对IniRealm返回用户密码 (在ini文件中存在)
和 token中的密码 进行对比,如果不一致抛出异常(org.apache.shiro.authc.IncorrectCredentialsException)
授权流程
1、对subject进行授权,调用方法isPermitted(”permission串”)
2、SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
3、ModularRealmAuthorizer执行realm(自定义的Realm)从数据库查询权限数据
调用realm的授权方法:doGetAuthorizationInfo
4、realm从数据库查询权限数据,返回ModularRealmAuthorizer
5、ModularRealmAuthorizer调用PermissionResolver进行权限串比对
6、如果比对后,isPermitted中”permission串”在realm查询到权限数据中,说明用户访问permission串有权限,否则 没有权限,抛出异常。
实现
代码地址:https://github.com/cayzlh/spring-boot-shiro-demo
自定义Realm
CustRealm:
public class CustomRealm extends AuthorizingRealm {
@Autowired @Lazy private AuthorizingService authorizingService;
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String username = (String) super.getAvailablePrincipal(principalCollection); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Set<String> roles = authorizingService.findRoleListByUsername(username); authorizationInfo.setRoles(roles); roles.forEach(role -> { Set<String> permissions = authorizingService.findPermissionsByRole(roles); authorizationInfo.addStringPermissions(permissions); }); return authorizationInfo; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); UserInfo user = authorizingService.selectByUserName(username); if (null == user) { throw new UnknownAccountException("doGetAuthenticationInfo() has an UnknownAccountException: "+username); } String passwordInToken = new String(token.getPassword()); String passwordInDb = user.getPassword(); if (!StringUtils.equals(passwordInDb, passwordInToken)) { throw new IncorrectCredentialsException("doGetAuthenticationInfo() has an IncorrectCredentialsException: "+username); } return new SimpleAuthenticationInfo(username, passwordInToken, ByteSource.Util.bytes(user.getSalt()), getName()); } }
|
自定义SessionManager
在我们项目中, 由于使用前后端分离的架构,所以要自定义Shiro的session管理:
public class CustomSessionManager extends DefaultWebSessionManager {
private static final String HEADER_TOKEN = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public CustomSessionManager() { super(); }
@Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String id = WebUtils.toHttp(request).getHeader(HEADER_TOKEN); if (!StringUtils.isEmpty(id)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return id; } else {
return super.getSessionId(request, response); } } }
|
通过监听请求Header里的token字段,如果有值,则作为shiro的sessionid。
配置Shiro
@Configuration public class ShiroConfig {
@Bean(name = "customRealm") public CustomRealm customRealm() { return new CustomRealm(); }
@Bean(name = "sessionManager") public SessionManager sessionManager() { return new CustomSessionManager(); }
@Bean(name = "securityManager") public DefaultWebSecurityManager defaultWebSecurityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm()); securityManager.setSessionManager(sessionManager()); return securityManager; }
@Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); }
@Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); return shiroFilterFactoryBean; }
@Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; }
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
}
|
其中:
@Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; }
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
|
在spring boot中, shiro使用@RequiresRoles,@RequiresPermissions注解无效时,需要添加这两个配置。
其他代码
AuthorizingService:
@Service public class AuthorizingService {
@Autowired private UserDao userDao;
@Autowired private RoleDao roleDao;
@Autowired private PermissionDao permissionDao;
public UserInfo selectByUserName(String username) { return userDao.selectByUsername(username); }
public Set<String> findRoleListByUsername(String username) { return roleDao.selectByUsername(username); }
public Set<String> findPermissionsByRole(Set<String> roles) { HashSet<String> permissions = Sets.newHashSet(); roles.forEach(role -> permissions.addAll(permissionDao.selectByRole(role))); return permissions; }
public String login(String username, String password) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject currentUser = SecurityUtils.getSubject(); currentUser.login(token); currentUser.getSession().setTimeout(60 * 60 * 1000); return currentUser.getSession().getId().toString(); }
public void logout() { Subject currentUser = SecurityUtils.getSubject(); currentUser.logout(); } }
|
PermissionDao:
@Repository public class PermissionDao {
public Set<String> selectByRole(String role) { switch (role) { case "admin": return Sets.newHashSet("Idea", "navicat", "notepad", "webstorm", "chrome"); case "java": return Sets.newHashSet("Idea"); case "mysql": return Sets.newHashSet("navicat"); case "html": return Sets.newHashSet("notepad"); case "javascript": return Sets.newHashSet("webstorm"); case "guest": return Sets.newHashSet("chrome"); default: return Sets.newHashSet(); } } }
|
RoleDao:
@Repository public class RoleDao {
public Set<String> selectByUsername(String username) { switch (username) { case "zhangsan": return Sets.newHashSet("admin"); case "lisi": return Sets.newHashSet("java", "mysql"); case "wangwu": return Sets.newHashSet("html", "javascript"); default: return Sets.newHashSet("guest"); } }
}
|
UserDao:
@Repository public class UserDao {
public UserInfo selectByUsername(String username) {
switch (username) { case "zhangsan": return UserInfo.builder() .userName("zhangsan").password("123456").salt("123456") .build(); case "lisi": return UserInfo.builder() .userName("lisi").password("123456").salt("123456") .build(); case "wangwu": return UserInfo.builder() .userName("wangwu").password("123456").salt("123456") .build(); default: return null; }
}
}
|
这里没有使用数据库, 直接模拟数据库操作。
集群
在实际项目运行中,为了达到高可用的目的,通常要把应用部署在多台服务器上,这个时候就要对session进行集群的管理
添加redis支持
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
|
配置文件
spring: application: name: shiro-demo redis: host: redistest.xxxx.com port: 6379 database: 10 password: test@2018 timeout: 180000 jedis: pool: max-active: 100 max-wait: 360000 min-idle: 0 max-idle: 100 my: shiro: session: expireTime: 1800 prefix: you-shiro-session
|
新增Java类:ShiroCacheManager
public class ShiroCacheManager implements CacheManager {
private RedisTemplate redisTemplate;
private int expireTime;
@Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { return new ShiroRedisCache<>(name, redisTemplate, expireTime); }
public ShiroCacheManager(RedisTemplate redisTemplate, int expireTime) { this.redisTemplate = redisTemplate; this.expireTime = expireTime; }
public class ShiroRedisCache<K, V> implements Cache<K, V> {
private String cacheKey;
private RedisTemplate redisTemplate;
private int expireTime;
private ShiroRedisCache(String cacheKey, RedisTemplate redisTemplate, int expireTime) { this.cacheKey = cacheKey; this.redisTemplate = redisTemplate; this.expireTime = expireTime; }
private Object hashKey(K key) { if (key instanceof PrincipalCollection) { PrincipalCollection principalCollection = (PrincipalCollection) key; return principalCollection.getPrimaryPrincipal().toString(); } return key; }
@Override public V get(K key) throws CacheException { BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey); Object realKey = hashKey(key); return boundHashOperations.get(realKey); }
@Override public V put(K key, V value) throws CacheException { BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey); Object realKey = hashKey(key); boundHashOperations.put((K) realKey, value); boundHashOperations.expire(expireTime, TimeUnit.SECONDS); return value; }
@Override public V remove(K key) throws CacheException { BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey); Object realKey = hashKey(key); V value = boundHashOperations.get(realKey); boundHashOperations.delete(realKey); return value; }
@Override public void clear() throws CacheException { redisTemplate.delete(cacheKey); }
@Override public int size() { BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey); return boundHashOperations.size().intValue(); }
@Override public Set<K> keys() { BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey); return boundHashOperations.keys(); }
@Override public Collection<V> values() { BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey); return boundHashOperations.values(); } } }
|
新增java类:RedisSessionDAO,继承EnterpriseCacheSessionDAO
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {
private int expireTime;
private String prefix;
private RedisTemplate redisTemplate;
public RedisSessionDAO(RedisTemplate redisTemplate, int expireTime, String prefix) { this.redisTemplate = redisTemplate; this.expireTime = expireTime; this.prefix = prefix; }
@Override protected Serializable doCreate(Session session) { log.info("doCreate({})", session.getId()); Serializable sessionId = super.doCreate(session); redisTemplate.opsForValue().set(prefix + ":" + sessionId.toString(), session); return sessionId; }
@Override protected Session doReadSession(Serializable sessionId) { log.info("doReadSession({})", sessionId); Session session = super.doReadSession(sessionId); if (session == null) { session = (Session) redisTemplate.opsForValue().get(prefix + ":" + sessionId.toString()); } return session; }
@Override protected void doUpdate(Session session) { super.doUpdate(session); String key = prefix + ":" + session.getId().toString(); if (!redisTemplate.hasKey(key)) { redisTemplate.opsForValue().set(key, session); } redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); }
@Override protected void doDelete(Session session) { log.info("doDelete({})", session.getId()); super.doDelete(session); redisTemplate.delete(prefix + ":" + session.getId().toString()); } }
|
新增Java类:RedisObjectSerializer implements RedisSerializer
public class RedisObjectSerializer implements RedisSerializer<Object> { private Converter<Object, byte[]> serializer = new SerializingConverter(); private Converter<byte[], Object> deserializer = new DeserializingConverter(); static final byte[] EMPTY_ARRAY = new byte[0];
@Override public Object deserialize(byte[] bytes) { if (isEmpty(bytes)) { return null; } try { return deserializer.convert(bytes); } catch (Exception ex) { throw new SerializationException("Cannot deserialize", ex); } }
@Override public byte[] serialize(Object object) { if (object == null) { return EMPTY_ARRAY; } try { return serializer.convert(object); } catch (Exception ex) { return EMPTY_ARRAY; } }
private boolean isEmpty(byte[] data) { return (data == null || data.length == 0); } }
|
修改ShiroConfig
@Configuration public class ShiroConfig implements InitializingBean {
@Value("${my.shiro.session.expireTime:1800}") private int expireTime;
@Value("${my.shiro.session.prefix:you-shiro-session}") private String prefix;
@Autowired private RedisTemplate redisTemplate;
@Override public void afterPropertiesSet() { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new RedisObjectSerializer()); redisTemplate.afterPropertiesSet(); }
@Bean public RedisTemplate redisTemplate() { return new RedisTemplate(); }
@Bean(name = "shiroCacheManager") public ShiroCacheManager shiroCacheManager() { return new ShiroCacheManager(redisTemplate, expireTime); }
@Bean(name = "redisSessionDAO") public RedisSessionDAO redisSessionDAO() { return new RedisSessionDAO(redisTemplate, expireTime, prefix); }
@Bean(name = "customRealm") public CustomRealm customRealm() { return new CustomRealm(); }
@Bean(name = "sessionManager") public SessionManager sessionManager() { CustomSessionManager sessionManager = new CustomSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; }
@Bean(name = "securityManager") public DefaultWebSecurityManager defaultWebSecurityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
securityManager.setCacheManager(shiroCacheManager());
securityManager.setSessionManager(sessionManager());
return securityManager; }
@Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); }
@Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); return shiroFilterFactoryBean; }
@Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; }
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
}
|
集群完成
^_^.
经过以上改造,shiro就可以在分布式应用中集群使用。
项目代码,在 with-redis 分支中。
测试
运行Demo,测试登录和请求其他接口:
–