核心组件
Shiro的架构主要包括Subject、SecurityManager、Realm和Session等核心组件。【原文链接】
- Subject:Subject代表当前用户,是与应用程序交互的主体。Subject可以执行身份验证、授权和会话管理等操作,是Shiro的核心概念。
- SecurityManager:SecurityManager是Shiro的核心组件,负责协调各个安全组件之间的交互。SecurityManager管理着所有的Subject,并负责对Subject进行身份验证、授权和会话管理等操作。
- Realm:Realm是Shiro与应用程序交互的桥梁,负责从数据源中获取用户的身份信息和权限信息。开发人员可以通过实现自定义的Realm来连接不同的数据源,如数据库、LDAP等。
- Session:Session代表用户的会话,用于存储用户的状态信息。Shiro提供了会话管理功能,可以管理用户的会话状态,包括会话的创建、销毁、超时等。
Shiro认证鉴权流程
- 客户端登录成功,生成会话信息保存在服务端(shiro-cache/redis),并返回token(token中携带sessionId信息)给客户端;
- 客户端访问服务端时,携带token;
- 服务端根据sessionId从缓存(shiro-cache/redis)中拿到会话信息;
- 客户端登出时(携带token),清除服务端相关会话信息;
认证鉴权
配置认证鉴权策略
shiro自带的策略:
- anno: 无需认证就可以访问(匿名会话)
- authc: 必须认证才能访问
- user: 必须拥有 记住我 功能才能用
- perms: 拥有对某个资源的权限才能访问
- role: 拥有某个角色权限才能访问
本文采用:
- 登录/获取验证码接口:无需认证(anno);
- 其他接口:全局配置自定义认证策略(添加了 JwtFilter认证过滤器,然后采用 注解方式 在
Controller层对需要鉴权的接口添加鉴权注解)
认证鉴权方式(请求头)
具体根据自身业务选择:
- Cookie
- token
本文采用:token
代码实现
shiro全局配置
import cn.com.Constant;
import cn.com.filter.JWTFilter;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
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.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.filter.mgt.FilterChainManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.*;
@Slf4j
@Configuration
public class ShiroConfig {
@Value("${shiro.cache.type}")
private String cacheType ;
@Value("${shiro.session-timeout}")
private long tomcatTimeout;
//shiro-redis提供的
@Resource
private RedisSessionDAO redisSessionDAO;
//shiro-redis提供的
@Resource
private RedisCacheManager redisCacheManager;
/**
* 管理shiro生命周期使用,默认配置就行了
*/
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 若要使用Shiro注解,需添加如下方法
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* shiro拦截器配置
*
*/
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(){
@Override
protected FilterChainManager createFilterChainManager() {
FilterChainManager manager = super.createFilterChainManager();
// URL携带中文出现400响应,servletPath中文校验bug
Map<String, Filter> filterMap = manager.getFilters();
Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
if (invalidRequestFilter instanceof InvalidRequestFilter) {
((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
}
return manager;
}
};
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 添加自定义认证过滤器
LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 添加自定义匹配指定路径的过滤器
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
/**
* anno: 无需认证就可以访问
* authc: 必须认证才能访问
* user: 必须拥有 记住我 功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
*/
filterChainDefinitionMap.put("/system/login/**","anon");
filterChainDefinitionMap.put("/system/logout", "anon");
filterChainDefinitionMap.put("/system/getVerify", "anon");
// 将过滤器应用于所有接口,自定义认证
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 自定义登录认证及授权
*/
@Bean
AccountRealm accountRealm() {
return new AccountRealm();
}
/**
* session缓存方式
*/
@Bean
public SessionDAO sessionDAO() {
if (Constant.CACHE_TYPE_REDIS.equals(cacheType)) {
return redisSessionDAO;
} else {
return new MemorySessionDAO();
}
}
/**
* shiro session的管理
*/
@Bean(name = "sessionManager")
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new CustomWebSessionManager();
sessionManager.setGlobalSessionTimeout(tomcatTimeout * 1000L);
// 设置SessionId不存于Cookie
sessionManager.setSessionIdCookieEnabled(false);
sessionManager.setSessionDAO(sessionDAO());
Collection<SessionListener> listeners = new ArrayList<>();
listeners.add(new BDSessionListener());
sessionManager.setSessionListeners(listeners);
return sessionManager;
}
/**
* shiro 安全管理器配置,shiro的容器管理包
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> realms = new ArrayList<>();
realms.add(accountRealm());
securityManager.setRealms(realms);
// 自定义缓存实现 使用redis
securityManager.setCacheManager(redisCacheManager);
securityManager.setSessionManager(sessionManager());
return securityManager;
}
}
自定义会话管理器
import cn.com.common.config.Constant;
import cn.com.common.utils.JWTUtils;
import cn.com.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
@Component
@Slf4j
public class CustomWebSessionManager extends DefaultWebSessionManager {
@Resource
private JWTUtils jwtUtils;
public MyWebSessionManager(){
super();
}
/**
* 重写: 从token中获取SessionId
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String sessionId = null;
HttpServletRequest req = WebUtils.toHttp(request);
String token = req.getHeader("token");
if(log.isTraceEnabled()){
log.trace("Token = {}", token);
}
// token不为空,从token中取出sessionId
if(StringUtils.isNotEmpty(token)){
sessionId = jwtUtils.getSessionIdFromToken(req);
if(StringUtils.isNotEmpty(sessionId)){
// 配置sessionId来源:url
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
// 标志sessionId是否有效
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
if(log.isTraceEnabled()){
log.trace("getSessionId = {} from Token = {}", sessionId, token);
}
}
}
// 设置是否启用 URL 重写来传递 。这是一个 Shiro 配置项,具体行为取决于 方法的实现。 `sessionId``isSessionIdUrlRewritingEnabled()`
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
return sessionId;
}
/**
* Shiro 创建新会话时调用,登录接口将token填充到response中。
*/
@Override
protected void onStart(Session session, SessionContext context) {
if (!WebUtils.isHttp(context)) {
log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response pair. No session ID cookie will be set.");
}else{
HttpServletRequest request = WebUtils.getHttpRequest(context);
HttpServletResponse response = WebUtils.getHttpResponse(context);
// 这个地方,shiro会给跳过认证(anno)的接口(形如/login登录接口/getVerity获取验证码接口)创建匿名会话
Serializable sessionId = session.getId();
this.storeSessionId(sessionId, request, response);
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
}
/**
* 作用: 登录接口,将SessionId生成Token,放入返回的响应头中
*/
private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
String uri = request.getRequestURI();
if (!uri.contains("/system/login/account")) {
return;
}
if (currentId == null) {
log.info("subject --> sessionId = null");
String msg = "sessionId cannot be null when persisting for subsequent requests.";
throw new IllegalArgumentException(msg);
}
String idString = currentId.toString();
String token = jwtUtils.generateToken(idString);
// 允许前端访问自定义请求头
response.setHeader("Access-Control-Expose-Headers", Constant.HEADER_TOKEN + ",newToken");
response.setHeader(Constant.HEADER_TOKEN, token);
}
}
登录认证及授权
import cn.com.common.config.ApplicationContextRegister;
import cn.com.common.config.Constant;
import cn.com.system.bean.perm.Account;
import cn.com.system.dao.MenuMapper;
import cn.com.system.plugins.UserAware;
import cn.com.system.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.List;
import java.util.Set;
@Slf4j
public class AccountRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
/**
* 多realm场景下这个判断才有用,当前只有一个realm
* 默认多realm场景下权限点会累加,加上判断后则只有一个realm的权限点(匹配上的)
*/
if(!principalCollection.getRealmNames().contains(getName())){
return null;
}
Long userId = null;
List list = principalCollection.asList();
if (list != null && !list.isEmpty()) {
userId = ((UserAware)list.get(0)).getUserId();
}
MenuMapper menuMapper = ApplicationContextRegister.getBean(MenuMapper.class);
Set<String> perms = menuMapper.listPermsByUserId(userId);
log.info("review perms: {}", perms.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(perms);
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
String password = new String((char[]) authenticationToken.getCredentials());
AccountService accountService = ApplicationContextRegister.getBean(AccountService.class);
// 查询用户信息
if(!accountService.checkExist(username)){
throw new UnknownAccountException("Account does not exist ! ");
}
Account user = accountService.getByName(username);
// 账号不存在
if (user == null) {
throw new UnknownAccountException("Account or Password is not correct ! ");
}
// 密码错误
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("Account or Password is not correct ! ");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
}
自定义认证过滤器JwtFilter
import cn.com.common.config.ApplicationContextRegister;
import cn.com.common.utils.JWTUtils;
import cn.com.common.utils.StringUtils;
import cn.com.common.config.shiro.ShiroUtil;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
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;
@Component
public class JWTFilter extends BasicHttpAuthenticationFilter {
/**
* 前置方法,配置支持跨域
*/
@Override
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 允许的来源域名
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
// 是否允许携带凭据(如 cookies)
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
// 允许的http请求方式
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
// 允许的自定义请求头
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 暴露给客户端的响应头
httpServletResponse.setHeader("Access-Control-Expose-Headers", "token,newToken");
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return true;
}
return super.onPreHandle(request, response, mappedValue);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue);
}
/**
* 是否认证失败
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = WebUtils.toHttp(request);
HttpServletResponse resp = WebUtils.toHttp(response);
// 从Header里面获取Token
String token = req.getHeader("token");
// 如果Header里面token为空,则从参数里面去取
if(StringUtils.isBlank(token)){
token = req.getParameter("token");
}
// 校验token
JWTUtils jwtUtils = ApplicationContextRegister.getBean(JWTUtils.class);
String sessionId = jwtUtils.getSessionIdFromToken(req);
if(StringUtils.isNotBlank(sessionId) && !ShiroUtil.validateSession(sessionId)){
return true;
}
// 认证失败
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
controller层添加鉴权注解
@Slf4j
@RestController
@RequestMapping("/account")
public class AccountController {
// 鉴权注解,只有拥有权限[account:get]才能访问该接口
@RequiresPermissions("account:get")
@GetMapping("/get")
BaseResponse<String> getAccount(String accountName) {
return null;
}
}

&spm=1001.2101.3001.5002&articleId=148395199&d=1&t=3&u=2972b11dc5d648be83cbaf4fd1f99174)
7296

被折叠的 条评论
为什么被折叠?



