shiro-spring-boot-starter

第2.1.7章 WEB系统最佳实践Spring文件配置之spring-shiro.xml
2016年还在使用shiro,后来使用应用springboot之后,因为有了网关,感觉网关就可以做一些拦截,就没必要一定要使用shiro,如果你使用平台还需要每个系统自己做权限拦截吗,除非有人绕开网关,直接公司后台的系统,那么这些后台系统的安全就崩溃了。可是如果你绕开不了网关,就不是那么容易了。
最近做后台系统或者本地化系统,就要求简单使用,而不是非得用平台式做法,搞那么多微服务,于是又将shiro捡起来了。shiro-spring-boot-starter不要轻易使用1.10.1当前最新版本,因为出现的一些错误,在互联网上找不到答案,除非你有时间仔细看官网的资料。但快节奏要掌握很多“无用知识”的我们,还是先交活,把架子搞起来,再做优化。
本地化就不需要一定要使用,多一个redis我都嫌,不得不捡起第2.1.7章 WEB系统最佳实践Spring文件配置之spring-shiro.xml
如果能支持caffeine就好了,或许可以自己扩展,但当前没有时间研究。先还是讲究如何落地吧。
参考的文章有
SpringBoot+Shiro+Vue实现身份验证
Shiro和SpringBoot集成前后端分离登陆验证和权限验证接口302获取不到返回结果的问题
Spring Boot + shiro 去除Redis缓存

 <shiro.version>1.6.0shiro.version>
<shiro-ehcache.version>1.4.2shiro-ehcache.version>
<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-spring-boot-starterartifactId>
    <version>${shiro.version}version>
dependency>
<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-ehcacheartifactId>
    <version>${shiro-ehcache.version}version>
dependency>
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;


public class UserRealm extends AuthorizingRealm {

    @Lazy
    @Autowired
    private SysUserService sysUserService;


    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        SysUser sysUser = sysUserService.selectByPhone(token.getUsername());
        if (sysUser != null){
            return new SimpleAuthenticationInfo(sysUser, sysUser.getPassword(), ByteSource.Util.bytes(sysUser.getSalt()),getName());
        }
        return null;
    }
}

登陆后的请求校验Authorization请求头中jwt是否有效

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

public class CustomDefaultWebSessionManager extends DefaultWebSessionManager {
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String sessionId = WebUtils.toHttp(request).getHeader("Authorization");
        // 如果请求头中有 Authorization 则其值为sessionId
        if (CheckEmptyUtil.isEmpty(sessionId)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }

}

这里做了一下改造,直接复制别人的是不可用的,因为我们一般权限都是通过@RequiresPermissions("sysUser:edit")这种方式,如果是vue前后端分离,就需要将servletPath 做一下转换才行
经过实践CustomPermissionsAuthorizationFilter 这种方式是不正确的,不需要多此一举。
在controller全局异常捕获增加,因为@RequiresPermissions通过aop切面被AuthorizationAttributeSourceAdvisor管理,这样使用@RequiresPermissions(value={"sysUser:load","sysUser:edit"},logical = Logical.OR)原生shiro已经具备的框架已经比较完善和方便扩展了。

    /**
     * 处理访问方法时权限不足问题
     * @param res
     * @param e
     * @throws IOException
     */
    @ExceptionHandler(value = UnauthorizedException.class)
    public void authcErrorHandler(HttpServletResponse res, Exception e) throws IOException {
        log.info("抛出UnauthorizedException权限异常");
        res.setHeader("Access-Control-Allow-Credentials", "true");
        res.setContentType("application/json; charset=utf-8");
        res.setStatus(HttpServletResponse.SC_OK);
        PrintWriter writer = res.getWriter();
        ResponseResult resp = new ResponseResult(false,"权限不足");
        writer.write(JSON.toJSONString(resp));
        writer.close();
    }
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 * 自定义权限
 */
