Apache Shiro是一个功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理。借助Shiro易于理解的API,您可以快速轻松地保护任何应用程序 - 从最小的移动应用程序到最大的Web和企业应用程序。网上找到大部分文章都是以前SpringMVC下的整合方式,很多人都不知道shiro提供了官方的starter可以方便地跟SpringBoot整合。
请看shiro官网关于springboot整合shiro的链接:Integrating Apache Shiro into Spring-Boot Applications
整合准备
这篇文档的介绍也相当简单。我们只需要按照文档说明,然后在spring容器中注入一个我们自定义的Realm
,shiro通过这个realm就可以知道如何获取用户信息来处理鉴权(Authentication)
,如何获取用户角色、权限信息来处理授权(Authorization)
。如果是web应用程序的话需要引入shiro-spring-boot-web-starter
,单独的应用程序的话则引入shiro-spring-boot-starter
。
依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-spring-boot-web-starterartifactId>
<version>1.4.0-RC2version>
dependency>
用户实体
首先创建一个用户的实体,用来做认证
package com.maoxs.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Data
public class User implements Serializable {
private Long uid;
private String uname;
private String nick;
private String pwd;
private String salt;
private Date created;
private Date updated;
private Set<String> roles = new HashSet<>();
private Set<String> perms = new HashSet<>();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
这里了为了方便,就不去数据库读取了,方便测试我们把,权限信息,角色信息,认证信息都静态模拟下。
Resources
package com.maoxs.service;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
@Service
public class ResourcesService {
public Set<String> getResourcesByUserId(Long uid) {
Set<String> perms = new HashSet<>();
perms.add(“docker:run”);
perms.add(“docker:ps”);
perms.add(“mvn:debug”);
perms.add(“mvn:test”);
perms.add(“mvn:install”);
perms.add(“npm:clean”);
perms.add(“npm:run”);
perms.add(“npm:test”);
return perms;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
Role
package com.maoxs.service;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
@Service
public class RoleService {
public Set<String> getRolesByUserId(Long uid) {
Set<String> roles = new HashSet<>();
roles.add("docker");
roles.add("maven");
roles.add("node");
return roles;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
User
package com.maoxs.service;
import com.maoxs.pojo.User;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class UserService {
public User findUserByName(String uname) {
User user = new User();
user.setUname(uname);
user.setNick(uname + "NICK");
user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");
user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");
user.setUid(new Random().nextLong());
user.setCreated(new Date());
return user;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
认证
Shiro 从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource , 即安全数据源。
Realm
package com.maoxs.realm;
import com.maoxs.cache.MySimpleByteSource;
import com.maoxs.pojo.User;
import com.maoxs.service.ResourcesService;
import com.maoxs.service.RoleService;
import com.maoxs.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Set;
-
这个类是参照JDBCRealm写的,主要是自定义了如何查询用户信息,如何查询用户的角色和权限,如何校验密码等逻辑
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private ResourcesService resourcesService;
{
HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
hashMatcher.setStoredCredentialsHexEncoded(false);
hashMatcher.setHashIterations(1024);
this.setCredentialsMatcher(hashMatcher);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
throw new AuthorizationException(“PrincipalCollection method argument cannot be null.”);
}
User user = (User) getAvailablePrincipal(principals);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
System.out.println(“获取角色信息:” + user.getRoles());
System.out.println(“获取权限信息:” + user.getPerms());
info.setRoles(user.getRoles());
info.setStringPermissions(user.getPerms());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
if (username null) {
throw new AccountException(“请输入用户名”);
}
User userDB = userService.findUserByName(username);
if (userDB null) {
throw new UnknownAccountException(“用户不存在”);
}
Set<String> roles = roleService.getRolesByUserId(userDB.getUid());
Set<String> perms = resourcesService.getResourcesByUserId(userDB.getUid());
userDB.getRoles().addAll(roles);
userDB.getPerms().addAll(perms);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());
if (userDB.getSalt() != null) {
info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt()));
}
return info;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
相关配置
然后呢在只需要吧这个Realm注册到Spring容器中就可以啦
@Bean
public CustomRealm customRealm() {
CustomRealm realm = new CustomRealm();
return realm;
}
为了保证实现了Shiro内部lifecycle函数的bean执行 也是shiro的生命周期,注入LifecycleBeanPostProcessor
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
紧接着配置安全管理器,SecurityManager是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
return securityManager;
}
除此之外Shiro是一堆一堆的过滤链,所以要对shiro 的过滤进行设置,
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("favicon.ico", "anon");
chainDefinition.addPathDefinition("/login", "anon");
chainDefinition.addPathDefinition("/**", "user");
return chainDefinition;
}
yml
这里要说明下由于我们引入的是shiro-spring-boot-web-starter
,官方对配置进行了一系列的简化,并加入了一些自动配置项,所以我们要在yml中加入
shiro:
web:
enabled: true
loginUrl: /login
除此之外呢还有这些属性
键 |
默认值 |
描述 |
shiro.enabled |
true |
启用Shiro的Spring模块 |
shiro.web.enabled |
true |
启用Shiro的Spring Web模块 |
shiro.annotations.enabled |
true |
为Shiro的注释启用Spring支持 |
shiro.sessionManager.deleteInvalidSessions |
true |
从会话存储中删除无效会话 |
shiro.sessionManager.sessionIdCookieEnabled |
true |
启用会话ID到cookie,用于会话跟踪 |
shiro.sessionManager.sessionIdUrlRewritingEnabled |
true |
启用会话URL重写支持 |
shiro.userNativeSessionManager |
false |
如果启用,Shiro将管理HTTP会话而不是容器 |
shiro.sessionManager.cookie.name |
JSESSIONID |
会话cookie名称 |
shiro.sessionManager.cookie.maxAge |
-1 |
会话cookie最大年龄 |
shiro.sessionManager.cookie.domain |
空值 |
会话cookie域 |
shiro.sessionManager.cookie.path |
空值 |
会话cookie路径 |
shiro.sessionManager.cookie.secure |
false |
会话cookie安全标志 |
shiro.rememberMeManager.cookie.name |
rememberMe |
RememberMe cookie名称 |
shiro.rememberMeManager.cookie.maxAge |
一年 |
RememberMe cookie最大年龄 |
shiro.rememberMeManager.cookie.domain |
空值 |
RememberMe cookie域名 |
shiro.rememberMeManager.cookie.path |
空值 |
RememberMe cookie路径 |
shiro.rememberMeManager.cookie.secure |
false |
RememberMe cookie安全标志 |
shiro.loginUrl |
/login.jsp |
未经身份验证的用户重定向到登录页面时使用的登录URL |
shiro.successUrl |
/ |
用户登录后的默认登录页面(如果在当前会话中找不到替代) |
shiro.unauthorizedUrl |
空值 |
页面将用户重定向到未授权的位置(403页) |
在Controller中添加登录方法
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public Result login(@RequestParam("username") String userName, @RequestParam("password") String Password) throws Exception {
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userName, Password);
token.setRememberMe(true);
try {
currentUser.login(token);
log.info("==========登录成功=======");
return new Result(true, "登录成功");
} catch (UnknownAccountException e) {
log.info("==========用户名不存在=======");
return new Result(false, "用户名不存在");
} catch (DisabledAccountException e) {
log.info("==========您的账户已经被冻结=======");
return new Result(false, "您的账户已经被冻结");
} catch (IncorrectCredentialsException e) {
log.info("==========密码错误=======");
return new Result(false, "密码错误");
} catch (ExcessiveAttemptsException e) {
log.info("==========您错误的次数太多了吧,封你半小时=======");
return new Result(false, "您错误的次数太多了吧,封你半小时");
} catch (RuntimeException e) {
log.info("==========运行异常=======");
return new Result(false, "运行异常");
}
}
@RequestMapping("/logout")
public String logOut() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return “index”;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
这样就实现了整合认证的流程,,如果token信息与数据库表总username和password数据一致,则该用户身份认证成功。
鉴权
只用注解控制鉴权授权
使用注解的优点是控制的粒度细,并且非常适合用来做基于资源的权限控制。
只用注解的话非常简单。我们只需要使用url配置配置一下所以请求路径都可以匿名访问:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
chain.addPathDefinition("/**", "anon");
// chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
return chain;
}
然后在控制器类上使用shiro提供的种注解来做控制:
注解 |
功能 |
@RequiresGuest |
只有游客可以访问 |
@RequiresAuthentication |
需要登录才能访问 |
@RequiresUser |
已登录的用户或“记住我”的用户能访问 |
@RequiresRoles |
已登录的用户需具有指定的角色才能访问 |
@RequiresPermissions |
已登录的用户需具有指定的权限才能访问 |
示例
@RestController
public class Test1Controller {
@GetMapping("/hello")
public String hello() {
return "hello spring boot";
}
@RequiresGuest
@GetMapping("/guest")
public String guest() {
return "@RequiresGuest";
}
@RequiresAuthentication
@GetMapping("/authn")
public String authn() {
return "@RequiresAuthentication";
}
@RequiresUser
@GetMapping("/user")
public String user() {
return "@RequiresUser";
}
@RequiresPermissions("mvn:install")
@GetMapping("/mvnInstall")
public String mvnInstall() {
return "mvn:install";
}
@RequiresPermissions("gradleBuild")
@GetMapping("/gradleBuild")
public String gradleBuild() {
return "gradleBuild";
}
@RequiresRoles("docker")
@GetMapping("/docker")
public String docker() {
return "docker programmer";
}
@RequiresRoles("python")
@GetMapping("/python")
public String python() {
return "python programmer";
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
注意 解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring aop的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射,需加入以下配置:
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
只用url配置控制鉴权授权
shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限:
配置缩写 |
对应的过滤器 |
功能 |
anon |
AnonymousFilter |
指定url可以匿名访问 |
authc |
FormAuthenticationFilter |
指定url需要form表单登录,默认会从请求中获取username 、password ,rememberMe 等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic |
BasicHttpAuthenticationFilter |
指定url需要basic登录 |
logout |
LogoutFilter |
登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation |
NoSessionCreationFilter |
禁止创建会话 |
perms |
PermissionsAuthorizationFilter |
需要指定权限才能访问 |
port |
PortFilter |
需要指定端口才能访问 |
rest |
HttpMethodPermissionFilter |
将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles |
RolesAuthorizationFilter |
需要指定角色才能访问 |
ssl |
SslFilter |
需要https请求才能访问 |
user |
UserFilter |
需要已登录或“记住我”的用户才能访问 |
在spring容器中使用ShiroFilterChainDefinition
来控制所有url的鉴权和授权。优点是配置粒度大,对多个Controller做鉴权授权的控制。下面是栗子
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
chain.addPathDefinition("/user/login", "anon");
chain.addPathDefinition("/page/401", "anon");
chain.addPathDefinition("/page/403", "anon");
chain.addPathDefinition("/my/hello", "anon");
chain.addPathDefinition("/my/changePwd", "authc");
chain.addPathDefinition("/my/user", "user");
chain.addPathDefinition("/my/mvnBuild", "authc,perms[mvn:install]");
chain.addPathDefinition("/my/npmClean", "authc,perms[npm:clean]");
chain.addPathDefinition("/my/docker", "authc,roles[docker]");
chain.addPathDefinition("/my/python", "authc,roles[python]");
chain.addPathDefinition("/logout", "anon,logout");
chain.addPathDefinition("/**", "authc");
return chain;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
二者结合,url配置控制鉴权,注解控制授权
就个人而言,我是非常喜欢注解方式的。但是两种配置方式灵活结合,才是适应不同应用场景的最佳实践。只用注解或只用url配置,会带来一些比较累的工作。用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制
。下面是示例:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
chain.addPathDefinition("/user/login", "anon");
chain.addPathDefinition("/page/401", "anon");
chain.addPathDefinition("/page/403", "anon");
chain.addPathDefinition("/hello", "anon");
chain.addPathDefinition("/guest", "anon");
chain.addPathDefinition("/**", "authc");
return chain;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
@RestController
public class Test5Controller {
@GetMapping("/hello")
public String hello() {
return "hello spring boot";
}
@RequiresGuest
@GetMapping("/guest")
public String guest() {
return "@RequiresGuest";
}
@RequiresAuthentication
@GetMapping("/authn")
public String authn() {
return "@RequiresAuthentication";
}
@RequiresUser
@GetMapping("/user")
public String user() {
return "@RequiresUser";
}
@RequiresPermissions("mvn:install")
@GetMapping("/mvnInstall")
public String mvnInstall() {
return "mvn:install";
}
@RequiresPermissions("gradleBuild")
@GetMapping("/gradleBuild")
public String gradleBuild() {
return "gradleBuild";
}
@RequiresRoles("python")
@GetMapping("/python")
public String python() {
return "python programmer";
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
记住我
记住我功能在各大网站是比较常见的,实现起来也是大同小异,主要就是利用cookie来实现,而shiro对记住我功能的实现也是比较简单的,只需要几步即可。
首先呢配置下Cookie的生成模版,配置下cookie的name,cookie的有效时间等等。
@Bean
public SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
然后呢配置rememberMeManager
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return cookieRememberMeManager;
}
rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中。
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm(redisCacheManager));
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
好了记住我功能就到这里了,不过要记住一点,如果使用了authc的过滤的url的是不能使用记住我功能的,切记,至于什么原因,很好理解。有一些操作你是不需要别人在记住我功能下完成的,这样很不安全,所以shiro规定记住我功能最多得user级别的,不能到authc级别。
启用缓存
Shiro提供了类似Spring的Cache抽象,即Shiro本身不实现Cache,但是对Cache进行了又抽象,方便更换不同的底层Cache实现。对应前端的一个页面访问请求会同时出现很多的权限查询操作,这对于权限信息变化不是很频繁的场景,每次前端页面访问都进行大量的权限数据库查询是非常不经济的。因此,非常有必要对权限数据使用缓存方案。
由于Spring和Shiro都各自维护了自己的Cache抽象,为防止Realm注入的service里缓存注解和事务注解失效,所以定义自己的CacheManager处理缓存。
整合Redis
CacheManager代码如下。
package com.maoxs.cache;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.util.Destroyable;
import org.springframework.data.redis.cache.RedisCacheManager;
import java.util.Collection;
import java.util.Set;
public class ShiroRedisCacheManager implements CacheManager, Destroyable {
private RedisCacheManager cacheManager;
public RedisCacheManager getCacheManager() {
return cacheManager;
}
public void setCacheManager(RedisCacheManager cacheManager) {
this.cacheManager = cacheManager;
}
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
if (name == null) {
return null;
}
return new ShiroRedisCache<K, V>(name, getCacheManager());
}
@Override
public void destroy() throws Exception {
cacheManager = null;
}
@Slf4j
class ShiroRedisCache<K, V> implements org.apache.shiro.cache.Cache<K, V> {
private RedisCacheManager cacheManager;
private org.springframework.cache.Cache cache;
public ShiroRedisCache(String name, RedisCacheManager cacheManager) {
if (name == null || cacheManager == null) {
throw new IllegalArgumentException("cacheManager or CacheName cannot be null.");
}
this.cacheManager = cacheManager;
this.cache = cacheManager.getCache(name);
}
@Override
public V get(K key) throws CacheException {
log.info("从缓存中获取key为{}的缓存信息", key);
if (key == null) {
return null;
}
org.springframework.cache.Cache.ValueWrapper valueWrapper = cache.get(key);
if (valueWrapper == null) {
return null;
}
return (V) valueWrapper.get();
}
@Override
public V put(K key, V value) throws CacheException {
log.info("创建新的缓存,信息为:{}={}", key, value);
cache.put(key, value);
return get(key);
}
@Override
public V remove(K key) throws CacheException {
log.info("干掉key为{}的缓存", key);
V v = get(key);
cache.evict(key);
return v;
}
@Override
public void clear() throws CacheException {
log.info("清空所有的缓存");
cache.clear();
}
@Override
public int size() {
return cacheManager.getCacheNames().size();
}
@Override
public Set<K> keys() {
return (Set<K>) cacheManager.getCacheNames();
}
@Override
public Collection<V> values() {
return (Collection<V>) cache.get(cacheManager.getCacheNames()).get();
}
@Override
public String toString() {
return "ShiroSpringCache [cache=" + cache + "]";
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
然后呢就是把这个CacheManager注入到securityManager中
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
CollectionSerializer<Serializable> collectionSerializer = CollectionSerializer.getInstance();
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(collectionSerializer));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
@Bean
public ShiroRedisCacheManager shiroRedisCacheManager(RedisCacheManager redisCacheManager) {
ShiroRedisCacheManager cacheManager = new ShiroRedisCacheManager();
cacheManager.setCacheManager(redisCacheManager);
return cacheManager;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
相对应的Realm和securityManager也要稍做更改
@Bean
public CustomRealm customRealm(RedisCacheManager redisCacheManager) {
CustomRealm realm = new CustomRealm();
realm.setCachingEnabled(true);
realm.setCacheManager(shiroRedisCacheManager(redisCacheManager));
realm.setAuthenticationCachingEnabled(true);
realm.setAuthorizationCachingEnabled(true);
realm.setAuthenticationCacheName("fulinauthen");
realm.setAuthenticationCacheName("fulinauthor");
return realm;
}
@Bean
public DefaultWebSecurityManager securityManager(RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm(redisCacheManager));
securityManager.setCacheManager(shiroRedisCacheManager(redisCacheManager));
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
这样的话每次认证的时候就会把权限信息放入redis中,就不用反复的去查询数据库了。
注意
Realm里注入的UserService等service,需要延迟注入,所以都要添加@Lazy注解(如果不加需要自己延迟注入),否则会导致该service里的@Cacheable缓存注解、@Transactional事务注解等失效。
整合的时候应该会有人遇到不能序列化的问题吧,原因是因为用了Shiro的SimpleAuthenticationInfo中的setCredentialsSalt注入的属性ByteSource没有实现序列化接口,此时呢只用把源码一贴,实现下序列化接口即可
package com.maoxs.cache;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;
import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;
-
解决ByteSource 序列化问题
*/
public class MySimpleByteSource implements ByteSource, Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public MySimpleByteSource() {
}
public MySimpleByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MySimpleByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MySimpleByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MySimpleByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MySimpleByteSource(File file) {
this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);
}
public MySimpleByteSource(InputStream stream) {
this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public byte[] getBytes() {
return this.bytes;
}
public boolean isEmpty() {
return this.bytes null || this.bytes.length 0;
}
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
public String toString() {
return this.toBase64();
}
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource) o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
然后在realm中改变使用
if (userDB.getSalt() != null) {
info.setCredentialsSalt(new MySimpleByteSource(userDB.getSalt()));
}
整合Ehcache
整合ehcache就更简单,套路都是一样的只不过2.x和3.x 需要注入不同的CacheManager即可。这里需要注入下3.x的Ehcache是实现了Jcache,不过整合起来都是一样的,详情可以去看我之前的整合Spring抽象缓存的帖子。
官方提供了shiro-ehcache的整合包,不过这个整合包是针对Ehcache2.x的。
Redis存储Session
关于共享session的问题大家都应该知道了,传统的部署项目,两个相同的项目部署到不同的服务器上,Nginx负载均衡后会导致用户在A上登陆了,经过负载均衡后,在B上要重新登录,因为A上有相关session信息,而B没有。这种情况也称为“有状态”服务。而“无状态”服务则是:在一个公共的地方存储session,每次访问都会统一到这个地方来拿。思路呢就是实现Shiro的Session接口,然后呢自己控制,这里我们实现AbstractSessionDAO。
package com.maoxs.cache;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ShiroRedisSessionDao extends AbstractSessionDAO {
private RedisTemplate redisTemplate;
public ShiroRedisSessionDao(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void update(Session session) throws UnknownSessionException {
log.info("更新seesion,id=[{}]", session.getId().toString());
try {
redisTemplate.opsForValue().set(session.getId().toString(), session, 30, TimeUnit.MINUTES);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
@Override
public void delete(Session session) {
log.info("删除seesion,id=[{}]", session.getId().toString());
try {
String key = session.getId().toString();
redisTemplate.delete(key);
} catch (Exception e) {
log.info(e.getMessage(), e);
}
}
@Override
public Collection<Session> getActiveSessions() {
log.info("获取存活的session");
return Collections.emptySet();
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
log.info("创建seesion,id=[{}]", session.getId().toString());
try {
redisTemplate.opsForValue().set(session.getId().toString(), session, 30, TimeUnit.MINUTES);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
log.info("获取seesion,id=[{}]", sessionId.toString());
Session readSession = null;
try {
readSession = (Session) redisTemplate.opsForValue().get(sessionId.toString());
} catch (Exception e) {
log.error(e.getMessage());
}
return readSession;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
最后吧你写好的SessionDao注入到shiro的securityManager中即可
@Bean(name = "sessionManager")
public DefaultWebSessionManager sessionManager(RedisTemplate redisTemplate) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
CollectionSerializer<Serializable> collectionSerializer = CollectionSerializer.getInstance();
redisTemplate.setDefaultSerializer(collectionSerializer);
redisTemplate.setValueSerializer(collectionSerializer);
ShiroRedisSessionDao redisSessionDao = new ShiroRedisSessionDao(redisTemplate);
sessionManager.setSessionDAO(redisSessionDao);
sessionManager.setDeleteInvalidSessions(true);
SimpleCookie cookie = new SimpleCookie();
cookie.setName("starrkCookie");
sessionManager.setSessionIdCookie(cookie);
sessionManager.setSessionIdCookieEnabled(true);
return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager(RedisTemplate redisTemplate, RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm(redisCacheManager));
securityManager.setCacheManager(shiroRedisCacheManager(redisCacheManager));
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setSessionManager(sessionManager(redisTemplate));
return securityManager;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
这样每次读取Session就会从Redis中取读取了,当然还有谢谢开源的插件解决方案,比如crazycake ,有机会在补充这个。
本博文是基于springboot2.x 如果有什么不对的请在下方留言。
相关连接:
个人博客地址 : www.fulinlin.com
csdn博客地址:https://blog.csdn.net/qq_32867467
集合源码地址 : https://gitee.com/Maoxs/springboot-test
注:如果不对联系本宝宝及时改正~~