springboot+shiro+redis專案整合
阿新 • • 發佈:2018-11-08
介紹:
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼學和會話管理。使用Shiro的易於理解的API,您可以快速、輕鬆地獲得任何應用程式,從最小的移動應用程式到最大的網路和企業應用程式。(摘自百度百科)
本文使用springboot+mybatisplus+shiro實現資料庫動態的管理使用者、角色、許可權管理,在本文的最後我會提供原始碼的下載地址,想看到效果的小夥伴可以直接下載執行就ok了
因為shiro的功能比較多,本章只介紹如下幾個功能
1.當用戶沒有登陸時只能訪問登陸介面
2.當用戶登陸成功後,只能訪問該使用者下僅有的許可權
3.一個使用者不能兩個人同時線上
一、資料庫設計
本文的資料庫表為5個分別是: 使用者表、角色表、許可權表、使用者角色中間表、角色許可權中間表,表的結構和資料專案中會提供(sql和redis工具下方的下載地址中都會有)
二、引入依賴
<?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>
三、編輯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
四、建立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();
}
}
五、自定義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;
}
}
六、限制併發人數登陸
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異常,可以忽略。");
}
}
}
七、異常處理類,攔截未授權頁面(未授權頁面有三種實現方式,我這裡使用異常處理)
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";
}
}
八、最後附上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";
}
}