相關教程:html
1. springboot+shiro整合教程
java
2. springboot+shiro+redis(單機redis版)整合教程
node
3. springboot+shiro+redis(單機redis版)整合教程-續(添加動態角色權限控制) web
本教程整合環境: java8 maven redis(集羣)redis
開發工具: ideaspring
版本: springboot 1.5.15.RELEASE數據庫
注:apache
1.本教程數據操做是模擬數據庫操做,並無真正進行持久化,自行修改便可。api
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
返回:退出登陸成功