1. 前言
在上一篇文章《SpringBoot极简集成Shiro》中,讲解了SpringBoot极简集成Shiro的过程,但因为是极简集成,所以有些地方不适合生产环境,可以进行优化,如:集群环境下的Session的分布式会话;每次用户授权时,都需要走数据库查询等问题。
所以,本篇文章将在上一篇文章的基础上,通过Redis来实现如下功能:
- 实现Session分布式会话功能
- 将用户的身份认证信息和授权信息缓存在Redis中,避免多次查询数据库
2. 项目结构
在之前的基础上,项目结构基本没有改变,就是增加一个ShiroSessionManager.java
,用于获取SessionId

3. 编码实现
3.1 pom导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--增加Redis相关依赖-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
3.2 application.yml
增加了redis的配置
server:
port: 8903
spring:
application:
name: lab-user
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/laboratory?charset=utf8
username: root
password: root
redis:
host: 127.0.0.1
port: 6379
password: 123456
mybatis:
type-aliases-package: cn.ntshare.laboratory.entity
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
3.3 ShiroSessionManager.java
/**
* 自定义Session获取规则,采用http请求头authToken携带sessionId的方式
* 登录成功后,会返回会话的sessionId,前端需要在请求头中加入该sessionId
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
public final static String HEADER_TOKEN_NAME = "token";
public ShiroSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(HEADER_TOKEN_NAME);
if (StringUtils.isEmpty(id)) {
// 按照默认规则从cookie中获取SessionId
return super.getSessionId(request, response);
} else {
// 从Header头中获取sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}
3.4 ShiroConfig.java
该文件在之前的基础上改动了如下几个方面:
- 开启了身份认证和授权信息的缓存
- 新增了redisCacheManager,sessionManager
- 新增了redisSessionDAO
代码如下:
import cn.ntshare.laboratory.realm.UserRealm;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private Integer redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// 开启缓存
userRealm.setCachingEnabled(true);
// 开启身份验证缓存,即缓存AuthenticationInfo信息
userRealm.setAuthenticationCachingEnabled(true);
// 设置身份缓存名称前缀
userRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存
userRealm.setAuthorizationCachingEnabled(true);
// 这是权限缓存名称前缀
userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
// 使用Redis作为缓存
securityManager.setCacheManager(redisCacheManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 路径过滤规则
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/");
Map<String, String> map = new LinkedHashMap<>();
// 有先后顺序
map.put("/login", "anon");
map.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 开启Shiro注解模式,可以在Controller中的方法上添加注解
* 如:@
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public SessionManager sessionManager() {
ShiroSessionManager sessionManager = new ShiroSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost);
redisManager.setPort(redisPort);
if (redisPassword != null && !("").equals(redisPassword)) {
redisManager.setPassword(redisPassword);
}
return redisManager;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
// 设置缓存名前缀
redisSessionDAO.setKeyPrefix("shiro:session:");
return redisSessionDAO;
}
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
// 选择属性字段作为缓存标识,这里选择account字段
redisCacheManager.setPrincipalIdFieldName("account");
// 设置信息缓存时间
redisCacheManager.setExpire(86400);
return redisCacheManager;
}
}
3.5 UserRealm.java
这个文件中认证和授权的部分并未改动,只增加了清楚缓存的方法
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
// 用户授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了一次授权");
User user = (User) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
List<Role> roleList = roleService.findRoleByUserId(user.getId());
Set<String> roleSet = new HashSet<>();
List<Integer> roleIds = new ArrayList<>();
for (Role role : roleList) {
roleSet.add(role.getRole());
roleIds.add(role.getId());
}
// 放入角色信息
authorizationInfo.setRoles(roleSet);
// 放入权限信息
List<String> permissionList = permissionService.findByRoleId(roleIds);
authorizationInfo.setStringPermissions(new HashSet<>(permissionList));
return authorizationInfo;
}
// 用户认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) throws AuthenticationException {
System.out.println("执行了身份认证");
UsernamePasswordToken token = (UsernamePasswordToken) authToken;
User user = userService.findByAccount(token.getUsername());
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
/**
* 清除当前授权缓存
* @param principalCollection
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principalCollection) {
super.clearCachedAuthorizationInfo(principalCollection);
}
/**
* 清除当前用户身份认证缓存
* @param principalCollection
*/
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principalCollection) {
super.clearCachedAuthenticationInfo(principalCollection);
}
@Override
public void clearCache(PrincipalCollection principalCollection) {
super.clearCache(principalCollection);
}
}
3.6 LoginController.java
@RestController
@RequestMapping("")
public class LoginController {
@PostMapping("/login")
public ServerResponseVO login(@RequestParam(value = "account") String account,
@RequestParam(value = "password") String password) {
Subject userSubject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(account, password);
try {
// 登录验证
userSubject.login(token);
// 封装返回信息
return ServerResponseVO.success(userSubject.getSession().getId());
} catch (UnknownAccountException e) {
return ServerResponseVO.error(ServerResponseEnum.ACCOUNT_NOT_EXIST);
} catch (DisabledAccountException e) {
return ServerResponseVO.error(ServerResponseEnum.ACCOUNT_IS_DISABLED);
} catch (IncorrectCredentialsException e) {
return ServerResponseVO.error(ServerResponseEnum.INCORRECT_CREDENTIALS);
} catch (Throwable e) {
e.printStackTrace();
return ServerResponseVO.error(ServerResponseEnum.ERROR);
}
}
@GetMapping("/login")
public ServerResponseVO login() {
return ServerResponseVO.error(ServerResponseEnum.NOT_LOGIN_IN);
}
@GetMapping("/auth")
public String auth() {
return "已成功登录";
}
@GetMapping("/role")
@RequiresRoles("vip")
public String role() {
System.out.println("测试负载均衡效果");
return "测试Vip角色";
}
@GetMapping("/permission")
@RequiresPermissions(value = {"add", "update"}, logical = Logical.AND)
public String permission() {
return "测试Add和Update权限";
}
}
经过上面的改动,已经可以实现分布式会话、缓存身份信息和缓存授权信息这些功能了,下面进入测试环节。
4. 测试效果
4.1 搭建集群
启动两个UserApplication,端口号分别为8903和8904
Nginx配置如下:
server {
server_name dev.ntshare.cn;
location / {
proxy_pass http://load.ntshare.cn;
}
}
upstream load.ntshare.cn {
server 127.0.0.1:8903 weight=1;
server 127.0.0.1:8904 weight=1;
}
4.2 Postman访问测试
使用vip用户登录

查看Redis情况

redis此时只有两个缓存,一个是session的缓存,一个是身份认证信息的缓存,并且身份认证缓存的key中使用了账号信息作为标识符
访问一个需要VIP角色的接口,注意添加Header头

再看Redis中的缓存数量:

多了一个角色授权的缓存信息
使用Redis作为数据缓存后,系统只会在第一次身份认证和第一次角色授权时进行数据库查询,后面的操作都会走Redis缓存。
4.3 其他用户和接口测试
略
5. 总结
- 重写
SessionManager
,自定义获取sessionId的规则,并且前端将sessionId添加到请求头中,实现session的分布式会话。 - 通过集成Redis的方式,Shiro框架将认证信息和授权信息放入Redis中,避免了同一用户的多次认证和授权时重复查询数据库的问题。