前后分离springboot2.1集成shiro使用redis做权限认证缓存

卞浩漫
2023-12-01

整整搞了两天,网上好多文章没有标注出小版本,让我很是艰难。这里记录一下。

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 hui.yunfei@qq.com 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);
    }

 

 类似资料: