一、概述
1. Shiro介绍
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
2. 基本功能
-
Authentication
:身份认证 / 登录,验证用户是不是拥有相应的身份; -
Authorization
:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限; -
Session Management
:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的; -
Cryptography
:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储; -
支持的特性:
-
Web Support
:Web 支持,可以非常容易的集成到 Web 环境; -
Caching
:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;扫描二维码关注公众号,回复: 13748455 查看本文章 -
Concurrency
:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去; -
Testing
:提供测试支持; -
Run As
:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问; -
Remember Me
:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
-
注意:Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。
3. 架构
-
Subject
:主体,可以看到主体可以是任何可以与应用交互的 “用户”; -
SecurityManager
:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。 -
Authenticator
:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了; -
Authrizer
:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能; -
Realm
:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm; -
SessionManager
:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器); -
SessionDAO
:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能; -
CacheManager
:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能 -
Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的。
对于我们而言,最简单的一个 Shiro 应用:
-
应用代码通过
Subject
来进行认证和授权,而Subject
又委托给SecurityManager
; -
我们需要给
Shiro
的SecurityManager
注入Realm
,从而让SecurityManager
能得到合法的用户及其权限进行判断。
二、认证(登录)
1. 引入依赖
maven 仓库地址:https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.6.0</version>
</dependency>
2. Shiro 中常见类
(1)常见类
Shiro 中常用的类或者接口:
名字 | 类型 | 说明 |
---|---|---|
SecurityManager | 接口 | 安全管理器,对主体进行认证授权,还额外提供登录,退出和创建用户的功能 |
DefaultWebSecurityManager | 对象 | 默认的 Web SecurityManager,会提供一些默认值 |
ShiroFilterFactoryBean | 对象 | Shiro 的过滤器对象,这个对象将配置 Shiro 相关的一个规则的拦截 |
Realm | 接口 | 数据源,从数据源存取认证和授权相关的数据 |
AuthenticatingRealm | 抽象类 | 只负责认证(登录),通过实现该类来指定认证方法 |
AuthorizingRealm | 抽象类 | 负责认证(登录)和授权,通过实现该类来指定认证和授权方法 |
AuthenticationToken | 接口 | 登录未验证的数据 |
AuthenticationInfo | 接口 | 身份验证/登录过程相关的帐户信息。 |
Subject | 接口 | 程序中用这个对象来操作 shiro |
(2)常见异常
异常类 | 说明 |
---|---|
AuthenticationException |
Shiro 中所有异常的父类 |
AccountException | 账号异常 可以我们自己抛出 |
UnknownAccountException | 账号不存在的异常 可以我们自己抛出 |
LockedAccountException | 账号异常锁定异常 可以我们自己抛出 |
IncorrectCredentialsException | 密码错误异常 这个异常会在Shiro进行密码验证是抛出 |
3. 通过Shiro认证账号
1. Realm 接口
Shiro
从 Realm
获取安全数据(如用户、角色、权限),就是说 SecurityManager
要验证用户身份,那么它需要从 Realm
获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm
得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm
看成 DataSource
,即安全数据源。
public interface Realm {
// 返回一个唯一的 Realm 名字
String getName();
// 多 Realm 中,判断此 Realm 是否支持此 Token
boolean supports(AuthenticationToken token);
// 依据未认证的 AuthenticationToken,返回认证后的 AuthenticationInfo
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}
2. 自定义 Realm 类
常用的两个已经实现了 Realm
接口的抽象类:
AuthenticatingRealm
:只负责认证(登录)AuthorizingRealm
:负责认证(登录)和授权,是AuthenticatingRealm
的子类
这里先使用 AuthenticatingRealm
,后面认证再使用 AuthorizingRealm
public class MyRealm extends AuthenticatingRealm {
/**
* Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
* @param authenticationToken 用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
* @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证,密码是由 Shiro 进行验证是否合法
* @throws AuthenticationException 如果认证失败Shiro就会抛出AuthenticationException
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//获取用户在浏览器中输入的账号
String username = token.getUsername();
//认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
String dbusername = username;
if (!"admin".equals(dbusername) && !"zhangsan".equals(dbusername)) {
//判断用户账号是否正确
throw new UnknownAccountException("账号错误");
}
if ("zhangsan".equals(username)) {
throw new LockedAccountException("账号被锁定");
}
//定义一个密码这个密码应该来自数据库中
//如果要从数据库中取,就定义一下 UserMapper,然后从数据库中获取即可
String dbpassword = "123456";
//认证密码是否正确
return new SimpleAuthenticationInfo(dbusername, dbpassword, getName());
}
}
3. ShiroConfig
定义 ShiroConfig
@Configuration
public class ShiroConfig {
//配置Shiro的安全管理器
@Bean
public SecurityManager securityManager(Realm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
securityManager.setRealm(myRealm);
return securityManager;
}
//配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
@Bean
public Realm myRealm() {
MyRealm realm = new MyRealm();
return realm;
}
//配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
//创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
shiroFilter.setSecurityManager(securityManager);
//用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
//作用是用于通知Shiro我们可以使用这里路径转向到登录页面,当Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
shiroFilter.setLoginUrl("/");
//登录成功后转向页面
shiroFilter.setSuccessUrl("/success");
//用于指定没有权限转向页面
shiroFilter.setUnauthorizedUrl("/noPermission");
//定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问什么样的请求不可以访问
/*
* anon:无需认证就可以访问
* authc:必须认证了才能访问
* user:必须用有了 记住我 功能才能用
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
* logout:登出后会释放当前用户的内存
*/
Map<String, String> map = new LinkedHashMap<String, String>();
// '/login' 表示某个请求的名字;'anon'表示权限
map.put("/login", "anon");
map.put("/logout", "logout");
// 注意: ** 表示任意子孙路径
// * 表示任意的一个路径
// ? 表示任意的一个字符
map.put("/admin/**", "authc");
map.put("/user/**", "authc");
/**
* 表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
* 如果没有指定 /** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
*/
map.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(map);
return shiroFilter;
}
}
4. UserController
@Controller
public class TestController {
@RequestMapping("/login")
public String login(String username, String password, Model model) {
//创建一个shiro的Subject对象,利用这个对象来完成用户的登录认证
//Subject可以理解为当前用户,shiro 提供了自己的 Session
Subject subject = SecurityUtils.getSubject();
//判断用户是否认证过(是否登录过),进入 if 说明需要认证
if (!subject.isAuthenticated()) {
//创建一个用户账号和密码的Token对象,并设置用户输入的账号和密码。这个对象将在Shiro中被获取
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
//调用login后Shiro就会自动执行我们自定义的Realm中的认证方法
subject.login(token);
} catch (UnknownAccountException e) {
//用户的账号错误异常,可以看上面的 MyRealm类,是我自己在后台抛出的
model.addAttribute("errorMessage", "账号不存在");
return "login";
} catch (LockedAccountException e) {
//账号被锁定异常,可以看上面的 MyRealm类,是我自己在后台抛出的
model.addAttribute("errorMessage", "账号被冻结");
return "login";
} catch (IncorrectCredentialsException e) {
//用户密码错误异常,是 shiro 在认证密码时抛出的
model.addAttribute("errorMessage", "密码错误");
return "login";
}
}
//登录成功,跳转到登录成功页面
return "success";
}
/** 登出有两种方式
* 1. 在 ShiroConfig 中直接配置登出即可:map.put("/logout", "logout");
* 2. 使用 Subject 的 logout() 来登出,如下:
*/
@RequestMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
//退出登录
subject.logout();
return "login";
}
}
5. 密码加密(了解)
public class MyRealm extends AuthenticatingRealm {
/**
* Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
* @param authenticationToken 用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
* @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证,密码是由 Shiro 进行验证是否合法
* @throws AuthenticationException 如果认证失败Shiro就会抛出AuthenticationException
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//获取用户在浏览器中输入的账号
String username = token.getUsername();
//认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
String dbusername = username;
if (!"admin".equals(dbusername) && !"zhangsan".equals(dbusername)) {
//判断用户账号是否正确
throw new UnknownAccountException("账号错误");
}
if ("zhangsan".equals(username)) {
throw new LockedAccountException("账号被锁定");
}
//定义一个密码这个密码应该来自数据库中
String dbpassword = "123456";
//参数1:为加密算法,这里选择MD5加密
//参数2:为被加密的数据的数据
//参数3:为加密时的盐值,用于改变加密后数据结果,通常这个盐值需要选择一个表中唯一的数据例如表中的账号
//参数4:为需要对数据使用指定的算法加密多少次
Object obj = new SimpleHash("MD5", dbpassword, "", 1);
//认证密码是否正确 使用加密后的密码登录
return new SimpleAuthenticationInfo(dbusername, obj.toString(), getName());
}
}
三、授权
1. 在 Realm 中配置权限
1. 自定义 Realm 类
自定义 Realm 类,继承 AuthorizingRealm
抽象类, AuthorizingRealm
类在 AuthenticatingRealm
类的基础上,多了一个授权的抽象方法。
public class MyRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 在这里进行认证,详见认证中的自定义 Realm 类(二.3.2)
}
@Override
//Shiro用户授权的回调方法
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//从Shiro中获取用户名
Object username = principalCollection.getPrimaryPrincipal();
//创建一个SimpleAuthorizationInfo类的对象,利用这个对象需要设置当前用户的权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//用户对应的角色信息集合(从数据库中取)
Set<String> roles = new HashSet<>();
if ("admin".equals(username)) {
//表明该用户有 admin 和 user 角色
roles.add("admin");
roles.add("user");
} else if ("zhangsan".equals(username)) {
//表明该用户只有 user 角色
roles.add("user");
}
//用户对应的权限信息(从数据库取)
Set<String> psermission = new HashSet<>();
if ("admin".equals(username)) {
//表明该用户具有 admin 角色的 add 权限
psermission.add("admin:add");
}
//设置角色和权限信息
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(psermission);
return simpleAuthorizationInfo;
}
}
2. 在 ShiroConfig 中配置路径的权限规则
@Configuration
public class ShiroConfig {
//配置Shiro的安全管理器
@Bean
public SecurityManager securityManager(Realm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
securityManager.setRealm(myRealm);
return securityManager;
}
//配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
@Bean
public Realm myRealm() {
MyRealm realm = new MyRealm();
return realm;
}
//配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
//创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
shiroFilter.setSecurityManager(securityManager);
//用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
//作用是用于通知Shiro我们可以使用这里路径转向到登录页面,当Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
shiroFilter.setLoginUrl("/");
//登录成功后转向页面
shiroFilter.setSuccessUrl("/success");
//用于指定没有权限转向页面
shiroFilter.setUnauthorizedUrl("/noPermission");
//定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问什么样的请求不可以访问
/*
* anon:无需认证就可以访问
* authc:必须认证了才能访问
* user:必须用有了 记住我 功能才能用
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
* logout:登出后会释放当前用户的内存
*/
Map<String, String> map = new LinkedHashMap<String, String>();
// '/login' 表示某个请求的名字;'anon'表示权限
map.put("/login", "anon");
map.put("/logout", "logout");
//roles[admin] 表示 以/admin/**开头的请求需要拥有admin角色才可以访问
//perms[admin:add] 表示 /admin/test 的请求需要拥有 admin:add权限才可访问
//注意:权限匹配是从上到下进行匹配的,因此 /admin/test 要写在 /admin/** 前面
map.put("/admin/test", "authc,perms[admin:add]");
map.put("/admin/**", "authc,roles[admin]");
map.put("/user/**", "authc,roles[user]");
map.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(map);
return shiroFilter;
}
}
2. 基于注解的权限控制
1. ShiroConfig 中开启注解支持
注意:启动注解的权限控制以后需要删除在 Shiro
配置类中的权限拦截的配置规则
@Configuration
public class ShiroConfig {
@Bean
public MyRealm myRealm() {
return new MyRealm();
}
@Bean
public SecurityManager securityManager(MyRealm myRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager(myRealm);
return manager;
}
//配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
//例如什么样的请求可以访问什么样的请求不可以访问等等
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
shiroFilterFactory.setSecurityManager(securityManager);
// 登录页面
shiroFilterFactory.setLoginUrl("/");
// 登录成功后访问的地址
shiroFilterFactory.setSuccessUrl("/success");
// 登录失败时访问的地址
shiroFilterFactory.setUnauthorizedUrl("/nopermission");
/**
* 配置权限拦截规则
*/
Map<String, String> map = new LinkedHashMap<>();
map.put("/login", "anon");
map.put("/logout", "logout");
//配置注解,需要将 ShrioConfig 中所有的角色、权限配置删掉
//map.put("/admin/add", "authc,perms[admin:add]");
//map.put("/admin/**", "authc,roles[admin]");
//map.put("/user/**", "authc,roles[user]");
shiroFilterFactory.setFilterChainDefinitionMap(map);
return shiroFilterFactory;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
/**
* 开启aop支持
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
2. 使用注解
-
@RequiresAuthentication
:表示当前Subject
已经通过login
进行了身份验证;即Subject.isAuthenticated()
返回true
。 -
@RequiresUser
:表示当前Subject
已经身份验证或者通过记住我登录的。 -
@RequiresGuest
:表示当前Subject
没有身份验证或通过记住我登录过,即是游客身份。 -
@RequiresRoles(String[] value, Logical logical)
value
:角色列表。例如:value={“admin”, “user”}
logical
:多个角色之间要满足的关系(Logical.AND
或Logical.OR
),默认是Logical.AND
-
@RequiresPermissions (String[] value, Logical logical)
value
:角色权限列表。例如:value={“user:a”, “user:b”}
logical
:多个角色之间要满足的关系(Logical.AND
或Logical.OR
),默认是Logical.AND
@Controller
public class TestController {
@RequestMapping("/nopermission")
public String noPermission() {
return "noPermission";
}
//RequiresRoles Shiro的注解 表示访问这功能必须要拥有 admin角色
//注意使用了注解以后需要配置Spring声明式异常捕获,否则将在浏览器中直接看到Shiro的错误信息而不是友好的信息提示
@RequiresRoles(value = {
"admin"})
@RequestMapping("/admin/test")
public @ResponseBody String adminTest() {
return "这个adminTest请求";
}
@RequiresPermissions(value = {
"admin:add"})
@RequestMapping("/admin/add")
public @ResponseBody String adminAdd() {
Subject subject = SecurityUtils.getSubject();
//验证当前用户是否拥有这个权限
//subject.checkPermission();
//验证当前用户是否拥有这个角色
subject.checkRole("admin:add");
return "这个adminAdd请求";
}
//配置一个Spring的异常监控,当工程抛出了value所指定的所以异常类型以后将直接进入到当前方法中
@ExceptionHandler(value = {
Exception.class})
public String myError(Throwable throwable) {
//获取异常的类型,应该根据不同的异常类型进入到不通的页面显示不同提示信息
System.out.println(throwable.getClass());
return "noPermission";
}
}
四、会话管理(Redis)
1. 整合 Redis
- 添加 pom 文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置 redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123
2. 重写 SessionDAO
public class MyRedisSessionDAO extends AbstractSessionDAO {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
public static final int DEFAULT_TIME_OUT = 30;
public static final String SESSION_PREFIX = "shiro_session";
@Override
protected Serializable doCreate(Session session) {
// 生成 SessionId
Serializable sessionId = this.generateSessionId(session);
// 必须要将生成的id设置到session实力当中
this.assignSessionId(session, sessionId);
// 将 session 保存到 redis 中
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId != null) {
Session session = (Session) redisTemplate.opsForValue().get(SESSION_PREFIX + sessionId);
return session;
}
return null;
}
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
@Override
public void delete(Session session) {
if (session != null) {
redisTemplate.delete(SESSION_PREFIX + session.getId());
}
}
// 返回所有的 session
@Override
public Collection<Session> getActiveSessions() {
Set<Object> keys = redisTemplate.keys(SESSION_PREFIX + "*");
Set<Session> set = keys.parallelStream()
.map(key -> (Session) redisTemplate.opsForValue().get(key))
.collect(Collectors.toSet());
return set;
}
// 将 session 保存到 redis 中
private void saveSession(Session session) {
if (session != null && session.getId() != null) {
redisTemplate.opsForValue().set(SESSION_PREFIX + session.getId(), session, 30, TimeUnit.MINUTES);
}
}
}
3. 将重写的 SessionDAO
配置到 SecurityManager
中
@Configuration
public class ShiroConfig {
@Bean
public MyRealm myRealm() {
return new MyRealm();
}
// 创建自己的 SessionDAO 实例
@Bean
public SessionDAO myRedisSessionDAO() {
return new MyRedisSessionDAO();
}
//创建 Session 管理对象,使用自定义的 SessionDao
@Bean
public SessionManager sessionManager(SessionDAO myRedisSessionDAO) {
DefaultWebSessionManager manager = new DefaultWebSessionManager();
manager.setSessionDAO(myRedisSessionDAO);
return manager;
}
@Bean
public SecurityManager securityManager(MyRealm myRealm,
SessionManager sessionManager) {
// 配置 Realm
DefaultWebSecurityManager manager = new DefaultWebSecurityManager(myRealm);
// 配置 SessioonManager
manager.setSessionManager(sessionManager);
return manager;
}
//配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
//例如什么样的请求可以访问什么样的请求不可以访问等等
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
shiroFilterFactory.setSecurityManager(securityManager);
// 登录页面
shiroFilterFactory.setLoginUrl("/");
// 登录成功后访问的地址
shiroFilterFactory.setSuccessUrl("/success");
// 登录失败时访问的地址
shiroFilterFactory.setUnauthorizedUrl("/nopermission");
/**
* 配置权限拦截规则
*/
Map<String, String> map = new LinkedHashMap<>();
map.put("/login", "anon");
map.put("/logout", "logout");
//配置注解,需要将 ShrioConfig 中所有的角色、权限配置删掉
map.put("/admin/add", "authc,perms[admin:add]");
map.put("/admin/**", "authc,roles[admin]");
map.put("/user/**", "authc,roles[user]");
shiroFilterFactory.setFilterChainDefinitionMap(map);
return shiroFilterFactory;
}
}
4. 查看 Redis
五、缓存(Redis)
1. 创建 CacheManager
在 CacheManager
中需要提供一个 Cache
对象。Shiro
提供了类似于 Spring
的 Cache
抽象,即 Shiro
本身不实现 Cache
,但是对 Cache
进行了又抽象,方便更换不同的底层 Cache
实现。
public class MyRedisCacheManager extends AbstractCacheManager {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected Cache createCache(String s) throws CacheException {
return new RedisCache();
}
// 创建 RedisCache 对象实现 Cache 接口
class RedisCache<K, V> implements Cache<K, V> {
public static final String CACHE_KEY = "redis_cache";
@Override
public V get(K k) throws CacheException {
return (V) redisTemplate.boundHashOps(CACHE_KEY).get(k);
}
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.boundHashOps(CACHE_KEY).put(k, v);
return v;
}
@Override
public V remove(K k) throws CacheException {
V v = get(k);
redisTemplate.boundHashOps(CACHE_KEY).delete(k);
return v;
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(CACHE_KEY);
}
@Override
public int size() {
return (int) (long) redisTemplate.boundHashOps(CACHE_KEY).size();
}
@Override
public Set<K> keys() {
return redisTemplate.boundHashOps(CACHE_KEY).keys();
}
@Override
public Collection<V> values() {
return redisTemplate.boundHashOps(CACHE_KEY).values();
}
}
}
2. 配置 ShiroConfig
@Configuration
public class ShiroConfig {
// 创建自定义的 CacheManager 对象
@Bean
public CacheManager cacheManager() {
return new MyRedisCacheManager();
}
// 配置 SecurityManager 的 CacheManager 为自定义的 MyRedisCacheManager
@Bean
public SecurityManager securityManager(MyRealm myRealm,
CacheManager cacheManager) {
// 配置 Realm
DefaultWebSecurityManager manager = new DefaultWebSecurityManager(myRealm);
manager.setCacheManager(cacheManager);
return manager;
}
@Bean
public MyRealm myRealm() {
return new MyRealm();
}
3. 查看 Redis
查看 Redis ,大致可以看出 Shiro 将权限信息缓存了起来