前後分離springboot2.1集成shiro使用redis做權限認證緩存

整整搞了兩天,網上好多文章沒有標註出小版本,讓我很是艱難。這裏記錄一下。

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);
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章