前言
本文基于token进行身份认证,由于接入cas会和shiro的session管理冲突,所以关闭shiro的session,进行无状态管理。
特此记录一下shiro如何进行无状态管理。
#一、引入依赖
此处引入的为 shiro-spring
,版本为 1.7.1
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
#二、实现DefaultWebSubjectFactory
实现 DefaultWebSubjectFactory
关闭session
package com.hcframe.base.module.shiro;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;
/**
* @author lhc
* @version 1.0
* @className StatelessDefaultSubjectFactory
* @date 2021年04月19日 1:54 下午
* @description 描述
*/
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
#三、实现AuthenticationToken
此处是为了将用户信息改为token传递,通过token方式进行验证
package com.hcframe.base.module.shiro;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @author lhc
* @version 1.0
* @className AuthToken
* @date 2021年04月19日 2:56 下午
* @description 实现shiro AuthenticationToken
*/
public class AuthToken implements AuthenticationToken {
private String token;
public AuthToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
#四、实现shiro的过滤器
此处为权限过滤器,具体内容参见注释
package com.hcframe.base.module.shiro;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author lhc
* @version 1.0
* @className AuthFilter
* @date 2021年04月19日 2:56 下午
* @description 实现shiro 过滤器
*/
public class AuthFilter extends AuthenticatingFilter {
/**
* @author lhc
* @description 创建token
* @date 4:35 下午 2021/4/26
* @params [request, response]
* @return org.apache.shiro.authc.AuthenticationToken
**/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 增加跨域支持
String myOrigin = httpServletRequest.getHeader("origin");
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with, X-Access-Token, datasource-Key");
httpResponse.setHeader("Access-Control-Allow-Origin", myOrigin);
httpResponse.setCharacterEncoding("UTF-8");
// 返回错误状态信息
Map<String, Object> result = new HashMap<>();
result.put("code", 3);
result.put("msg", "未登陆");
String json = JSON.toJSONString(result);
httpResponse.getWriter().print(json);
return null;
}
return new AuthToken(token);
}
/**
* @author lhc
* @description 步骤1.所有请求全部拒绝访问
* @date 4:37 下午 2021/4/26
* @params [request, response, mappedValue]
* @return boolean
**/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
}
/**
* @author lhc
* @description 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
* @date 4:37 下午 2021/4/26
* @params [request, response]
* @return boolean
**/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 增加跨域支持
String myOrigin = httpServletRequest.getHeader("origin");
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with, X-Access-Token, datasource-Key");
httpResponse.setHeader("Access-Control-Allow-Origin", myOrigin);
httpResponse.setCharacterEncoding("UTF-8");
// 返回错误状态信息
Map<String, Object> result = new HashMap<>();
result.put("code", 3);
result.put("msg", "未登陆");
String json = JSON.toJSONString(result);
httpResponse.getWriter().print(json);
return false;
}
return executeLogin(request, response);
}
/**
* @author lhc
* @description 登陆失败时候调用
* @date 4:38 下午 2021/4/26
* @params [token, e, request, response]
* @return boolean
**/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
//处理登录失败的异常
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String myOrigin = httpServletRequest.getHeader("origin");
httpResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with, X-Access-Token, datasource-Key");
httpResponse.setHeader("Access-Control-Allow-Origin", myOrigin);
httpResponse.setCharacterEncoding("UTF-8");
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
Map<String, Object> result = new HashMap<>();
result.put("code", 3);
result.put("msg", "未登陆");
String json = JSON.toJSONString(result);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
}
return false;
}
/**
* @author lhc
* @description 获取请求的token
* @date 4:38 下午 2021/4/26
* @params [httpRequest]
* @return java.lang.String
**/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader("X-Access-Token");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter("token");
}
}
return token;
}
}
#五、编写自定义的Realm
编写自定义realm,此步骤是为了定义权限校验和用户信息验证。
#六、编写Shiro配置类
编写shiro配置类,将bean交给Spring管理
package com.hcframe.base.module.shiro;
import com.hcframe.base.module.shiro.service.SystemRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionManager;
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.mgt.DefaultWebSubjectFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* 不加这个注解不生效,具体不详
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
/**
* 将自己的验证方式加入容器
*/
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
customRealm.setCachingEnabled(false);
return customRealm;
}
/**
* @return org.apache.shiro.web.mgt.DefaultWebSubjectFactory
* @author lhc
* @description // 自定义subject工厂
* @date 4:50 下午 2021/4/19
* @params []
**/
@Bean
public DefaultWebSubjectFactory subjectFactory() {
return new StatelessDefaultSubjectFactory();
}
/**
* @return org.apache.shiro.session.mgt.SessionManager
* @author lhc
* @description // 自定义session管理器
* @date 5:50 下午 2021/4/19
* @params []
**/
@Bean
public SessionManager sessionManager() {
DefaultSessionManager shiroSessionManager = new DefaultSessionManager();
// 关闭session校验轮询
shiroSessionManager.setSessionValidationSchedulerEnabled(false);
return shiroSessionManager;
}
/**
* 权限管理,配置主要是Realm的管理认证
*/
@Bean("securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 禁用session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// 设置自定义subject工厂
securityManager.setSubjectFactory(subjectFactory());
// 设置自定义session管理器
securityManager.setSessionManager(sessionManager());
// 设置自定义realm
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, Filter> filters = new HashMap<>(1);
// 设置自定义过滤器
filters.put("auth", new AuthFilter());
shiroFilterFactoryBean.setFilters(filters);
LinkedHashMap<String, String> map = new LinkedHashMap<>();
// 用户登陆
map.put("/ftUser/login", "anon");
// Vue静态资源
map.put("/img/**", "anon");
map.put("/static/**", "anon");
map.put("/tinymce/**", "anon");
map.put("/favicon.ico", "anon");
map.put("/manifest.json", "anon");
map.put("/robots.txt", "anon");
map.put("/precache*", "anon");
map.put("/service-worker.js", "anon");
// swagger UI 静态资源
map.put("/swagger-ui.html","anon");
map.put("/doc.html","anon");
map.put("/swagger-resources/**","anon");
map.put("/webjars/**","anon");
map.put("/v2/api-docs","anon");
map.put("/v2/api-docs-ext","anon");
map.put("/swagger/**","anon");
// druid 资源路径
map.put("/druid/**","anon");
// cas 接口
map.put("/cas/valid","anon");
map.put("/cas/logout","anon");
// 其余路径均拦截
map.put("/**", "auth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 加入注解的使用,不加入这个注解不生效
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
#七、添加权限注解
此处只展示权限注解,其余注解请查询官方文档
注意
添加权限的注解必须被自定义拦截器拦截
否则会出现不调用自定义 CustomRealm
中的doGetAuthorizationInf()
方法的情况
代码示例:
@GetMapping("/system/list")
@RequiresPermissions(value = { "systemManage","system:list" },logical = Logical.OR)
public ResultVO<Integer> resetPassword(String userId,@PathVariable Integer version) {
return manageService.resetPassword(userId,version);
}
- value :字符串数组,填写之前realm中权限注入的字符串即可
- logical:逻辑关系,数组中权限的逻辑关系,分为AND和OR两种,默认为AND