在上一篇文章《 SpringBoot极简集成Shiro 》中,讲解了SpringBoot极简集成Shiro的过程,但因为是极简集成,所以有些地方不适合生产环境,可以进行优化,如:集群环境下的Session的分布式会话;每次用户授权时,都需要走数据库查询等问题。
所以,本篇文章将在上一篇文章的基础上,通过Redis来实现如下功能:
在之前的基础上,项目结构基本没有改变,就是增加一个 ShiroSessionManager.java
,用于获取SessionId
<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> 复制代码
增加了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 复制代码
/** * 自定义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; } } } 复制代码
该文件在之前的基础上改动了如下几个方面:
代码如下:
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; } } 复制代码
这个文件中认证和授权的部分并未改动,只增加了清楚缓存的方法
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); } } 复制代码
@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权限"; } } 复制代码
经过上面的改动,已经可以实现分布式会话、缓存身份信息和缓存授权信息这些功能了,下面进入测试环节。
启动两个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; } 复制代码
使用vip用户登录
查看Redis情况
redis此时只有两个缓存,一个是session的缓存,一个是身份认证信息的缓存,并且身份认证缓存的key中使用了账号信息作为标识符
访问一个需要VIP角色的接口,注意添加Header头
再看Redis中的缓存数量:
多了一个角色授权的缓存信息
使用Redis作为数据缓存后,系统只会在第一次身份认证和第一次角色授权时进行数据库查询,后面的操作都会走Redis缓存。