整整搞了兩天,網上好多文章沒有標註出小版本,讓我很是艱難。這裏記錄一下。
1:pom文件
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yunfei</groupId>
<artifactId>xxx</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>xxx</name>
<description>study demo</description>
<properties>
<java.version>1.8</java.version>
<mybatis-spring-boot>1.3.0</mybatis-spring-boot>
<mysql-connector>5.1.39</mysql-connector>
<fastjson.version>1.2.47</fastjson.version>
<ehcache.version>2.6.11</ehcache.version>
<ehcache-web.version>2.0.4</ehcache-web.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-codec.version>1.9</commons-codec.version>
<shiro-spring.version>1.4.0</shiro-spring.version>
<shiro-redis.version>3.1.0</shiro-redis.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.7.0</version>
</dependency>
<!-- MySQL 連接驅動依賴 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector}</version>
</dependency>
<!-- SpringBoot Mybatis 依賴 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot}</version>
</dependency>
<!-- lombok依賴 可以減少大量的模塊代碼-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--Slf4j 依賴-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
<!-- logback 依賴 是slf4j的實現-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- Druid數據庫連接池組件 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.18</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>${ehcache.version}</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-web</artifactId>
<version>${ehcache-web.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<!--poi-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring.version}</version>
</dependency>
<dependency>
<!--session持久化插件-->
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis.version}</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.5</version>
<configuration>
<!--允許移動生成的文件 -->
<verbose>true</verbose>
<!--允許覆蓋生成的文件 -->
<overwrite>true</overwrite>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
<include>**/*.yml</include>
<include>**/*.*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.yml</include>
</includes>
</resource>
</resources>
</build>
</project>
2:shiro配置類
package com.yunfei.cultural.shiro;
import com.yunfei.cultural.filter.MyFormAuthenticationFilter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
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.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Description: shiro配置類
* @Author: HuiYunfei
* @Date: 2019/11/9
*/
@Configuration
@Slf4j
@Data
@ConfigurationProperties(prefix = "spring.redis")
public class ShiroConfig {
private String host;
private int port = 6379;
private Duration timeout;
/**
* Filter工廠,設置對應的過濾條件和跳轉條件
*
* @return ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//自定義過濾器,前後分離重定向會出現302等ajax跨域錯誤,這裏直接返回錯誤不重定向
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("authc", new MyFormAuthenticationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 過濾器鏈定義映射
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
/*
* anon:所有url都都可以匿名訪問,authc:所有url都必須認證通過纔可以訪問;
* 過濾鏈定義,從上向下順序執行,authc 應放在 anon 下面
* */
filterChainDefinitionMap.put("/system/login", "anon");
filterChainDefinitionMap.put("/file/*", "anon");
//filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
// 所有url都必須認證通過纔可以訪問
filterChainDefinitionMap.put("/**", "authc");
// 配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了, 位置放在 anon、authc下面
filterChainDefinitionMap.put("/system/logout", "logout");
// 未登錄
//shiroFilterFactoryBean.setLoginUrl("/system/unLogin");
// 未授權
//shiroFilterFactoryBean.setUnauthorizedUrl("/system/unAuthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* RedisSessionDAO shiro sessionDao層的實現 通過redis, 使用的是shiro-redis開源插件
*
* @return RedisSessionDAO
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
redisSessionDAO.setExpire(1800);
return redisSessionDAO;
}
/**
* Session ID 生成器
*
* @return JavaUuidSessionIdGenerator
*/
@Bean
public JavaUuidSessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
/**
* 自定義sessionManager,禁用cookie,使用http header方式傳入sessionId token
*
* @return SessionManager
*/
@Bean
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setSessionIdCookieEnabled(false);
mySessionManager.setSessionDAO(redisSessionDAO());
//這裏修改sessionIdCookie的Name屬性爲jsid可以避免同一請求都會在redis生成一條新的sessionId記錄
mySessionManager.getSessionIdCookie().setName("jsid");
return mySessionManager;
}
/**
* 配置shiro redisManager, 使用的是shiro-redis開源插件
*
* @return RedisManager
*/
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
//redisManager.setPort(port);
redisManager.setTimeout((int) timeout.toMillis());
return redisManager;
}
/**
* cacheManager 緩存 redis實現, 使用的是shiro-redis開源插件
*
* @return RedisCacheManager
*/
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
// 必須要設置主鍵名稱,shiro-redis 插件用過這個緩存用戶信息
redisCacheManager.setPrincipalIdFieldName("id");
return redisCacheManager;
}
/**
* 權限管理,配置主要是Realm的管理認證
*
* @return SecurityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
// 自定義session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定義緩存實現 使用redis
securityManager.setCacheManager(cacheManager());
return securityManager;
}
/**
* 自定義安全域,用戶驗證、權限等數據在此提供
* @return
*/
@Bean
public ShiroRealm myShiroRealm() {
ShiroRealm myShiroRealm = new ShiroRealm();
//關閉
myShiroRealm.setAuthenticationCachingEnabled(false);
//myShiroRealm.setAuthenticationCacheName("authenticcationCache");
myShiroRealm.setAuthorizationCachingEnabled(true);
myShiroRealm.setAuthorizationCacheName("authorizationCache");
return myShiroRealm;
}
/*
* 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
* 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public SimpleCookie cookie() {
// cookie的name,對應的默認是 JSESSIONID
SimpleCookie cookie = new SimpleCookie("SHARE_JSESSIONID");
cookie.setHttpOnly(true);
// path爲 / 用於多個系統共享 JSESSIONID
//cookie.setPath("/");
return cookie;
}
/* 此項目使用 shiro 場景爲前後端分離項目,這裏先註釋掉,統一異常處理已在 GlobalExceptionHand.java 中實現 */
}
這裏要注意的是我在很多博客上看到說前後分離的時候shiro過濾器不能跳轉jsp,要直接返回給客戶端狀態讓客戶端控制跳轉,所以這裏要shiroFilterFactoryBean.setLoginUrl("/system/unLogin");重定向一下,然後在controller裏邊返回給json給前端。但是!!!實際上這麼操作會出現前端頁面循環跳轉跨域問題:request doesnt pass access control check:Redirect is not allowed for a preflight request。所以改成在上邊添加自定義登陸校驗異常過濾器MyFormAuthenticationFilter,然後設爲"authc"。
在緩存了用戶的認證、授權信息後shiro提供的退出方法有一個bug就是無法刪除用戶的認證信息,看過底層redis操作的源碼可以發現認證和授權的刪除方法並不太一樣。有興趣的可以去看看源碼然後在登陸認證方法返回SimpleAuthenticationInfo對象的時候返回用戶的id去做對應的修改。
package com.yunfei.cultural.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Created by [email protected] on 2019/11/18
*/
@Slf4j
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
public MyFormAuthenticationFilter() {
super();
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
log.info("進入自定義shiro攔截器isAccessAllowed方法");
if(request instanceof HttpServletRequest){
if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")){
log.info("進入自定義shiro攔截器isAccessAllowed方法:OPTIONS請求");
return true;
}
}
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
log.info("進入身份認證失敗filter");
// HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// httpServletResponse.setStatus(200);
// httpServletResponse.setContentType("application/json;charset=utf-8");
// PrintWriter pw = httpServletResponse.getWriter();
// ResultObj result=new ResultObj();
// result.setInfo(401);
// result.setMsg("身份認證失敗,請重新登錄");
// pw.write(JSONObject.toJSONString(result));
// pw.flush();
// pw.close();
// return false;
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
我這個地方直接printwriter打印的信息前端看不到不知道爲啥,沒辦法只能把http狀態碼改成401校驗錯誤給前端讓他們判斷是否校驗成功。
理論上角色、權限認證失敗也可以直接重寫對應的過濾器RolesAuthorizationFilter、PermissionsAuthorizationFilter的onAccessDenied方法。我這麼試過但是沒有起作用,因爲我的權限、角色認證失敗被異常處理類捕捉了。
package com.yunfei.cultural.utils.exception;
import com.yunfei.cultural.utils.result.ResultObj;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import java.util.List;
/**
* @author http://gblfy.com
* @Description 全局異常處理
* @Date 2019/9/14 15:34
* @version1.0
*/
@EnableWebMvc
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHand {
/**
* 401 - 未登錄
*/
@ExceptionHandler(UnLoginException.class)
public ResultObj handleUnLoginException(UnLoginException e) {
String msg = e.getMessage();
log.error("登錄異常:", e);
ResultObj resultObj = new ResultObj();
resultObj.setInfo(401);
resultObj.setMsg(msg);
return resultObj;
}
/**
* 900 - 參數異常
*/
@ExceptionHandler(LogicException.class)
public ResultObj handleLogicException(LogicException e) {
String msg = e.getMessage();
log.error("參數異常", e);
ResultObj resultObj = new ResultObj();
resultObj.setInfo(900);
resultObj.setMsg(msg);
return resultObj;
}
/**
* 403 - 無權限
*/
@ExceptionHandler(UnauthorizedException.class)
public ResultObj handleLoginException(UnauthorizedException e) {
String msg = e.getMessage();
log.error("用戶無權限:", e);
ResultObj resultObj = new ResultObj();
resultObj.setInfo(403);
resultObj.setMsg("用戶無權限");
return resultObj;
}
/**
* 999 - 服務器異常
*/
@ExceptionHandler(SystemException.class)
public ResultObj handleSysException(SystemException e) {
String msg = "服務內部異常!" + e.getMessage();
log.error(msg, e);
ResultObj resultObj = new ResultObj();
resultObj.setInfo(999);
resultObj.setMsg(e.getMessage());
return resultObj;
}
/**
* 999 - 服務器異常
*/
@ExceptionHandler(Exception.class)
public ResultObj handleException(Exception e) {
String msg = "服務內部異常!" + e.getMessage();
log.error(msg, e);
ResultObj resultObj = new ResultObj();
resultObj.setInfo(999);
resultObj.setMsg(e.getMessage());
return resultObj;
}
/**
* 處理參數綁定異常,並拼接出錯的參數異常信息。
* <p>
* 創建人:leigq <br>
* 創建時間:2017年10月16日 下午9:09:22 <br>
* <p>
* 修改人: <br>
* 修改時間: <br>
* 修改備註: <br>
* </p>
*
* @param result
*/
private String handleBindingResult(BindingResult result) {
if (result.hasErrors()) {
final List<FieldError> fieldErrors = result.getFieldErrors();
return fieldErrors.iterator().next().getDefaultMessage();
}
return null;
}
}
有人要問那爲什麼登陸認證異常全局異常沒有捕捉到呢,捕捉到了不就也可以不用重寫過濾器了嗎?理論上是這樣但可能我捕捉的異常非shiro內部的登陸異常也可能是其他原因反正我沒有成功,有搞成功的小夥伴可以貼在下邊哦。
3:shiro認證授權類
package com.yunfei.cultural.shiro;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunfei.cultural.entity.TUser;
import com.yunfei.cultural.mapper.TRolePermissionsMapper;
import com.yunfei.cultural.mapper.TUserRoleMapper;
import com.yunfei.cultural.model.vo.RolePermissionsModel;
import com.yunfei.cultural.model.vo.UserRoleModel;
import com.yunfei.cultural.service.UserService;
import com.yunfei.cultural.utils.MySimpleByteSource;
import com.yunfei.cultural.utils.ShiroUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
/**
* @Description: 自定義shiro認證賦權類
* @Author: HuiYunfei
* @Date: 2019/11/9
*/
@Slf4j
@Component
public class ShiroRealm extends AuthorizingRealm {
public ShiroRealm() {
}
@Autowired
@SuppressWarnings("all")
public ShiroRealm(UserService userService,TUserRoleMapper userRoleMapper,TRolePermissionsMapper rolePermissionsMapper) {
this.userService = userService;
this.rolePermissionsMapper=rolePermissionsMapper;
this.userRoleMapper=userRoleMapper;
}
@Resource
private UserService userService;
@Autowired
private TUserRoleMapper userRoleMapper;
@Autowired
private TRolePermissionsMapper rolePermissionsMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
TUser user = (TUser) principals.getPrimaryPrincipal();
//TUser user = userService.findUserByUserName(username);
//獲取用戶角色
List<UserRoleModel> userRoleList=userRoleMapper.findUserRoleByUserId(user.getId());
if(userRoleList.size()>0){
userRoleList.forEach(t->{
authorizationInfo.addRole(t.getRoleMarking());
});
}
//獲取用戶權限
List<RolePermissionsModel> rolePermissionsList = rolePermissionsMapper.findRolePermissionsByUserId(user.getId());
if(rolePermissionsList.size()>0){
rolePermissionsList.forEach(t->{
authorizationInfo.addStringPermission(t.getPermissionsMarking());
});
}
return authorizationInfo;
}
/*主要是用來進行身份認證的,也就是說驗證用戶輸入的賬號和密碼是否正確。*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
//獲取用戶的輸入的賬號.
String username = (String) token.getPrincipal();
//實際項目中,這裏可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
TUser user = userService.findUserByUserName(username);
if(user==null){
throw new UnknownAccountException();
}
if(user.getStatus()==1){
throw new DisabledAccountException("賬號已禁用!");
}
//處理session
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
DefaultWebSessionManager sessionManager = (DefaultWebSessionManager)securityManager.getSessionManager();
Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();//獲取當前已登錄的用戶session列表
if(sessions.size()>0){
for(Session session:sessions){
//清除該用戶以前登錄時保存的session
if(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY)!=null){
Object obj = ((SimplePrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY)).asList().get(0);
ObjectMapper objectMapper = new ObjectMapper();
TUser tUser = objectMapper.convertValue(obj, TUser.class);
if(username.equals(tUser.getUsername())) {
sessionManager.getSessionDAO().delete(session);
}
}
}
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user, //用戶名
user.getPassword(), //密碼
//ByteSource.Util.bytes(user.getSalt()),// md5(salt+password),採用明文訪問時,不需要此句
new MySimpleByteSource(user.getSalt()),
getName() //realm name
);
return authenticationInfo;
}
/**
* 將自己的驗證方式加入容器
*
* 憑證匹配器(由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了)
*
* @param credentialsMatcher
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
/**
* 散列算法:這裏可以使用MD5算法 也可以使用SHA-256
*/
hashedCredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);
// 散列的次數,比如散列16次,相當於 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);
super.setCredentialsMatcher(hashedCredentialsMatcher);
}
/**
* 重寫方法,清除當前用戶的的 授權緩存
* @param principals
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
/**
* 重寫方法,清除當前用戶的 認證緩存
* @param principals
*/
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
/**
* 自定義方法:清除所有 授權緩存
*/
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
/**
* 自定義方法:清除所有 認證緩存
*/
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
/**
* 自定義方法:清除所有的 認證緩存 和 授權緩存
*/
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
身份認證方法發現每次用戶重新登陸以後之前的token並沒有過期,所以加了一個處理session的功能。
4:自定義session獲取類。因項目是前後分離的,前端是在Ajax的請求頭加上token訪問的,所以要重寫這個取session的方法
package com.yunfei.cultural.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.session.Session;
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;
/**
* @Description: 傳統結構項目中,shiro從cookie中讀取sessionId以此來維持會話,在前後端分離的項目中(也可在移動APP項目使用),
* 我們選擇在ajax的請求頭中傳遞sessionId,因此需要重寫shiro獲取sessionId的方式。
* 自定義MySessionManager類繼承DefaultWebSessionManager類,重寫getSessionId方法
* @Author: HuiYunfei
* @Date: 2019/11/9
*/
@Slf4j
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果請求頭中有 Authorization 則其值爲sessionId
if (!StringUtils.isEmpty(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 null;//super.getSessionId(request, response);
}
}
//這個方法加不加我也沒看出來區別
@Override
protected Session retrieveSession(SessionKey sessionKey){
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if(sessionKey instanceof WebSessionKey){
request = ((WebSessionKey)sessionKey).getServletRequest();
}
if(request != null && sessionId != null){
Session session = (Session) request.getAttribute(sessionId.toString());
if(session != null){
return session;
}
}
Session session = super.retrieveSession(sessionKey);
if(request != null && sessionId != null){
request.setAttribute(sessionId.toString(),session);
}
return session;
}
}
5:登陸退出方法
public LoginResult login(LoginParams params) {
LoginResult result = new LoginResult();
// 獲取Subject實例對象,用戶實例
Subject currentUser = SecurityUtils.getSubject();
// 將用戶名和密碼封裝到UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(params.getUsername(), params.getPassword());
// 認證
try {
// 傳到 MyShiroRealm 類中的方法進行認證
currentUser.login(token);
// 構建緩存用戶信息返回給前端
TUser user = (TUser) currentUser.getPrincipals().getPrimaryPrincipal();
//TUser user = this.userMapper.findByUserName(username);
//校驗當前用戶是否有角色
List<UserRoleModel> userRoleList=userRoleMapper.findUserRoleByUserId(user.getId());
if(userRoleList.size()==0){
throw new LogicException("用戶暫無角色,不能登錄");
}
//校驗當前用戶是否有權限登錄到後臺(是否管理員角色)
boolean isAdmin=false;
for (UserRoleModel userRole : userRoleList) {
if(userRole.getRoleMarking().equals(CommonConstants.ROLE_ADMIN_MARKING)){
isAdmin=true;
}
}
result.setIsAdmin(isAdmin);
BeanUtils.copyProperties(user, result);
result.setToken(currentUser.getSession().getId().toString());
userMapper.updateByPrimaryKeySelective(TUser.builder().id(user.getId()).token(result.getToken()).build());
}catch (UnknownAccountException e) {
throw new LogicException("賬號不存在!");
}catch (IncorrectCredentialsException e) {
throw new LogicException("密碼錯誤!");
}
return result;
}
@Override
public void logout(JSONObject params) {
Subject subject = SecurityUtils.getSubject();
subject.logout();
}
6:shiroUtils
package com.yunfei.cultural.utils;
import com.yunfei.cultural.entity.TUser;
import com.yunfei.cultural.shiro.ShiroRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import sun.misc.BASE64Encoder;
import java.security.SecureRandom;
import java.util.Random;
/**
* Shiro工具類
*/
public class ShiroUtils {
/** 加密算法 */
public final static String hashAlgorithmName = "SHA-256";
/** 循環次數 */
public final static int hashIterations = 16;
public static String sha256(String password, String salt) {
return new SimpleHash(hashAlgorithmName, password, salt, hashIterations).toString();
}
// 獲取一個測試賬號 admin
public static void main(String[] args) {
// 3743a4c09a17e6f2829febd09ca54e627810001cf255ddcae9dabd288a949c4a
String salt=getNextSalt();
System.out.println("salt:"+salt);
System.out.println("password:"+sha256("yunfei",salt)) ;
}
public static String getNextSalt() {
Random RANDOM = new SecureRandom();
byte[] salt = new byte[16];
RANDOM.nextBytes(salt);
String str = new BASE64Encoder().encode(salt);
return str;
}
/**
* 獲取會話
*/
public static Session getSession() {
return SecurityUtils.getSubject().getSession();
}
/**
* Subject:主體,代表了當前“用戶”
*/
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
/**
* 重新賦值權限(在比如:給一個角色臨時添加一個權限,需要調用此方法刷新權限,否則還是沒有剛賦值的權限)
* @param myRealm 自定義的realm
* @param username 用戶名
*/
// public static void reloadAuthorizing(ShiroRealm myRealm, String userName){
// Subject subject = SecurityUtils.getSubject();
// String realmName = subject.getPrincipals().getRealmNames().iterator().next();
// //第一個參數爲用戶名,第二個參數爲realmName,test想要操作權限的用戶
// subject.runAs(new SimplePrincipalCollection(userName, subject.getPrincipals().getRealmNames().iterator().next()));
// myRealm.getAuthorizationCache().remove(subject.getPrincipals());
// subject.releaseRunAs();
// }
/**
* @Description:清除所有用戶的權限信息(修改用戶、修改角色時調用)
* @Author: HuiYunfei
* @Date: 2019/11/12
*/
public static void clearAllCachedAuthorizationInfo(){
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next();
shiroRealm.clearAllCachedAuthorizationInfo();
}
/**
* @Description:清除所有用戶的認證緩存(暫未啓用認證緩存)
* @Author: HuiYunfei
* @Date: 2019/11/12
*/
public static void clearAllCachedAuthenticationInfo(){
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next();
shiroRealm.clearAllCachedAuthorizationInfo();
}
public static TUser getUserEntity() {
return (TUser) SecurityUtils.getSubject().getPrincipal();
}
public static Integer getUserId() {
return getUserEntity().getId();
}
public static void setSessionAttribute(Object key, Object value) {
getSession().setAttribute(key, value);
}
public static Object getSessionAttribute(Object key) {
return getSession().getAttribute(key);
}
public static boolean isLogin() {
return SecurityUtils.getSubject().getPrincipal() != null;
}
public static void logout() {
SecurityUtils.getSubject().logout();
}
}
裏邊提供了獲取加密密碼方法和清楚認證授權緩存的方法。這樣在修改用戶、角色、權限相關信息的時候可以刪除緩存實現直接刷新對應用戶權限功能。(清除單個用戶的方法沒調成功清除所有的是可用的)
最後:
前後分離解決跨域問題,在主啓動文件直接添加過濾器
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
// 允許cookies跨域
config.setAllowCredentials(true);
// 允許向該服務器提交請求的URI,*表示全部允許。。這裏儘量限制來源域,比如http://xxxx:8080
// ,以降低安全風險。。
config.addAllowedOrigin("*");
// 允許訪問的頭信息,*表示全部
config.addAllowedHeader("*");
// 預檢請求的緩存時間(秒),即在這個時間段裏,對於相同的跨域請求不會再預檢了
config.setMaxAge(18000L);
// 允許提交請求的方法,*表示全部允許,也可以單獨設置GET、PUT等
config.addAllowedMethod("*");
/*
* config.addAllowedMethod("HEAD"); config.addAllowedMethod("GET");//
* 允許Get的請求方法 config.addAllowedMethod("PUT");
* config.addAllowedMethod("POST"); config.addAllowedMethod("DELETE");
* config.addAllowedMethod("PATCH");
*/
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}