springboot shiro 权限管理
版本 :
- springboot 2.3.4-RELEASE
- shiro 1.6.0
注: 此版本基于 ssm 与 shiro 下的权限管理_XGLLHZ的博客-优快云博客 文章(有些小问题,目前未修改),关联阅读便于理解。
核心组件 :
- Subject 一般指用户
- SecurityManager 安全管理器,管理所有 subject
- SessionManager 会话管理器,管理用户登录后的 session
- Authentication 认证
- Authorization 鉴权
- CacheManager 缓存管理器,管理缓存(缓存可自定义)
流程图 :

TokenFilter :
// token filter(请求先进入此拦截器)
public class TokenFilter extends AbstractFilter {
private static final Logger logger = LogManager.getLogger(TokenFilter.class);
private final SYSUserService userService;
public TokenFilter(SYSUserService userService) {
this.userService = userService;
}
/**
* 此拦截器工作流程:
* 1、判断是否携带 token
* 2、判断 token 是否过期
* 3、判断用户登录是否过期(即 session 中是否存在用户信息)
* 4、在 token 未过期且用户登录过期的情况刷新用户 session(系统内部自动登录)
*
* 此处做自动登录的目的:
* 在 spring-shiro 中,用户登录信息是放在 session 中的,
* 而 session 默认有效时间为半小时,也就是说默认情况下,
* 用户每隔半小时就得登录一次,针对这个问题有两种解决方法:
* 1、session 有效时长设置为无限长(缺点是当用户数量过大时会严重占用系统内存)
* 2、利用 token 机制结合 session(有效时长可设两小时),当 token 有效 session
* 失效时,刷新 session 中的用户信息,当 token 过期时提示用户重新登录
*
* @param servletRequest
* @param servletResponse
* @return
* @throws IOException
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws ServletException, IOException {
logger.info("enter TokenFilter.doFilter() params = {}", servletRequest.toString());
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setContentType("application/json; charset=utf-8");
String token = request.getHeader("token");
// 若 token 为空则说明该请求为非法请求或不需要权限的请求
if (StringUtils.isNotBlank(token)) {
// 若 token 过期则返回响应体
if (TokenUtil.checkToken(token) == 1) {
ObjectMapper mapper = new ObjectMapper();
PrintWriter writer = response.getWriter();
writer.write(mapper.writeValueAsString(new APIResponse<>(ConstConfig.RE_LOGIN_EXPIRE_CODE,
ConstConfig.RE_LOGIN_EXPIRE_MSG)));
writer.flush();
writer.close();
return;
}
// 若 token 未过期则判断内存中是否有此用户身份验证信息
Object object = null;
try {
object = SecurityUtils.getSubject().getPrincipal();
} catch (Exception e) {
logger.error("The userInfo is overdue in session!");
}
// 若没有此用户身份验证信息则根据 token 获取用户信息并调用 shiro 中的 subject.login()
// 类似于系统内部自动登录(刷新 session 中的用户信息)
if (object == null) {
String username = userService.getUsernameByToken(token);
if (StringUtils.isBlank(username)) {
ObjectMapper mapper = new ObjectMapper();
PrintWriter writer = response.getWriter();
writer.write(mapper.writeValueAsString(new APIResponse<>(ConstConfig.RE_CHECK_TOKEN_ERROR_CODE,
ConstConfig.RE_CHECK_TOKEN_ERROR_MSG)));
writer.flush();
writer.close();
return;
}
SYSUserPo userPo = userService.getUserByUsername(username);
if (userPo == null) {
ObjectMapper mapper = new ObjectMapper();
PrintWriter writer = response.getWriter();
writer.write(mapper.writeValueAsString(new APIResponse<>(ConstConfig.SERVER_EXCEPTION_CODE,
"User data does not exist")));
writer.flush();
writer.close();
return;
}
// 调用 shiro 中的登录方法
UsernamePasswordToken usernamePasswordToken =
new UsernamePasswordToken(userPo.getUsername(), userPo.getPassword());
Subject subject = SecurityUtils.getSubject();
subject.login(usernamePasswordToken);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
ShiroRealm :
// 重写 shiro 父类中鉴权和认证的方法,以实现具体逻辑
public class ShiroRealm extends AuthorizingRealm {
private static final Logger logger = LogManager.getLogger(ShiroRealm.class);
private final SYSUserService userService;
public ShiroRealm(SYSUserService userService) {
this.userService = userService;
}
/**
* 鉴权
* 获取权限校验需要的信息(当前登录用户所具有的权限信息:权限、角色)
* 调用时间: 1、默认情况下是每次请求资源(url)时都会调用;
* 2、但可以将用户权限(资源)信息存放到缓存中,存放方式是在 subject.login()
* 之后调用 subject.isPermitted("test")方法,在这个方法内部会调用下面的
* 方法来获取用户权限(资源)信息,同时需要在 shiro 配置文件中配置相应的缓存管理器;
* 3、放入缓存之后只在每次登录时调用;
* 检验方式: shiro 的权限校验方式有两种,即基于权限(资源)、基于角色
* 此项目采用基于权限(资源)的方式
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("enter ShiroRealm.doGetAuthorizationInfo() authority");
// 获取登录成功的用户名
// 这里会从 session 中获取(登录执行 subject.login 方法时会将用户信息放入 session)
String username = (String) principalCollection.fromRealm(getName()).iterator().next();
// 根据用户名获取该用户所具有的权限列表
SYSUserPo sysUserPo = new SYSUserPo();
sysUserPo.setUsername(username);
List<SYSPermPo> permList = userService.listPermByUser(sysUserPo);
List<String> perms = null;
if (permList != null && permList.size() != 0) {
perms = permList.stream().map(SYSPermPo::getPermUrl).collect(Collectors.toList());
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(perms);
return info;
}
/**
* 认证
* 获取身份验证需要的信息(数据库中的用户数据)
* 调用时间: 登录时调用,即请求 /admin/user/login时(service 中 的 subject.login())
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
logger.info("enter ShiroRealm.doGetAuthenticationInfo() authenticate");
// 用户登录提交的信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
if (StringUtils.isBlank(username)) {
throw new GlobalException(ConstConfig.RE_NAME_OR_PASSWORD_ERROR_CODE,
ConstConfig.RE_NAME_OR_PASSWORD_ERROR_MSG);
}
// 根据用户名查询到的信息
SYSUserPo userPo = userService.getUserByUsername(username);
if (userPo == null) {
throw new GlobalException(ConstConfig.SERVER_EXCEPTION_CODE, "User data does not exist");
}
return new SimpleAuthenticationInfo(userPo.getUsername(), userPo.getPassword(), getName());
}
}
ShiroConfig :
// shiro config
@Component
public class ShiroConfig {
public static final String PERMISSION_STRING = "perms[\"{0}\"]";
private final SYSPermMapper permMapper;
private final SYSUserService userService;
public ShiroConfig(SYSPermMapper permMapper, SYSUserService userService) {
this.permMapper = permMapper;
this.userService = userService;
}
/**
* 自定义实现 ShiroFilterFactoryBean
*
* 关于 shiro 中的 filterChains(过滤链)
*
* 1、map 中的 key 表示要拦截的请求 url,value 表示处理此 url 所使用的拦截器,
* 其中 value 的值可以是 shiro 提供的拦截器也可以是自定义拦截器
*
* 2、value 的值可以为多个(以逗号隔开),表示该 url(key) 要被多个拦截器处理,
* 其中执行顺序为 value 中的顺序,如: filterChains.put("/admin/user/list", "tokenFilter,authc")
* 表示 /admin/user/list 请求会依次经过 tokenFilter(自定义)、authc 拦截器
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// loginUrl 在前后端分离下,此处应为返回登录提示的接口 api
// 即,当系统发现当前请求地址需要授权才可访问时返回登录提示
factoryBean.setLoginUrl("/admin/user/login_code");
// unAuthorizedUrl 在前后端分离下,此处应为鉴权失败后返回权限不足的接口 api
// 即,当鉴权失败后返回权限不足提示
factoryBean.setUnauthorizedUrl("/admin/user/authorizingFail");
// 自定义拦截器 token 校验
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("tokenFilter", new TokenFilter(userService));
factoryBean.setFilters(filters);
Map<String, String> filterChains = new LinkedHashMap<>(); // 承载过滤链的变量
// 从数据库中获取权限(资源)url 及其对应的 roleList
// 将存在 roleList 的 url 加入到过滤链中,因为 sys_perm 表中放的是所有资源的 url,
// 其中有些资源不需要权限就可以访问,所以不需要放到过滤链中
// 凡是放到过滤链中的 url 都会被拦截
List<SYSPermPo> list = permMapper.allUrlRole();
if (list != null && list.size() != 0) {
list.stream().peek(a -> {
if (StringUtils.isNoneBlank(a.getPermUrl()) && !a.getRoleList().isEmpty()) {
filterChains.put(a.getPermUrl(), "tokenFilter,"
+ MessageFormat.format(PERMISSION_STRING, a.getPermUrl()));
}
}).collect(Collectors.toList());
}
factoryBean.setFilterChainDefinitionMap(filterChains);
return factoryBean;
}
@Bean
public SecurityManager securityManager(ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);
return securityManager;
}
@Bean
public ShiroRealm shiroRealm() {
return new ShiroRealm(userService);
}
}
shiro filter :
| 标识 | 名称 | 优先级 | 说明 | 对应类 |
|---|---|---|---|---|
| anon | 匿名拦截器 | 1 | 不需要登录即可访问,一般用于静态资源 | AnonymousFilter |
| authc | 登录拦截器 | 2 | 需要登陆才可访问 | FormAuthenticationFilter |
| authcBasicHttp | Http拦截器 | 3 | Http 拦截器 非常用类型 | BasicAuthenticationFilter |
| logout | 登出拦截器 | 4 | 用户登出拦截器 主要属性 redirectUrl 登出后重定向地址 | LogoutFilter |
| noSessionCreation | 不创建会话拦截器 | 5 | 没用过 | NoSessionCreationFilter |
| perms | 权限拦截器 | 6 | 验证用户是否有权限访问资源 | PermissionAuthenticationFilter |
| port | 端口拦截器 | 7 | 拦截端口,针对指定端口做出重定向 | PortFilter |
| rest | rest 风格拦截器 | 8 | 根据 rest 风格 url 构建权限 非常用 | HttpMethodPermissionFilter |
| roles | 角色拦截器 | 9 | 验证用户是否有角色访问资源 | RolesAuthorizationFilter |
| ssl | ssl 拦截器 | 10 | 拦截非 https 请求,并跳转到 443 | SslFilter |
| user | 用户拦截器 | 11 | 用户认证通过或开启记住我功能 | SslFilter |
RefreshFilterChains :
// shiro 过滤链是在服务启动时从数据库加载到内存中的
// 则当数据库权限数据发生变化时需要刷新内存中的过滤链
// 在添加修改角色权限相关方法中使用 refreshFilterChains.refreshFilterChains() 刷新过滤链
@Component
public class RefreshFilterChains {
private static final Logger logger = LogManager.getLogger(RefreshFilterChains.class);
public static final String PERMISSION_STRING = "perms[\"{0}\"]";
private final ShiroFilterFactoryBean shiroFilterFactoryBean;
private final SYSPermMapper permMapper;
public RefreshFilterChains(ShiroFilterFactoryBean shiroFilterFactoryBean, SYSPermMapper permMapper) {
this.shiroFilterFactoryBean = shiroFilterFactoryBean;
this.permMapper = permMapper;
}
/**
* 刷新过滤链(线程安全)
*/
public void refreshFilterChains() {
logger.info("refresh shiro filter chains");
synchronized (shiroFilterFactoryBean) {
AbstractShiroFilter filter;
try {
// 获取 shiro 拦截器实例
filter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
// 获取路径匹配过滤链解析器实例
PathMatchingFilterChainResolver resolver = (PathMatchingFilterChainResolver) filter.getFilterChainResolver();
// 获取默认过滤链管理器
DefaultFilterChainManager manager = (DefaultFilterChainManager) resolver.getFilterChainManager();
// 清除过滤链
manager.getFilterChains().clear();
shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
Map<String, String> filterChains = new LinkedHashMap<>();
List<SYSPermPo> list = permMapper.allUrlRole();
if (list != null && list.size() != 0) {
list.stream().peek(a -> {
if (StringUtils.isNoneBlank(a.getPermUrl()) && !a.getRoleList().isEmpty()) {
filterChains.put(a.getPermUrl(), "tokenFilter,"
+ MessageFormat.format(PERMISSION_STRING, a.getPermUrl()));
}
}).collect(Collectors.toList());
}
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChains);
// 重新生成过滤链
Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(filterChainDefinitionMap)) {
filterChains.forEach((key, value) -> {
manager.createChain(key, value.replace(" ", ""));
});
}
logger.info("The refreshed filter chains is {}", filterChainDefinitionMap.toString());
} catch (Exception e) {
logger.error("Failed refresh shiro filter chains");
e.printStackTrace();
}
}
}
}
完结 撒花 庆祝
人生何处不相逢.mp3-娴公主
本文介绍了一个基于SpringBoot 2.3.4和Shiro 1.6.0的权限管理系统,详细阐述了Shiro的核心组件,包括Subject、SecurityManager、SessionManager、Authentication和Authorization。流程涉及TokenFilter的自动登录逻辑,以及ShiroRealm中自定义的鉴权和认证方法。此外,还展示了ShiroConfig的配置,包括过滤链的设置和刷新。整个系统实现了用户权限的管理和刷新,确保了安全性。
548

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



