簡介
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼學和會話管理。使用Shiro的易於理解的API,您可以快速、輕松地獲得任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。
demo地址在最下方給出。
本文主要實現shiro的以下幾個功能:
1.當用戶沒有登陸時只能訪問登錄接口,訪問其他接口會返回無效的授權碼
2.當用戶登陸成功后,只能訪問該用戶權限下的接口,其他接口會返回無權限訪問
3.一個用戶不能兩個人同時在線,后登錄的會自動踢出先登錄的用戶
本文使用框架如下:
核心框架:spring boot 2.0.0, spring
mvc框架:spring mvc
持久層框架:mybatis
數據庫連接池:alibaba druid
安全框架:apache shiro
緩存框架:redis
日志框架:logback
數據庫設計:
數據庫主要分為5個表,分別是:用戶表,角色表,權限表,角色權限表,用戶角色表
由於數據表結構我直接拷貝的以前的項目,所以上表中很多字段該文不會用到,各位請根據自己的實際情況修改。
<?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.alex</groupId> <artifactId>springboot</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot</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/> <!-- 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> <shiro.version>1.4.0</shiro.version> <shiro-redis.version>3.1.0</shiro-redis.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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 訪問靜態資源 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </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> <!-- 分頁插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.5</version> </dependency> <!-- alibaba的druid數據庫連接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.0</version> </dependency> <!--@Slf4j自動化日志對象-log--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.16</version> </dependency> <!-- shiro spring. --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <!-- shiro ehcache --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>${shiro.version}</version> </dependency> <!-- shiro+redis緩存插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>${shiro-redis.version}</version> </dependency> <!--工具類--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> <!-- fastjson阿里巴巴jSON處理器 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.13</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
編輯application.yml
server:
port: 8080
spring:
datasource:
name: test
url: jdbc:mysql://127.0.0.1:3306/springboot
username: admin
password: 123456
# 使用druid數據源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
#初始化大小,最小,最大
initialSize: 5
minIdle: 5
maxActive: 20
# 配置獲取連接等待超時的時間
maxWait: 60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
minEvictableIdleTimeMillis: 300000
# 校驗SQL,Oracle配置 spring.datasource.validationQuery=SELECT 1 FROM DUAL,如果不配validationQuery項,則下面三項配置無用
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打開PSCache,並且指定每個連接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize : 20
# 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
filters: stat, wall, logback
connectionProperties : druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
redis:
host: 127.0.0.1
port: 6379
password : 123456
timeout: 0
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
## 該配置節點為獨立的節點,有很多同學容易將這個配置放在spring的節點下,導致配置無法被識別
mybatis:
mapper-locations: classpath:mapping/*.xml #注意:一定要對應mapper映射xml文件的所在路徑
type-aliases-package: com.alex.springboot.model # 注意:對應實體類的路徑
#pagehelper分頁插件
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
創建ShiroConfig
package com.alex.springboot.system.config; import com.alex.springboot.system.shiro.CredentialsMatcher; import com.alex.springboot.system.shiro.SessionControlFilter; import com.alex.springboot.system.shiro.SessionManager; import com.alex.springboot.system.shiro.ShiroRealm; 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.servlet.SimpleCookie; 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.beans.factory.annotation.Value; 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 { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.port}") private int redisPort; @Value("${spring.redis.password}") private String redisPassword; @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 沒有登陸的用戶只能訪問登陸頁面,前后端分離中登錄界面跳轉應由前端路由控制,后台僅返回json數據 shiroFilterFactoryBean.setLoginUrl("/common/unauth"); // 登錄成功后要跳轉的鏈接 //shiroFilterFactoryBean.setSuccessUrl("/auth/index"); // 未授權界面; shiroFilterFactoryBean.setUnauthorizedUrl("common/unauth"); //自定義攔截器 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("/common/**", "anon"); // 靜態資源 filterChainDefinitionMap.put("/static/**", "anon"); // 登錄方法 filterChainDefinitionMap.put("/admin/login*", "anon"); // 表示可以匿名訪問 //此處需要添加一個kickout,上面添加的自定義攔截器才能生效 filterChainDefinitionMap.put("/admin/**", "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 ShiroRealm myShiroRealm() { ShiroRealm myShiroRealm = new ShiroRealm(); myShiroRealm.setCredentialsMatcher(credentialsMatcher()); return myShiroRealm; } @Bean public CredentialsMatcher credentialsMatcher() { return new CredentialsMatcher(); } /** * cacheManager 緩存 redis實現 * 使用的是shiro-redis開源插件 * * @return */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); redisCacheManager.setKeyPrefix("SPRINGBOOT_CACHE:"); //設置前綴 return redisCacheManager; } /** * RedisSessionDAO shiro sessionDao層的實現 通過redis * 使用的是shiro-redis開源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setKeyPrefix("SPRINGBOOT_SESSION:"); return redisSessionDAO; } /** * Session Manager * 使用的是shiro-redis開源插件 */ @Bean public SessionManager sessionManager() { SimpleCookie simpleCookie = new SimpleCookie("Token"); simpleCookie.setPath("/"); simpleCookie.setHttpOnly(false); SessionManager sessionManager = new SessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); sessionManager.setSessionIdCookieEnabled(false); sessionManager.setSessionIdUrlRewritingEnabled(false); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionIdCookie(simpleCookie); return sessionManager; } /** * 配置shiro redisManager * 使用的是shiro-redis開源插件 * * @return */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(redisHost); redisManager.setPort(redisPort); redisManager.setTimeout(1800); //設置過期時間 redisManager.setPassword(redisPassword); return redisManager; } /** * 限制同一賬號登錄同時登錄人數控制 * * @return */ @Bean public SessionControlFilter kickoutSessionControlFilter() { SessionControlFilter kickoutSessionControlFilter = new SessionControlFilter(); kickoutSessionControlFilter.setCache(cacheManager()); kickoutSessionControlFilter.setSessionManager(sessionManager()); kickoutSessionControlFilter.setKickoutAfter(false); kickoutSessionControlFilter.setMaxSession(1); kickoutSessionControlFilter.setKickoutUrl("/common/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生命周期處理器 * 此方法需要用static作為修飾詞,否則無法通過@Value()注解的方式獲取配置文件的值 * */ @Bean public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } }
自定義Realm
package com.alex.springboot.system.shiro; import com.alex.springboot.model.Menu; import com.alex.springboot.model.Role; import com.alex.springboot.model.User; import com.alex.springboot.service.MenuService; import com.alex.springboot.service.RoleService; import com.alex.springboot.service.UserService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; 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.springframework.beans.factory.annotation.Autowired; import java.util.List; @Slf4j public class ShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private MenuService menuService; /** * 認證信息.(身份驗證) : Authentication 是用來驗證用戶身份 * */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { log.info("---------------- 執行 Shiro 憑證認證 ----------------------"); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String name = token.getUsername(); // 從數據庫獲取對應用戶名密碼的用戶 User user = userService.getUserByName(name); if (user != null) { // 用戶為禁用狀態 if (!user.getLoginFlag().equals("1")) { throw new DisabledAccountException(); } log.info("---------------- Shiro 憑證認證成功 ----------------------"); SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用戶 user.getPassword(), //密碼 getName() //realm name ); return authenticationInfo; } throw new UnknownAccountException(); } /** * 授權 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("---------------- 執行 Shiro 權限獲取 ---------------------"); Object principal = principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); if (principal instanceof User) { User userLogin = (User) principal; if(userLogin != null){ List<Role> roleList = roleService.findByUserid(userLogin.getId()); if(CollectionUtils.isNotEmpty(roleList)){ for(Role role : roleList){ info.addRole(role.getEnname()); List<Menu> menuList = menuService.getAllMenuByRoleId(role.getId()); if(CollectionUtils.isNotEmpty(menuList)){ for (Menu menu : menuList){ if(StringUtils.isNoneBlank(menu.getPermission())){ info.addStringPermission(menu.getPermission()); } } } } } } } log.info("---------------- 獲取到以下權限 ----------------"); log.info(info.getStringPermissions().toString()); log.info("---------------- Shiro 權限獲取成功 ----------------------"); return info; } }
自定義密碼校驗器
package com.alex.springboot.system.shiro; import com.alex.springboot.utils.MD5Util; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; public class CredentialsMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken utoken = (UsernamePasswordToken) token; // 獲得用戶輸入的密碼:(可以采用加鹽(salt)的方式去檢驗) String inPassword = new String(utoken.getPassword()); // 獲得數據庫中的密碼 String dbPassword = (String) info.getCredentials(); // 進行密碼的比對 return this.equals(MD5Util.encrypt(inPassword), dbPassword); } }
自定義session容器,用於實現前后端分離,前端請求接口時將Token放在請求Header中,即可獲取到用戶的session信息(建議前端是將Token放在Header中,而不是放到body請求參數中,這樣可以統一做封裝處理,下面的代碼中是獲取Header中或者body中Token,建議直接獲取Header中Token即可)
package com.alex.springboot.system.shiro; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.SessionKey; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.session.mgt.WebSessionKey; import org.apache.shiro.web.util.WebUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable; public class SessionManager extends DefaultWebSessionManager { private static final String AUTHORIZATION = "Token"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public SessionManager() { } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { //獲取請求頭,或者請求參數中的Token String id = StringUtils.isEmpty(WebUtils.toHttp(request).getHeader(AUTHORIZATION)) ? request.getParameter(AUTHORIZATION) : WebUtils.toHttp(request).getHeader(AUTHORIZATION); // 如果請求頭中有 Token 則其值為sessionId if (StringUtils.isNotEmpty(id)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return id; } else { // 否則按默認規則從cookie取sessionId return super.getSessionId(request, response); } } /** * 獲取session 優化單次請求需要多次訪問redis的問題 * * @param sessionKey * @return * @throws UnknownSessionException */ @Override protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { Serializable sessionId = getSessionId(sessionKey); ServletRequest request = null; if (sessionKey instanceof WebSessionKey) { request = ((WebSessionKey) sessionKey).getServletRequest(); } if (request != null && null != sessionId) { Object sessionObj = request.getAttribute(sessionId.toString()); if (sessionObj != null) { return (Session) sessionObj; } } Session session = super.retrieveSession(sessionKey); if (request != null && null != sessionId) { request.setAttribute(sessionId.toString(), session); } return session; } }
自定義攔截器,用於限制用戶登錄人數
package com.alex.springboot.system.shiro; import com.alex.springboot.model.User; import com.alibaba.fastjson.JSON; 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 SessionControlFilter extends AccessControlFilter { private String kickoutUrl; //踢出后到的地址 private boolean kickoutAfter = false; //踢出之前登錄的/之后登錄的用戶 默認踢出之前登錄的用戶 private int maxSession = 1; //同一個帳號最大會話數 默認1 private SessionManager sessionManager; private Cache<String, Deque<Serializable>> 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(); User user = (User) subject.getPrincipal(); String username = user.getLoginName(); 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異常,可以忽略。"); } } public String getKickoutUrl() { return kickoutUrl; } public void setKickoutUrl(String kickoutUrl) { this.kickoutUrl = kickoutUrl; } public boolean isKickoutAfter() { return kickoutAfter; } public void setKickoutAfter(boolean kickoutAfter) { this.kickoutAfter = kickoutAfter; } public int getMaxSession() { return maxSession; } public void setMaxSession(int maxSession) { this.maxSession = maxSession; } public SessionManager getSessionManager() { return sessionManager; } public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } public Cache<String, Deque<Serializable>> getCache() { return cache; } public void setCache(CacheManager cacheManager) { this.cache = cacheManager.getCache("shiro_redis_cache"); } }
全局異常攔截
package com.alex.springboot.system.handler; import com.alex.springboot.system.enums.ResultStatusCode; import com.alex.springboot.system.vo.Result; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authz.UnauthorizedException; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import javax.validation.ConstraintViolationException; @Slf4j @ControllerAdvice @ResponseBody public class ExceptionAdvice { /** * 400 - Bad Request */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler({HttpMessageNotReadableException.class, MissingServletRequestParameterException.class, BindException.class, ServletRequestBindingException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class}) public Result handleHttpMessageNotReadableException(Exception e) { log.error("參數解析失敗", e); if (e instanceof BindException){ return new Result(ResultStatusCode.BAD_REQUEST.getCode(), ((BindException)e).getAllErrors().get(0).getDefaultMessage()); } return new Result(ResultStatusCode.BAD_REQUEST.getCode(), e.getMessage()); } /** * 405 - Method Not Allowed */ @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public Result handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { log.error("不支持當前請求方法", e); return new Result(ResultStatusCode.METHOD_NOT_ALLOWED, null); } /** * shiro權限異常處理 * @return */ @ExceptionHandler(UnauthorizedException.class) public Result unauthorizedException(UnauthorizedException e){ log.error(e.getMessage(), e); return new Result(ResultStatusCode.UNAUTHO_ERROR); } /** * 500 * @param e * @return */ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public Result handleException(Exception e) { e.printStackTrace(); log.error("服務運行異常", e); return new Result(ResultStatusCode.SYSTEM_ERR, null); } }
未授權和被踢出后跳轉方法
package com.alex.springboot.controller; import com.alex.springboot.system.enums.ResultStatusCode; import com.alex.springboot.system.vo.Result; import org.apache.shiro.SecurityUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/common") @RestController public class CommonController { /** * 未授權跳轉方法 * @return */ @RequestMapping("/unauth") public Result unauth(){ SecurityUtils.getSubject().logout(); return new Result(ResultStatusCode.UNAUTHO_ERROR); } /** * 被踢出后跳轉方法 * @return */ @RequestMapping("/kickout") public Result kickout(){ return new Result(ResultStatusCode.INVALID_TOKEN); } }
登錄和退出登錄
package com.alex.springboot.controller; import com.alex.springboot.system.enums.ResultStatusCode; import com.alex.springboot.system.vo.Result; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/admin") public class LoginController { @RequestMapping("/login") public Result login(String loginName, String pwd){ try { UsernamePasswordToken token = new UsernamePasswordToken(loginName, pwd); //登錄不在該處處理,交由shiro處理 Subject subject = SecurityUtils.getSubject(); subject.login(token); if (subject.isAuthenticated()) { JSON json = new JSONObject(); ((JSONObject) json).put("token", subject.getSession().getId()); return new Result(ResultStatusCode.OK, json); }else{ return new Result(ResultStatusCode.SHIRO_ERROR); } }catch (IncorrectCredentialsException | UnknownAccountException e){ return new Result(ResultStatusCode.NOT_EXIST_USER_OR_ERROR_PWD); }catch (LockedAccountException e){ return new Result(ResultStatusCode.USER_FROZEN); }catch (Exception e){ return new Result(ResultStatusCode.SYSTEM_ERR); } } /** * 退出登錄 * @return */ @RequestMapping("/logout") public Result logout(){ SecurityUtils.getSubject().logout(); return new Result(ResultStatusCode.OK); } }
測試接口方法
package com.alex.springboot.controller; import com.alex.springboot.service.UserService; import com.alex.springboot.system.vo.Grid; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/admin/user") public class UserController { @Autowired private UserService userService; @RequiresPermissions("sys:user:view") @RequestMapping("findList") public Grid findList(){ return userService.findList(); } }
Demo地址:https://github.com/DeityJian/springboot-shiro.git