思维导图

Shiro

什么是Shiro

shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。

spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。

shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。

使用shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。

用户通过subject登陆,形成一个UsernamePasswordToken,令牌,在域realm里完成认证、授权,成功后加入缓存。(realm可以写、也可以用默认的,也可以写很多个域)

Shiro

Shiro架构

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;

/**
* Shiro 的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
* 当访问到页面的时候,链接配置了相应的权限或者 Shiro 标签才会执行此方法否则不会执行,
* 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回 null 即可。
*
* @param principalCollection
* @return
*/
@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;
}

/**
* 在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。
* 因为在 Shiro 中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
* 通常情况下,在 Realm 中会直接从我们的数据源中获取 Shiro 需要的验证信息。
* 可以说,Realm 是专用于安全框架的 DAO. Shiro 的认证过程最终会交由 Realm 执行,
* 这时会调用 Realm 的getAuthenticationInfo(token)方法。
*
* 该方法主要执行以下操作:
*
* 1、检查提交的进行认证的令牌信息
* 2、根据令牌信息从数据源(通常为数据库)中获取用户信息
* 3、对用户信息进行匹配验证。
* 4、验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
* 5、验证失败则抛出AuthenticationException异常信息。
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@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,测试登录和请求其他接口:

登录

请求