第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>
看看controler的写法和ajax的写法,就知道问题在哪里了。
因为Content-type
使用了application/json
,故而需要使用@RequestBody
来取值,因为参数在payload
中,不像application/x-www-form-urlencoded
,是在url参数中。
这样针对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功能没有啥作用。