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

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

1:pom文件



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.4.RELEASE
         
    
    com.yunfei
    xxx
    0.0.1-SNAPSHOT
    xxx
    study demo

    
        1.8
        1.3.0
        5.1.39
        1.2.47
        2.6.11
        2.0.4
        3.3.2
        1.9
        1.4.0
        3.1.0
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.redisson
            redisson
            2.7.0
        
        
        
            mysql
            mysql-connector-java
            ${mysql-connector}
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            ${mybatis-spring-boot}
        
        
        
            org.projectlombok
            lombok
        
        
        
            org.slf4j
            jcl-over-slf4j
        
        
        
            ch.qos.logback
            logback-classic
        
        
            javax.servlet
            javax.servlet-api
        

        
        
            com.alibaba
            druid
            1.0.18
        
        
        
            com.alibaba
            fastjson
            ${fastjson.version}
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        

        
        
            redis.clients
            jedis
            2.9.0
        
        
            net.sf.ehcache
            ehcache-core
            ${ehcache.version}
        
        
            net.sf.ehcache
            ehcache-web
            ${ehcache-web.version}
        
        
            commons-net
            commons-net
            3.6
        
        
            org.apache.commons
            commons-lang3
            ${commons-lang3.version}
        
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        

        
            commons-codec
            commons-codec
            ${commons-codec.version}
        

        
        
            org.apache.poi
            poi
            RELEASE
        
        
            org.apache.poi
            poi-ooxml
            RELEASE
        

        
            org.apache.shiro
            shiro-spring
            ${shiro-spring.version}
        
        
            
            org.crazycake
            shiro-redis
            ${shiro-redis.version}
        
    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
            
                org.mybatis.generator
                mybatis-generator-maven-plugin
                1.3.5
                
                    
                    true
                    
                    true
                
            
        

        
            
                src/main/resources
                
                    **/*.xml
                    **/*.properties
                    **/*.yml
                    **/*.*
                
            
            
                src/main/java
                
                    **/*.properties
                    **/*.yml
                
            
        
    



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 filterMap = new LinkedHashMap<>();
        filterMap.put("authc", new MyFormAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        // 过滤器链定义映射
        Map 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;
    }

    /**
     * 处理参数绑定异常,并拼接出错的参数异常信息。
     * 

* 创建人:leigq
* 创建时间:2017年10月16日 下午9:09:22
*

* 修改人:
* 修改时间:
* 修改备注:
*

* * @param result */ private String handleBindingResult(BindingResult result) { if (result.hasErrors()) { final List 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 userRoleList=userRoleMapper.findUserRoleByUserId(user.getId());
        if(userRoleList.size()>0){
            userRoleList.forEach(t->{
                authorizationInfo.addRole(t.getRoleMarking());
            });
        }

        //获取用户权限
        List 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 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 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);
    }

 

你可能感兴趣的:(spring集成,中间件)