public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {
    private final static String SLASH = "/";
    private final static String COLON = ":";
    /**
     * 根据请求接口路径进行验证
     * @param request
     * @param response
     * @param mappedValue
     * @return
     * @throws IOException
     */
    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
        // 获取接口请求路径
        String servletPath = WebUtils.toHttp(request).getServletPath();
        String permssionPath = getPermissionPath(servletPath);
        mappedValue = new String[]{permssionPath};
        return super.isAccessAllowed(request, response, mappedValue);
    }

    private String getPermissionPath(String servletPath){
        String[] paths = servletPath.split(SLASH);
        return String.join(COLON,paths[1],paths[2]);
    }

    /**
     * 解决权限不足302问题
     * @param request
     * @param response
     * @return
     * @throws IOException
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        Subject subject = getSubject(request, response);
        if (subject.getPrincipal() == null) {
            saveRequestAndRedirectToLogin(request, response);
        } else {
            ResponseResult resp = new ResponseResult(false, "无访问权限");
            WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
            WebUtils.toHttp(response).getWriter().print(resp);
        }
        return false;
    }
}

前端只需要定义指令即可

import type { Directive } from 'vue'
import { usePermission } from '@/hooks'

const auth: Directive<HTMLElement, string | undefined> = {
	mounted(el, binding) {
		const { checkPermission } = usePermission()
		if (!checkPermission(binding.value)) {
			el.remove()
		}
	},
}

export default auth

使用参考

删除
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
 *
 * 重写权限验证问题,登录失效302后返回状态码
 *
 */
@Slf4j
public class ShiroFormAuthenticationFilter extends FormAuthenticationFilter {

    /**
     * 屏蔽OPTIONS请求
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        boolean accessAllowed = super.isAccessAllowed(request, response, mappedValue);
        if (!accessAllowed) {
            // 判断请求是否是options请求
            String method = WebUtils.toHttp(request).getMethod();
            if (StringUtils.equalsIgnoreCase("OPTIONS", method)) {
                return true;
            }
        }
        return accessAllowed;
    }


    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                return executeLogin(request, response);
            } else {
                return true;
            }
        } else {
            // 返回固定的JSON串
            ResponseResult resp = new ResponseResult(false, "未登录");
            WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
            WebUtils.toHttp(response).getWriter().print(resp);
            return false;
        }
    }

}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtConfiguration {

    @Value("${jwt.shiro.hashIterations:0}")
    public int hashIterations;

    @Value("${jwt.shiro.hashAlgorithmName:}")
    public String hashAlgorithmName;

    @Value("${jwt.shiro.saltSize:0}")
    public int saltSize;

    @Value("${jwt.session.expire:0}")
    public int expire;

    @Value("${jwt.session.token-header:}")
    public String tokenHeader;

    @Value("${jwt.session.rsa-secret:}")
    public String rsaSecret;

}
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.Map;

@Configuration
public class ShiroConfig {


    @Autowired
    private JwtConfiguration jwtConfiguration;

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 设置自定义的过滤器
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        filters.put("logout", new ShiroLogoutFilter());
        filters.put("authc", new ShiroFormAuthenticationFilter());
        // 配置过滤器
        Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
        filterChainDefinitionMap.put("/auth/logout", "logout");
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/auth/login", "anon");
        // 认证
        filterChainDefinitionMap.put("/sysRole/getMenus", "authc");
        filterChainDefinitionMap.put("/sysRole/findAll", "authc");
        filterChainDefinitionMap.put("/sysRole/getPermissions", "authc");
        filterChainDefinitionMap.put("/**/find", "authc");// 一般为主列表
        // 认证+授权
        filterChainDefinitionMap.put("/**", "authc,perms");
        shiroFilterFactoryBean.setLoginUrl("/login");
//        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        securityManager.setSessionManager(this.sessionManager());
        //设置缓存
        securityManager.setCacheManager(getCacheManager());
        return securityManager;
    }


    @Bean
    public UserRealm userRealm() {
        UserRealm myShiroRealm = new UserRealm();
        myShiroRealm.setCredentialsMatcher(this.hashedCredentialsMatcher());
        return myShiroRealm;
    }

    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName(jwtConfiguration.hashAlgorithmName);
        hashedCredentialsMatcher.setHashIterations(jwtConfiguration.hashIterations);
        return hashedCredentialsMatcher;
    }


    @Bean
    public DefaultWebSessionManager sessionManager() {
        return new CustomDefaultWebSessionManager();
    }

    /**
     *
     * 缓存框架
     * @return
     */
    @Bean
    public EhCacheManager getCacheManager(){
        EhCacheManager ehCacheManager = new EhCacheManager();
        ehCacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml");
        return ehCacheManager;
    }

    /**
     * Shiro生命周期处理器
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }



    /**
     * 开启shiro aop注解支持
     * 使用代理方式;所以需要开启代码支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


}

因为logout有个重定向,故而也需要自己扩展一下

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
 * 重写shiro logout逻辑,避免是Ajax请求发生302重定向问题
 */
