一、前言
Apache Shiro 是 Java 的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。
二、介绍
2.1 功能特点
Shiro 包含 10 个内容,如下图:
1) Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
2) Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
3) Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的。
4) Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
5) Web Support:Web支持,可以非常容易的集成到 web 环境。
6) Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
7) Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去。
8) Testing:提供测试支持。
9) Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
10) Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
2.2 运行原理
Shiro 运行原理图1(应用程序角度)如下:
1) Subject:主体,代表了当前“用户”。这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等。所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager。我们可以把 Subject 认为是一个门面,SecurityManager 才是实际的执行者。
2) SecurityManager:安全管理器。即所有与安全有关的操作都会与 SecurityManager 交互,且它管理着所有 Subject。可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,我们可以把它看成 DispatcherServlet 前端控制器。
3) Realm:域。Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法,也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作。我们可以把 Realm 看成 DataSource,即安全数据源。
Shiro 运行原理图2(Shiro 内部架构角度)如下:
1) Subject:主体,可以看到主体可以是任何与应用交互的“用户”。
2) SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher。它是 Shiro 的核心,所有具体的交互都通过 SecurityManager 进行控制。它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
3) Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,我们可以自定义实现。其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了。
4) Authrizer:授权器,或者访问控制器。它用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能。
5) Realm:可以有1个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的。它可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等。
6) SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 需要有人去管理它的生命周期,这个组件就是 SessionManager。而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境。
7) SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD。我们可以自定义 SessionDAO 的实现,控制 session 存储的位置。如通过 JDBC 写到数据库或通过 jedis 写入 redis 中。另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能。
8) CacheManager:缓存管理器。它来管理如用户、角色、权限等的缓存的。因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。
9) Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。
2.3 过滤器
当 Shiro 被运用到 web 项目时,Shiro 会自动创建一些默认的过滤器对客户端请求进行过滤。以下是 Shiro 提供的过滤器:
过滤器简称 | 对应的 Java 类 |
---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
logout | org.apache.shiro.web.filter.authc.LogoutFilter |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter |
解释:
/admins/**=anon # 表示该 uri 可以匿名访问
/admins/**=auth # 表示该 uri 需要认证才能访问
/admins/**=authcBasic # 表示该 uri 需要 httpBasic 认证
/admins/**=perms[user:add:*] # 表示该 uri 需要认证用户拥有 user:add:* 权限才能访问
/admins/**=port[8081] # 表示该 uri 需要使用 8081 端口
/admins/**=rest[user] # 相当于 /admins/**=perms[user:method],其中,method 表示 get、post、delete 等
/admins/**=roles[admin] # 表示该 uri 需要认证用户拥有 admin 角色才能访问
/admins/**=ssl # 表示该 uri 需要使用 https 协议
/admins/**=user # 表示该 uri 需要认证或通过记住我认证才能访问
/logout=logout # 表示注销,可以当作固定配置
注意:
anon,authcBasic,auchc,user 是认证过滤器。
perms,roles,ssl,rest,port 是授权过滤器。
三、基础入门
3.1 添加依赖
<!-- Shiro核心框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Shiro使用Srping框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Shiro使用redis缓存框架 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
3.2 配置文件
@Configuration
public class ShiroConfig {
@Value("${shiro.redis.host}")
private String host;
@Value("${shiro.redis.password}")
private String password;
@Value("${shiro.redis.port}")
private int port;
@Value("${shiro.redis.timeout}")
private int timeout;
/**
* Session超时时间,单位为毫秒(默认30分钟)
*/
@Value("${shiro.session.expireTime}")
private int expireTime;
/**
* 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟
*/
@Value("${shiro.session.validationInterval}")
private int validationInterval;
/**
* 验证码开关
*/
@Value("${shiro.user.captchaEbabled}")
private boolean captchaEbabled;
/**
* 验证码类型
*/
@Value("${shiro.user.captchaType}")
private String captchaType;
/**
* 设置Cookie的域名
*/
@Value("${shiro.cookie.domain}")
private String domain;
/**
* 设置cookie的有效访问路径
*/
@Value("${shiro.cookie.path}")
private String path;
/**
* 设置HttpOnly属性
*/
@Value("${shiro.cookie.httpOnly}")
private boolean httpOnly;
/**
* 设置Cookie的过期时间,秒为单位
*/
@Value("${shiro.cookie.maxAge}")
private int maxAge;
/**
* 登录地址
*/
@Value("${shiro.user.loginUrl}")
private String loginUrl;
/**
* 权限认证失败地址
*/
@Value("${shiro.user.unauthorizedUrl}")
private String unauthorizedUrl;
/**
* Shiro过滤器配置
* Filter Chain定义说明
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl(loginUrl);
/*// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
// 权限认证失败,则跳转到指定页面
shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);*/
// 自定义拦截器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("captchaValidate", captchaValidateFilter());
// 注销成功,则跳转到指定页面
filters.put("logout", logoutFilter());
// 重写user认证
filters.put("myUser", myUserFilter());
shiroFilterFactoryBean.setFilters(filters);
// Shiro连接约束配置,即权限控制
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 对静态资源设置匿名访问
filterChainDefinitionMap.put("/swagger**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/", "anon");
// 退出logout地址,shiro去清除session
filterChainDefinitionMap.put("/logout", "logout");
// 不需要拦截的访问
filterChainDefinitionMap.put("/login", "anon,captchaValidate");
// 所有请求需要认证 user:配置记住我或认证通过可以访问 authc: 需要认证才能进行访问 anon:所有url都都可以匿名访问
// filterChainDefinitionMap.put("/**", "user");
filterChainDefinitionMap.put("/**", "myUser");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public MyRedisCacheManager redisCacheManager() {
MyRedisCacheManager redisCacheManager = new MyRedisCacheManager();
redisCacheManager.setRedisTemplate(myRedisTemplate());
redisCacheManager.setExpireTime(expireTime);
return redisCacheManager;
}
/**
* 安全管理器
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(myShiroRealm());
// 注入缓存管理器,使用redis
securityManager.setCacheManager(redisCacheManager());
// session管理器,使用redis
securityManager.setSessionManager(sessionManager());
// 记住我
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher();
// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 身份认证realm
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 缓存管理器,使用redis实现
*/
@Bean
public MyRedisCacheManager cacheManager() {
MyRedisCacheManager redisCacheManager = new MyRedisCacheManager();
redisCacheManager.setRedisTemplate(myRedisTemplate());
return redisCacheManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(expireTime * 60);
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
/**
* 自定义sessionManager
* @return
*/
@Bean
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setSessionDAO(redisSessionDAO());
mySessionManager.setCacheManager(redisCacheManager());
//设置的最大时间,正负都可以,为负数时表示永不超时。系统默认超时时间是180000毫秒(30分钟)(现在设置为一天)
mySessionManager.setGlobalSessionTimeout(1000*60*expireTime);
return mySessionManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public MyRedisSessionDao redisSessionDAO() {
MyRedisSessionDao redisSessionDAO = new MyRedisSessionDao();
redisSessionDAO.setRedisTemplate(myRedisTemplate());
redisSessionDAO.setExpireTime(expireTime);
return redisSessionDAO;
}
/**
* 退出过滤器
*/
public LogoutFilter logoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setLoginUrl(loginUrl);
return logoutFilter;
}
/**
* 自定义用户认证拦截器
* @return
*/
public MyUserFilter myUserFilter() {
return new MyUserFilter();
}
/**
* 自定义验证码过滤器
*/
@Bean
public CaptchaValidateFilter captchaValidateFilter() {
CaptchaValidateFilter captchaValidateFilter = new CaptchaValidateFilter();
captchaValidateFilter.setCaptchaEbabled(captchaEbabled);
captchaValidateFilter.setCaptchaType(captchaType);
return captchaValidateFilter;
}
/**
* cookie属性设置
*/
public SimpleCookie rememberMeCookie() {
// 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie cookie = new SimpleCookie("rememberMe");
cookie.setDomain(domain);
cookie.setPath(path);
cookie.setHttpOnly(httpOnly);
// 记住我cookie生效时间30天,单位秒
cookie.setMaxAge(maxAge * 24 * 60 * 60);
return cookie;
}
/**
* cookie管理对象,记住我功能
*/
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("fCq+/xW488hMTCD+cmJ3aQ=="));
return cookieRememberMeManager;
}
/**
* 开启Shiro代理
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
/**
* 开启shiro aop注解支持
* 使用代理方式,所以需要开启代码支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public JedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setHostName(host);
factory.setPort(port);
//设置连接超时时间
factory.setTimeout(timeout);
factory.setPassword(password);
return factory;
}
@Bean
public RedisTemplate<String, Object> myRedisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
//设置序列化工具,这样ReportBean不需要实现Serializable接口
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new RedisObjectSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
//设置序列化工具,这样ReportBean不需要实现Serializable接口
setSerializer(template);
template.afterPropertiesSet();
return template;
}
private void setSerializer(StringRedisTemplate template) {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
// 存储Map时value需要的序列化配置
template.setHashValueSerializer(jackson2JsonRedisSerializer);
}
}
3.3 自定义Realm
Shiro 自带的 IniRealm,从 ini 配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义 realm。
其中,最基础的是 Realm 接口,CachingRealm 负责缓存处理,AuthenticationRealm 负责认证,AuthorizingRealm负责授权,通常自定义的 realm 继承 AuthorizingRealm。
自定义 Realm:
/**
* shiro身份校验核心类
*/
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private IMenuService menuService;
@Autowired
private IRoleService roleService;
@Autowired
private LoginService loginService;
/**
* 身份认证:验证用户输入的账号和密码是否正确
* 只有用户登录的时候才会执行身份认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
String username = token.getUsername();
String password = String.valueOf(token.getPassword());
SysUser sysUser = loginService.authentication(username, password);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
sysUser,
sysUser.getPassword(),
ByteSource.Util.bytes(sysUser.getCredentialsSalt()),
getName());
return info;
}
/**
* 授权
* 每次URL请求时,都要经过这边授权,通过才放行。
* 注意:第二次请求会走缓存,所以有任何权限变更都需要去清理缓存权限,否则权限变更不生效
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
Long sysUserId = ShiroUtils.getSysUserId();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 角色加入AuthorizationInfo认证对象
authorizationInfo.setRoles(roleService.selectRoleKeys(sysUserId));
// 权限加入AuthorizationInfo认证对象
authorizationInfo.setStringPermissions(menuService.selectPermsByUserId(sysUserId));
return authorizationInfo;
}
/**
* 清理缓存权限
*/
public void clearCachedAuthorizationInfo() {
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
}
参考
https://www.cnblogs.com/moonlightL/p/8126910.html