springboot+shiro+redis(集群redis版)整合教程
相关教程:
2. springboot+shiro+redis(单机redis版)整合教程
3. springboot+shiro+redis(单机redis版)整合教程-续(添加动态角色权限控制)
本教程整合环境: java8 maven redis(集群)
开发工具: idea
版本: springboot 1.5.15.RELEASE
注:
1.本教程数据操作是模拟数据库操作,并没有真正进行持久化,自行修改即可。
2.角色权限验证未实现,只实现基本的登录验证,自行扩展即可。
项目结构:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>webapp</groupId> <artifactId>springboot-shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot-shiro</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.15.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <!-- ↓↓↓↓↓↓↓↓↓ springboot2.x需要排除jedis、lettuce ↓↓↓↓↓↓↓↓↓ --> <!--<exclusions>--> <!--<exclusion>--> <!--<groupId>redis.clients</groupId>--> <!--<artifactId>jedis</artifactId>--> <!--</exclusion>--> <!--<exclusion>--> <!--<groupId>io.lettuce</groupId>--> <!--<artifactId>lettuce-core</artifactId>--> <!--</exclusion>--> <!--</exclusions>--> <!-- ↑↑↑↑↑↑↑↑ springboot2.x需要排除jedis、lettuce ↑↑↑↑↑↑↑↑ --> </dependency> <!-- ↓↓↓↓↓↓↓↓↓ springboot2.x需要重新引入jedis ↓↓↓↓↓↓↓↓↓ --> <!--<dependency>--> <!--<groupId>redis.clients</groupId>--> <!--<artifactId>jedis</artifactId>--> <!--</dependency>--> <!-- ↑↑↑↑↑↑↑↑ springboot2.x需要重新引入jedis ↑↑↑↑↑↑↑↑ --> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.yml:
server:
port: 1002
spring:
redis:
cache:
clusterNodes: 127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384
password: jiuxxxxxxx
commandTimeout: 5000
User.java:
package webapp.model; import lombok.Data; /** * Created by Administrator on 2018/9/5. */ @Data public class User { private Long id; private String userName; private String password; }
UserService.java:
package webapp.service; import webapp.model.User; /** * Created by Administrator on 2018/9/5. */ public interface UserService { User findOneByUserName(String userName); }
UserServiceImpl.java:
package webapp.service.impl; import org.springframework.stereotype.Service; import webapp.model.User; import webapp.service.UserService; /** * Created by Administrator on 2018/9/5. */ @Service public class UserServiceImpl implements UserService { @Override public User findOneByUserName(String userName) { User user = new User(); user.setId(1L); user.setUserName("007少侠"); user.setPassword("123456"); return user; } }
UserController.java:
package webapp.controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.*; import webapp.service.UserService; import javax.annotation.Resource; import java.io.Serializable; /** * Created by Administrator on 2018/9/5. */ @RestController @RequestMapping("/core/user") public class UserController { @Autowired private UserService userService; /** * 登录 * @param * @return */ @GetMapping("/login") public String login(String userName, String password) { System.out.println("登录" + userName); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(userName, password); subject.login(token); Session session = subject.getSession(); Serializable sessionId = session.getId(); System.out.println("登录成功 -> " + sessionId); return userName + "[" + sessionId + "]"; } @GetMapping("/logout") public String logout() { SecurityUtils.getSubject().logout(); return "退出登录成功"; } /** * 获取当前登录用户 * @return */ @GetMapping("/findUser") public String findUser() { Subject subject = SecurityUtils.getSubject(); PrincipalCollection collection = subject.getPrincipals(); if (null != collection && !collection.isEmpty()) { String userName = (String) collection.iterator().next(); System.out.println("获取当前登录用户" + userName); return userService.findOneByUserName(userName).toString(); } return "{\n" + " \"codeEnum\": \"OVERTIME\",\n" + " \"code\": 0,\n" + " \"data\": null,\n" + " \"msg\": \"未登陆/登陆超时\",\n" + " \"success\": false\n" + "}"; } }
集群版redis相关配置(JedisClusterConfig.java、RedisClusterCache.java)(此2个类也可以单独整合于springboot):
JedisClusterConfig.java(其中只引入了JedisPool的4个基本属性,其他属性使用了默认值,可以自行配置其他属性):
package webapp.conf; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; import java.util.HashSet; import java.util.Set; /** * Created by Administrator on 2018/9/7. */ @Configuration @ConditionalOnClass({ JedisCluster.class }) public class JedisClusterConfig { @Value("${spring.redis.cache.clusterNodes}") private String clusterNodes; @Value("${spring.redis.cache.password}") private String password; @Value("${spring.redis.cache.commandTimeout}") private Integer commandTimeout; @Bean public JedisCluster getJedisCluster() { String[] serverArray = clusterNodes.split(","); Set<HostAndPort> nodes = new HashSet<>(); for (String ipPort : serverArray) { String[] ipPortPair = ipPort.split(":"); nodes.add(new HostAndPort(ipPortPair[0].trim(), Integer.valueOf(ipPortPair[1].trim()))); } return new JedisCluster(nodes, commandTimeout, commandTimeout, 2, password, new GenericObjectPoolConfig()); } }
RedisClusterCache.java:
package webapp.redis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import redis.clients.jedis.JedisCluster; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; /** * Created by Administrator on 2018/9/7. */ @Component public class RedisClusterCache { @Autowired private JedisCluster jedisCluster; /** * 添加缓存数据 * @param key * @param obj * @param <T> * @return * @throws Exception */ public <T> String putCache(String key, T obj) throws Exception { final byte[] bkey = key.getBytes(); final byte[] bvalue = serializeObj(obj); return jedisCluster.set(bkey,bvalue); } /** * 添加缓存数据,设定缓存失效时间 * @param key * @param obj * @param expireTime 秒 * @param <T> * @throws Exception */ public <T> String putCacheWithExpireTime(String key, T obj, final int expireTime) throws Exception { final byte[] bkey = key.getBytes(); final byte[] bvalue = serializeObj(obj); String result = jedisCluster.setex(bkey, expireTime,bvalue); return result; } /** * 根据key取缓存数据 * @param key * @param <T> * @return * @throws Exception */ public <T> T getCache(final String key) throws Exception { byte[] result = jedisCluster.get(key.getBytes()); return (T) deserializeObj(result); } /** * 根据key删除缓存数据 * @return * @throws Exception */ public void delCache(final String key) throws Exception { jedisCluster.del(key.getBytes()); } /** * 序列化 * @param object * @return */ private static byte[] serializeObj(Object object) { ObjectOutputStream oos = null; ByteArrayOutputStream baos = null; try { baos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(baos); oos.writeObject(object); byte[] bytes = baos.toByteArray(); return bytes; } catch (Exception e) { throw new RuntimeException("序列化失败!", e); } } /** * 反序列化 * @param bytes * @return */ private static Object deserializeObj(byte[] bytes) { if (bytes == null){ return null; } ByteArrayInputStream bais = null; try { bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais); return ois.readObject(); } catch (Exception e) { throw new RuntimeException("反序列化失败!", e); } } }
shiro的session管理器配置:
RedisSessionDAO.java:
package webapp.redis; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.Serializable; /** * Created by Administrator on 2018/9/6. */ @Component public class RedisSessionDAO extends EnterpriseCacheSessionDAO { @Autowired private RedisClusterCache redisClusterCache; /** * 创建session,保存到redis数据库 * * @param session * @return */ @Override protected Serializable doCreate(Session session) { Serializable sessionId = super.doCreate(session); try { redisClusterCache.putCache(sessionId.toString(), session); } catch (Exception e) { e.printStackTrace(); } return sessionId; } /** * 获取session * * @param sessionId * @return */ @Override protected Session doReadSession(Serializable sessionId) { // 先从缓存中获取session,如果没有再去数据库中获取 Session session = super.doReadSession(sessionId); if (session == null) { try { session = redisClusterCache.getCache(sessionId.toString()); } catch (Exception e) { e.printStackTrace(); } } return session; } /** * 更新session的最后一次访问时间 * * @param session */ @Override protected void doUpdate(Session session) { super.doUpdate(session); try { redisClusterCache.putCache(session.getId().toString(), session); } catch (Exception e) { e.printStackTrace(); } } /** * 删除session * * @param session */ @Override protected void doDelete(Session session) { super.doDelete(session); try { redisClusterCache.delCache(session.getId().toString()); } catch (Exception e) { e.printStackTrace(); } } }
shiro相关配置:
ShiroCoreController.java:
package webapp.shiro; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * Created by Administrator on 2018/9/5. */ @RestController public class ShiroCoreController { @GetMapping("/loginUnAuth") public String loginUnAuth() { return "{\n" + " \"codeEnum\": \"OVERTIME\",\n" + " \"code\": 0,\n" + " \"data\": null,\n" + " \"msg\": \"未登陆/登陆超时\",\n" + " \"success\": false\n" + "}"; } @GetMapping("/authorUnAuth") public String authorUnAuth() { return "{\n" + " \"codeEnum\": \"ERR_PERMISSIONS\",\n" + " \"code\": -2,\n" + " \"data\": null,\n" + " \"msg\": \"无此权限\",\n" + " \"success\": false\n" + "}"; } }
UserShiroRealm.java:
package webapp.shiro; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.DefaultSubjectContext; import org.springframework.beans.factory.annotation.Autowired; import webapp.model.User; import webapp.service.UserService; import java.util.Collection; /** * Created by Administrator on 2018/9/5. */ public class UserShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private SessionDAO sessionDAO; /** * 角色权限和对应权限添加 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } /** * 用户认证 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //加这一步的目的是在Post请求的时候会先进认证,然后在到请求 if (authenticationToken.getPrincipal() == null) { return null; } String userName = authenticationToken.getPrincipal().toString(); //只允许同一账户单个登录 Subject subject = SecurityUtils.getSubject(); Session nowSession = subject.getSession(); Collection<Session> sessions = sessionDAO.getActiveSessions(); if(sessions != null && sessions.size() > 0) { for (Session session : sessions) { if (!nowSession.getId().equals(session.getId()) && (session.getTimeout() == 0 || userName.equals(String.valueOf(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY))))) { sessionDAO.delete(session); } } } User user = userService.findOneByUserName(userName); if (user == null) { return null; } else { //这里验证authenticationToken和simpleAuthenticationInfo的信息 return new SimpleAuthenticationInfo(userName, user.getPassword(), getName()); } } }
ShiroConfig.java:
package webapp.conf; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import webapp.redis.RedisSessionDAO; import webapp.shiro.UserShiroRealm; import java.util.HashMap; /** * shiro配置类 * Created by Administrator on 2018/9/5. */ @Configuration public class ShiroConfig { //将自己的验证方式加入容器 @Bean public UserShiroRealm userShiroRealm() { return new UserShiroRealm(); } @Bean public SimpleCookie getSimpleCookie() { SimpleCookie simpleCookie = new SimpleCookie(); simpleCookie.setName("SHRIOSESSIONID"); return simpleCookie; } //配置shiro session 的一个管理器 @Bean(name = "sessionManager") public DefaultWebSessionManager getDefaultWebSessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO); sessionManager.setGlobalSessionTimeout(-1000); //session有效期 默认值1800000 30分钟 1800000毫秒 -1000表示永久 SimpleCookie simpleCookie = getSimpleCookie(); simpleCookie.setHttpOnly(true); //设置js不可读取此Cookie simpleCookie.setMaxAge(3 * 365 * 24 * 60 * 60); //3年 cookie有效期 sessionManager.setSessionIdCookie(simpleCookie); return sessionManager; } //配置核心安全事务管理器 @Bean(name="securityManager") public SecurityManager securityManager(@Qualifier("userShiroRealm") UserShiroRealm userShiroRealm, @Qualifier("sessionManager") DefaultWebSessionManager sessionManager) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(userShiroRealm); manager.setSessionManager(sessionManager); return manager; } //权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userShiroRealm()); return securityManager; } //Filter工厂,设置对应的过滤条件和跳转条件 @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager()); HashMap<String, String> map = new HashMap<>(); //登出 //map.put("/logout", "logout"); //认证 /###/@@@/** map.put("/api/**", "authc"); //登录认证不通过跳转 shiroFilterFactoryBean.setLoginUrl("/loginUnAuth"); //首页 //shiroFilterFactoryBean.setSuccessUrl("/index"); //权限认证不通过跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/authorUnAuth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } //加入注解的使用,不加入这个注解不生效 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } }
启动项目,访问相关接口校验是否成功:
登录接口:http://localhost:1002/core/user/login?userName=002&password=123456
返回:002[42c6d423-e48e-4164-b17d-0cbc0f9ca832]
获取用户:http://localhost:1002/core/user/findUser
返回:User(id=1, userName=007少侠, password=123456)
退出登录:http://localhost:1002/core/user/logout
返回:退出登录成功