@Slf4j
public class ShiroLogoutFilter extends LogoutFilter {

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = this.getSubject(request, response);
        String redirectUrl = this.getRedirectUrl(request, response, subject);
        subject.logout();
        WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
        ResponseResult resp = new ResponseResult(true, "成功登出");
        WebUtils.toHttp(response).getWriter().print(resp);
        return false;
    }
}


<ehcache>
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            memoryStoreEvictionPolicy="LRU">
    defaultCache>
ehcache>

shiro-spring-boot-starter_第1张图片
看看controler的写法和ajax的写法,就知道问题在哪里了。
shiro-spring-boot-starter_第2张图片
因为Content-type使用了application/json,故而需要使用@RequestBody来取值,因为参数在payload中,不像application/x-www-form-urlencoded,是在url参数中。
shiro-spring-boot-starter_第3张图片
这样针对application/json就需要按照下面的方法才能取到值。

    @ApiOperation("登录")
    @PostMapping("login")
    public ResponseResult<LoginResultDto> login(@RequestBody UserVo userVo){
        String username = userVo.getUsername();
        String password = userVo.getPassword();
        Subject subject = SecurityUtils.getSubject()

接着再看vue侧的写法

import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'
public post = (url: string, data = {}, config: AxiosRequestConfig<any> = {}): Promise<any> =>
		this.instance({ url, method: 'post', data, ...config })

如果默认采用application/json问题是,有两个问题
1 controller接收参数需要一堆碎片化的dto,如果参数不多,那么我们的请求为什么一定要用application/json
前端换种请求,就可以识别application/x-www-form-urlencoded

public postForm = (url: string, data = {} , config: AxiosRequestConfig<any> = {}): Promise<any> =>
	axios({
		...this.baseConfig,
		headers:{
			...this.baseConfig.headers,
			'Content-Type': "application/x-www-form-urlencoded"
		},
		url,
		method: 'post',
		data,
		...config,
	})

Vue3 + Ts的记住密码实现,像这篇作者可能就没搞明白,base64只是一个编码方式,并不是加密方式.另外localStorage是永久性,并没有没有过期时间,还得自己去实现过期

public setItem(key: keyName, value: any, duration?: number) {
         try {
            let data: storageData = {
                value: value,
                expiryTime: duration ? this.getCurrentTimeStamp() + duration : 0,
            }
            switch(this.mode){
                case 'local': case 'session':
                    this.storage.setItem(`${this.prefix}${key}`, window.JSON.stringify(data))
                    break;
                case 'cookie':
                    debugger
                    this.storage.set(`${this.prefix}${key}`, window.JSON.stringify(data),{expires:duration})
                    break;
            }
         } catch (err) {
             console.warn(`Storage ${key} set error`, err)
         }
     }
public getItem(key: keyName) {
         let result = null;
         switch(this.mode){
            case 'local': case 'session':
                result = this.storage.getItem(`${this.prefix}${key}`)
                break;
            case 'cookie':
                result = this.storage.get(`${this.prefix}${key}`)
                break;
        }
         if (!result || result === 'null') return null
         let now = this.getCurrentTimeStamp()
         let obj: storageData
         try {
             obj = window.JSON.parse(result)
         } catch (error) {
             return null
         }
         if (obj.expiryTime === 0 || obj.expiryTime > now) {
             return obj.value
         }
         return null
     }
 

考虑localstorage和cookie是考虑了一定量的安全性,一个人如果拥有你电脑的使用权,而且你还记住了密码,那么base64没有什么用,但如果没有掌握你的电脑,使用cookie就需要设置httponly,但前后端分离的工程,前端将cookie当成存储工具,是控制不了的,只有服务端产生的才有用。
故而前后端分离的vue工程中,shiro中rememberme功能没有啥作用。

你可能感兴趣的:(岁月云——Web系统最佳实践,spring,boot,shiro)