第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.0</shiro.version>
<shiro-ehcache.version>1.4.2</shiro-ehcache.version>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<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;
}
}
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);
}
}
}
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 {
/**
* 根据请求接口路径进行验证
* @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();
mappedValue = new String[]{servletPath};
return super.isAccessAllowed(request, response, mappedValue);
}
/**
* 解决权限不足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 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.stereotype.Component;
@Component
public class JwtConfiguration {
@Value("${hashIterations:0}")
public int hashIterations;
@Value("${hashAlgorithmName:}")
public String hashAlgorithmName;
@Value("${saltSize:0}")
public int saltSize;
@Value("${jwt.expire:0}")
public int expire;
@Value("${jwt.token-header:}")
public String tokenHeader;
@Value("${jwt.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.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.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 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("authc", new ShiroFormAuthenticationFilter());
filters.put("perms", new CustomPermissionsAuthorizationFilter());
// 配置过滤器
Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/login", "anon");
// filterChainDefinitionMap.put("/**", "authc,perms"); // 登录+授权
filterChainDefinitionMap.put("/**", "anon");
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());
securityManager.setSessionManager(sessionManager());
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 SimpleCookie getSimpleCookie() {
SimpleCookie cookie = new SimpleCookie();
cookie.setName("kfyy.session.id");
cookie.setPath("");
cookie.setHttpOnly(false);
return cookie;
}
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionIdCookie(this.getSimpleCookie());
// sessionManager.setSessionDAO(this.redisSessionDAO());
// sessionManager.setGlobalSessionTimeout(this.sessiontimeout);
return sessionManager;
}
/**
*
* 缓存框架
* @return
*/
@Bean
public EhCacheManager getCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml");
return ehCacheManager;
}
/**
* 开启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;
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<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,
})