介紹:css
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、受權、密碼學和會話管理。使用Shiro的易於理解的API,您能夠快速、輕鬆地得到任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。(摘自百度百科)java
本文使用springboot+mybatisplus+shiro實現數據庫動態的管理用戶、角色、權限管理,在本文的最後我會提供源碼的下載地址,想看到效果的小夥伴能夠直接下載運行就ok了mysql
由於shiro的功能比較多,本章只介紹以下幾個功能git
1.當用戶沒有登錄時只能訪問登錄界面github
2.當用戶登錄成功後,只能訪問該用戶下僅有的權限web
3.一個用戶不能兩我的同時在線redis
1、數據庫設計spring
本文的數據庫表爲5個分別是: 用戶表、角色表、權限表、用戶角色中間表、角色權限中間表,表的結構和數據項目中會提供(sql和redis工具下方的下載地址中都會有)sql
2、引入依賴數據庫
<?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>com.chaoqi</groupId> <artifactId>springboot_mybatisplus</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot_mybatisplus</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> <relativePath/> </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-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- reids --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--添加jsp依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <!-- SpringBoot - MyBatis 逆向工程 --> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> <!-- MyBatis 通用 Mapper --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>1.1.4</version> </dependency> <!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency> <!-- shiro+redis緩存插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>2.4.2.1-RELEASE</version> </dependency> <!-- fastjson阿里巴巴jSON處理器 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.13</version> </dependency> <!--<dependency>--> <!--<groupId>org.springframework.boot</groupId>--> <!--<artifactId>spring-boot-starter-security</artifactId>--> <!--</dependency>--> <!--工具類--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <configuration> <configurationFile>src/main/resources/generatorConfig.xml</configurationFile> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <executions> <execution> <id>Generate MyBatis Artifacts</id> <goals> <goal>generate</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>3.5.0</version> </dependency> </dependencies> </plugin> </plugins> </build> </project>
3、編輯application.yml
server: port: 8080 spring: mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp datasource: url: jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8&useUnicode=true&useSSL=false username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver redis: host: localhost port: 6379 jedis: pool: max-idle: 8 min-idle: 0 max-active: 8 max-wait: -1 timeout: 0 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.chaoqi.springboot_mybatisplus.domain
4、建立ShiroConfig配置
package com.chaoqi.springboot_shiro_redis.config; import com.chaoqi.springboot_shiro_redis.secutity.KickoutSessionControlFilter; import com.chaoqi.springboot_shiro_redis.secutity.MyShiroRealm; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; 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.session.mgt.DefaultWebSessionManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 沒有登錄的用戶只能訪問登錄頁面 shiroFilterFactoryBean.setLoginUrl("/auth/login"); // 登陸成功後要跳轉的連接 shiroFilterFactoryBean.setSuccessUrl("/auth/index"); // 未受權界面; ----這個配置了沒卵用,具體緣由想深刻了解的能夠自行百度 //shiroFilterFactoryBean.setUnauthorizedUrl("/auth/403"); //自定義攔截器 Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>(); //限制同一賬號同時在線的個數。 filtersMap.put("kickout", kickoutSessionControlFilter()); shiroFilterFactoryBean.setFilters(filtersMap); // 權限控制map. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/auth/login", "anon"); filterChainDefinitionMap.put("/auth/logout", "logout"); filterChainDefinitionMap.put("/auth/kickout", "anon"); filterChainDefinitionMap.put("/**", "authc,kickout"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 設置realm. securityManager.setRealm(myShiroRealm()); // 自定義緩存實現 使用redis securityManager.setCacheManager(cacheManager()); // 自定義session管理 使用redis securityManager.setSessionManager(sessionManager()); return securityManager; } /** * 身份認證realm; (這個須要本身寫,帳號密碼校驗;權限等) * * @return */ @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); return myShiroRealm; } /** * cacheManager 緩存 redis實現 * 使用的是shiro-redis開源插件 * * @return */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * 配置shiro redisManager * 使用的是shiro-redis開源插件 * * @return */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost("localhost"); redisManager.setPort(6379); redisManager.setExpire(1800);// 配置緩存過時時間 redisManager.setTimeout(0); // redisManager.setPassword(password); return redisManager; } /** * Session Manager * 使用的是shiro-redis開源插件 */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; } /** * RedisSessionDAO shiro sessionDao層的實現 經過redis * 使用的是shiro-redis開源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } /** * 限制同一帳號登陸同時登陸人數控制 * * @return */ @Bean public KickoutSessionControlFilter kickoutSessionControlFilter() { KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter(); kickoutSessionControlFilter.setCacheManager(cacheManager()); kickoutSessionControlFilter.setSessionManager(sessionManager()); kickoutSessionControlFilter.setKickoutAfter(false); kickoutSessionControlFilter.setMaxSession(1); kickoutSessionControlFilter.setKickoutUrl("/auth/kickout"); return kickoutSessionControlFilter; } /*** * 受權所用配置 * * @return */ @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } /*** * 使受權註解起做用不如不想配置能夠在pom文件中加入 * <dependency> *<groupId>org.springframework.boot</groupId> *<artifactId>spring-boot-starter-aop</artifactId> *</dependency> * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Shiro生命週期處理器 * */ @Bean public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } }
5、自定義Realm
package com.chaoqi.springboot_shiro_redis.secutity; import com.chaoqi.springboot_shiro_redis.service.SysRoleService; import com.chaoqi.springboot_shiro_redis.service.UserService; import com.chaoqi.springboot_shiro_redis.dao.domain.SysUser; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.util.*; public class MyShiroRealm extends AuthorizingRealm { private static org.slf4j.Logger logger = LoggerFactory.getLogger(MyShiroRealm.class); //若是項目中用到了事物,@Autowired註解會使事物失效,能夠本身用get方法獲取值 @Autowired private SysRoleService roleService; @Autowired private UserService userService; /** * 認證信息.(身份驗證) : Authentication 是用來驗證用戶身份 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { logger.info("---------------- 執行 Shiro 憑證認證 ----------------------"); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String name = token.getUsername(); String password = String.valueOf(token.getPassword()); SysUser user = new SysUser(); user.setUserName(name); user.setPassWord(password); // 從數據庫獲取對應用戶名密碼的用戶 SysUser userList = userService.getUser(user); if (userList != null) { // 用戶爲禁用狀態 if (userList.getUserEnable() != 1) { throw new DisabledAccountException(); } logger.info("---------------- Shiro 憑證認證成功 ----------------------"); SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userList, //用戶 userList.getPassWord(), //密碼 getName() //realm name ); return authenticationInfo; } throw new UnknownAccountException(); } /** * 受權 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { logger.info("---------------- 執行 Shiro 權限獲取 ---------------------"); Object principal = principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); if (principal instanceof SysUser) { SysUser userLogin = (SysUser) principal; Set<String> roles = roleService.findRoleNameByUserId(userLogin.getId()); authorizationInfo.addRoles(roles); Set<String> permissions = userService.findPermissionsByUserId(userLogin.getId()); authorizationInfo.addStringPermissions(permissions); } logger.info("---- 獲取到如下權限 ----"); logger.info(authorizationInfo.getStringPermissions().toString()); logger.info("---------------- Shiro 權限獲取成功 ----------------------"); return authorizationInfo; } }
6、限制併發人數登錄
package com.chaoqi.springboot_shiro_redis.secutity; import com.alibaba.fastjson.JSON; import com.chaoqi.springboot_shiro_redis.dao.domain.SysUser; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.DefaultSessionKey; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.util.WebUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; public class KickoutSessionControlFilter extends AccessControlFilter { private String kickoutUrl; //踢出後到的地址 private boolean kickoutAfter = false; //踢出以前登陸的/以後登陸的用戶 默認踢出以前登陸的用戶 private int maxSession = 1; //同一個賬號最大會話數 默認1 private SessionManager sessionManager; private Cache<String, Deque<Serializable>> cache; public void setKickoutUrl(String kickoutUrl) { this.kickoutUrl = kickoutUrl; } public void setKickoutAfter(boolean kickoutAfter) { this.kickoutAfter = kickoutAfter; } public void setMaxSession(int maxSession) { this.maxSession = maxSession; } public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } //設置Cache的key的前綴 public void setCacheManager(CacheManager cacheManager) { this.cache = cacheManager.getCache("shiro_redis_cache"); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { Subject subject = getSubject(request, response); if(!subject.isAuthenticated() && !subject.isRemembered()) { //若是沒有登陸,直接進行以後的流程 return true; } Session session = subject.getSession(); SysUser user = (SysUser) subject.getPrincipal(); String username = user.getUserName(); Serializable sessionId = session.getId(); //讀取緩存 沒有就存入 Deque<Serializable> deque = cache.get(username); //若是此用戶沒有session隊列,也就是尚未登陸過,緩存中沒有 //就new一個空隊列,否則deque對象爲空,會報空指針 if(deque==null){ deque = new LinkedList<Serializable>(); } //若是隊列裏沒有此sessionId,且用戶沒有被踢出;放入隊列 if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) { //將sessionId存入隊列 deque.push(sessionId); //將用戶的sessionId隊列緩存 cache.put(username, deque); } //若是隊列裏的sessionId數超出最大會話數,開始踢人 while(deque.size() > maxSession) { Serializable kickoutSessionId = null; if(kickoutAfter) { //若是踢出後者 kickoutSessionId = deque.removeFirst(); //踢出後再更新下緩存隊列 cache.put(username, deque); } else { //不然踢出前者 kickoutSessionId = deque.removeLast(); //踢出後再更新下緩存隊列 cache.put(username, deque); } try { //獲取被踢出的sessionId的session對象 Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId)); if(kickoutSession != null) { //設置會話的kickout屬性表示踢出了 kickoutSession.setAttribute("kickout", true); } } catch (Exception e) {//ignore exception } } //若是被踢出了,直接退出,重定向到踢出後的地址 if (session.getAttribute("kickout") != null) { //會話被踢出了 try { //退出登陸 subject.logout(); } catch (Exception e) { //ignore } saveRequest(request); Map<String, String> resultMap = new HashMap<String, String>(); //判斷是否是Ajax請求 if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) { resultMap.put("user_status", "300"); resultMap.put("message", "您已經在其餘地方登陸,請從新登陸!"); //輸出json串 out(response, resultMap); }else{ //重定向 WebUtils.issueRedirect(request, response, kickoutUrl); } return false; } return true; } private void out(ServletResponse hresponse, Map<String, String> resultMap) throws IOException { try { hresponse.setCharacterEncoding("UTF-8"); PrintWriter out = hresponse.getWriter(); out.println(JSON.toJSONString(resultMap)); out.flush(); out.close(); } catch (Exception e) { System.err.println("KickoutSessionFilter.class 輸出JSON異常,能夠忽略。"); } } }
7、異常處理類,攔截未受權頁面(未受權頁面有三種實現方式,我這裏使用異常處理)
package com.chaoqi.springboot_shiro_redis.exception; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.UnauthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; /** * 全局異常處理類 */ @ControllerAdvice public class CtrlExceptionHandler { private static Logger logger = LoggerFactory.getLogger(CtrlExceptionHandler.class); //攔截未受權頁面 @ResponseStatus(value = HttpStatus.FORBIDDEN) @ExceptionHandler(UnauthorizedException.class) public String handleException(UnauthorizedException e) { logger.debug(e.getMessage()); return "403"; } @ResponseStatus(value = HttpStatus.FORBIDDEN) @ExceptionHandler(AuthorizationException.class) public String handleException2(AuthorizationException e) { logger.debug(e.getMessage()); return "403"; } }
8、最後附上logincontroller的代碼,調用login就能夠調到登錄頁面
package com.chaoqi.springboot_shiro_redis.web; import com.chaoqi.springboot_shiro_redis.dao.domain.SysUser; import com.chaoqi.springboot_shiro_redis.utils.RequestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.http.HttpServletRequest; @Controller @RequestMapping(value = "/auth") public class LoginController { @RequestMapping(value = "/login", method = RequestMethod.POST) public String submitLogin(String username, String password, HttpServletRequest request) { try { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); subject.login(token); SysUser user = (SysUser) subject.getPrincipal(); } catch (DisabledAccountException e) { request.setAttribute("msg", "帳戶已被禁用"); return "login"; } catch (AuthenticationException e) { request.setAttribute("msg", "用戶名或密碼錯誤"); return "login"; } // 執行到這裏說明用戶已登陸成功 return "redirect:/auth/index"; } @RequestMapping(value = "/login", method = RequestMethod.GET) public String loginPage() { return "login"; } @RequestMapping(value = "/index", method = RequestMethod.GET) public String loginSuccessMessage(HttpServletRequest request) { String username = "未登陸"; SysUser currentLoginUser = RequestUtils.currentLoginUser(); if (currentLoginUser != null && StringUtils.isNotEmpty(currentLoginUser.getUserName())) { username = currentLoginUser.getUserName(); } else { return "redirect:/auth/login"; } request.setAttribute("username", username); return "index"; } //被踢出後跳轉的頁面 @RequestMapping(value = "/kickout", method = RequestMethod.GET) public String kickOut() { return "kickout"; } }
至此shiro整合完成,源碼下載地址爲:https://github.com/caicahoqi/ChaoqiIsPrivateLibrary 若是在項目搭建中遇到問題能夠在評論區留言,博主看到第一時間會給予回覆,謝謝