Shiro--整合jwt--通过url路径控制权限--使用/教程/实例
原文网址:Shiro—整合jwt—通过url路径控制权限—使用/教程/实例_IT利刃出鞘的博客-CSDN博客
简介
说明
本文用实例介绍shiro的使用。采用jwt+url权限的方式控制权限。
- 使用jwt替代默认的authc作为认证方式,通过url路径控制授权。
- 其他尽量使用原生的shiro配置,尽量少自定义配置。
我自己自测通过,代码可用。
关联文章
本文是在此文章基础上进行修改,修改点如下:
数据库
- t_permission表的name字段改为url字段
- t_permission相应的插入的数据发生变化
Mapper
PermissionMapper
- 原先:返回t_permission.name字段
- 本文:返回t_permission.url字段
Controller
- 原先:使用@RequiresPermissions、@RequiresRoles控制权限
- 本文:去掉这两个注解,通过url控制权限
Shiro配置
注册授权过滤器(url路径过滤器)
- 提供url路径过滤器(UrlFiter类)
- 将url路径过滤器注册进去(ShiroConfig#shiroFilterFactoryBean)
修改认证过滤器(JwtFilter)
- 原先:只需通过返回true或者executeLogin即可
- 本文:自定义响应数据。(若不这么写,响应数据会很不友好,因为我们已经不是shiro的标准用法了)
除了上述修改之外,其他的代码、逻辑一点都没变。
修改点数据库
DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;
DROP TABLE IF EXISTS t_user;
DROP TABLE IF EXISTS t_role;
DROP TABLE IF EXISTS t_permission;
DROP TABLE IF EXISTS t_user_role_mid;
DROP TABLE IF EXISTS t_role_permission_mid;
CREATE TABLE t_user (
id bigint AUTO_INCREMENT,
user_name VARCHAR(100),
password VARCHAR(100),
salt VARCHAR(100),
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;
CREATE TABLE t_role (
id bigint AUTO_INCREMENT,
name VARCHAR(100),
description VARCHAR(100),
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;
CREATE TABLE t_permission (
id bigint AUTO_INCREMENT,
url VARCHAR(100),
description VARCHAR(100),
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;
CREATE TABLE t_user_role_mid (
id bigint AUTO_INCREMENT,
user_id bigint,
role_id bigint,
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;
CREATE TABLE t_role_permission_mid (
id bigint AUTO_INCREMENT,
role_id bigint,
permission_id bigint,
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;
-- 密码:12345
INSERT INTO `t_user` VALUES (1,"zhang3","a7d59dfc5332749cb801f86a24f5f590","e5ykFiNwShfCXvBRPr3wXg==");
-- 密码:abcde
INSERT INTO `t_user` VALUES (2,"li4","43e28304197b9216e45ab1ce8dac831b","jPz19y7arvYIGhuUjsb6sQ==");
INSERT INTO `t_role` VALUES (1,"admin","超级管理员");
INSERT INTO `t_role` VALUES (2,"productManager","产品管理员");
INSERT INTO `t_role` VALUES (3,"orderManager","订单管理员");
INSERT INTO `t_permission` VALUES (1,"/product/add","增加产品");
INSERT INTO `t_permission` VALUES (2,"/product/delete","删除产品");
INSERT INTO `t_permission` VALUES (3,"/product/edit","编辑产品");
INSERT INTO `t_permission` VALUES (4,"/product/view","查看产品");
INSERT INTO `t_permission` VALUES (5,"/order/add","增加订单");
INSERT INTO `t_permission` VALUES (6,"/order/delete","删除订单");
INSERT INTO `t_permission` VALUES (7,"/order/edit","编辑订单");
INSERT INTO `t_permission` VALUES (8,"/order/view","查看订单");
INSERT INTO `t_user_role_mid` VALUES (1,2,2);
INSERT INTO `t_user_role_mid` VALUES (2,1,1);
INSERT INTO `t_role_permission_mid` VALUES (1,1,1);
INSERT INTO `t_role_permission_mid` VALUES (2,1,2);
INSERT INTO `t_role_permission_mid` VALUES (3,1,3);
INSERT INTO `t_role_permission_mid` VALUES (4,1,4);
INSERT INTO `t_role_permission_mid` VALUES (5,1,5);
INSERT INTO `t_role_permission_mid` VALUES (6,1,6);
INSERT INTO `t_role_permission_mid` VALUES (7,1,7);
INSERT INTO `t_role_permission_mid` VALUES (8,1,8);
INSERT INTO `t_role_permission_mid` VALUES (9,2,1);
INSERT INTO `t_role_permission_mid` VALUES (10,2,2);
INSERT INTO `t_role_permission_mid` VALUES (11,2,3);
INSERT INTO `t_role_permission_mid` VALUES (12,2,4);
INSERT INTO `t_role_permission_mid` VALUES (13,3,5);
INSERT INTO `t_role_permission_mid` VALUES (14,3,6);
INSERT INTO `t_role_permission_mid` VALUES (15,3,7);
INSERT INTO `t_role_permission_mid` VALUES (16,3,8);
修改点代码
Shiro配置
添加Url过滤器(继承PathMatchingFilter)
package com.example.demo.config.shiro.filter;
import com.example.demo.common.entity.Result;
import com.example.demo.common.util.ApplicationContextHolder;
import com.example.demo.common.util.ResponseUtil;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.rbac.permission.service.PermissionService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Set;
@Slf4j
public class UrlFilter extends PathMatchingFilter {
private PermissionService permissionService;
@Override
protected boolean onPreHandle(ServletRequest request,
ServletResponse response,
Object mappedValue) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String uri = httpServletRequest.getRequestURI();
String token = httpServletRequest.getHeader(HttpHeaders.COOKIE);
String userIdStr = JwtUtil.getUserIdByToken(token);
Long userId = Long.parseLong(userIdStr);
if (permissionService == null) {
permissionService = ApplicationContextHolder.getContext().getBean(PermissionService.class);
}
Set<String> permissions = permissionService.getPermissionsByUserId(userId);
// 实际应该从数据库或者redis里通过userId获得拥有权限的url
if (permissions.contains(uri)) {
return true;
}
// 构造无权限时的response
HttpServletResponse httpResponse = (HttpServletResponse) response;
ResponseUtil.jsonResponse(httpResponse, HttpStatus.FORBIDDEN.value(),
"用户(" + userId + ")无此url(" + uri + ")权限");
return false;
}
}
注册Url过滤器
package com.example.demo.config.shiro;
import com.example.demo.common.constant.WhiteList;
import com.example.demo.config.shiro.filter.JwtFilter;
import com.example.demo.config.shiro.filter.UrlFilter;
import com.example.demo.config.shiro.realm.AccountRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Lazy
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果实现了AuthenticatingFilter.java要设置下边这个,因为shiro很多地方依据loginUrl进行判断
// shiroFilterFactoryBean.setLoginUrl("/login");
// // 登录成功后要跳转的链接
// shiroFilterFactoryBean.setSuccessUrl("/index");
// // 未授权界面;
// shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
WhiteList.ALL.forEach(str -> {
filterChainDefinitionMap.put(str, "anon");
});
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "jwtAuthc");
// 下边这行不要打开。原因待确定
// filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/**", "jwtAuthc,urlAuthz");
Map<String, Filter> customisedFilters = new LinkedHashMap<>();
//不能通过注入来设置过滤器。如果通过注入,则本过滤器优先级会最高(/**优先级最高,导致前边所有请求都无效)。
// springboot会扫描所有实现了javax.servlet.Filter接口的类,无需加@Component也会扫描到。
customisedFilters.put("jwtAuthc", new JwtFilter());
customisedFilters.put("urlAuthz", new UrlFilter());
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setFilters(customisedFilters);
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 关闭shiro自带的session。这样不能通过session登录shiro,后面将采用jwt凭证登录。
// 见:http://shiro.apache.org/session-management.html#SessionManagement-DisablingSubjectStateSessionStorage
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getDatabaseRealm());
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public AccountRealm getDatabaseRealm() {
return new AccountRealm();
}
/**
* setUsePrefix(true)用于解决一个奇怪的bug。如下:
* 在引入spring aop的情况下,在@Controller注解的类的方法中加入@RequiresRole等
* shiro注解,会导致该方法无法映射请求,导致返回404。加入这项配置能解决这个bug。
*/
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 开启shiro 注解。比如:@RequiresRole
* 本处不用此方法开启注解,使用引入spring aop依赖的方式。原因见:application.yml里的注释
*/
/*@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}*/
}
修改JwtFilter
package com.example.demo.config.shiro.filter;
import com.example.demo.common.util.ResponseUtil;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.JwtToken;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class JwtFilter extends AuthenticatingFilter {
/**
* 所有请求都会到这里来(无论是不是anon)。
* 返回true:表示允许向下走。后边会走PathMatchingFilter,看路径是否对应anon等
* 返回false:表示不允许向下走。
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest,
ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader(HttpHeaders.COOKIE);
// 自定义Header也可以,但浏览器不会存自定义的Header,需要前端自己去存
// String token = request.getHeader("Authentication");
if (!StringUtils.hasText(token)) {
return true;
} else {
boolean verified = JwtUtil.verifyToken(token);
if (!verified) {
HttpServletResponse response = (HttpServletResponse) servletResponse;
ResponseUtil.jsonResponse(response, HttpStatus.UNAUTHORIZED.value(), "认证失败");
return false;
}
}
// 此登录并非调用login接口,而是shiro层面的登录。
// 里边会调用下边的createToken方法
return executeLogin(servletRequest, servletResponse);
}
/**
* 这里的token会传给AuthorizingRealm子类(本处是AccountRealm)的doGetAuthenticationInfo方法作为参数
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest,
ServletResponse servletResponse) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader(HttpHeaders.COOKIE);
// 自定义Header也可以,但浏览器不会存自定义的Header,需要前端自己去存
// String token = request.getHeader("Authentication");
if (!StringUtils.hasText(token)) {
return null;
}
return new JwtToken(token);
}
}
工具类ResponseUtil
package com.example.demo.common.util;
import com.example.demo.common.entity.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletResponse;
public class ResponseUtil {
public static void jsonResponse(HttpServletResponse response, int status, String message) throws Exception {
//让浏览器用utf8来解析返回的数据
response.setHeader("Content-type", "application/json;charset=UTF-8");
//告诉servlet用UTF-8转码,而不是用默认的ISO8859
response.setCharacterEncoding("UTF-8");
response.setStatus(status);
Result result = new Result().failure().message(message);
String json = new ObjectMapper().writeValueAsString(result);
response.getWriter().print(json);
}
}
Mapper
修改SQL
package com.example.demo.rbac.permission.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.rbac.permission.entity.Permission;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.Set;
@Repository
public interface PermissionMapper extends BaseMapper<Permission> {
@Select("SELECT " +
" t_permission.`url` " +
"FROM " +
" t_user, " +
" t_user_role_mid, " +
" t_role, " +
" t_role_permission_mid, " +
" t_permission " +
"WHERE " +
" t_user.`id` = #{userId} " +
" AND t_user.id = t_user_role_mid.user_id " +
" AND t_user_role_mid.role_id = t_role.id " +
" AND t_role.id = t_role_permission_mid.role_id " +
" AND t_role_permission_mid.permission_id = t_permission.id")
Set<String> getPermissionsByUserId(@Param("userId") Long userId);
}
Controller
产品
package com.example.demo.business.product;
import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "产品")
@RestController
@RequestMapping("product")
public class ProductController {
@ApiOperation(value="增加产品")
@PostMapping("add")
public Result add() {
return new Result<>().message("product:add success");
}
@ApiOperation(value="删除产品")
@PostMapping("delete")
public Result delete() {
return new Result<>().message("product:delete success");
}
@ApiOperation(value="编辑产品")
@PostMapping("edit")
public Result edit() {
return new Result<>().message("product:edit success");
}
@ApiOperation(value="查看产品")
@GetMapping("view")
public Result view() {
return new Result<>().message("product:view success");
}
}
订单
package com.example.demo.business.order;
import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "订单")
@RestController
@RequestMapping("order")
public class OrderController {
@ApiOperation(value="增加订单")
@PostMapping("add")
public Result add() {
return new Result<>().message("order:add success");
}
@ApiOperation(value="删除订单")
@PostMapping("delete")
public Result delete() {
return new Result<>().message("order:delete success");
}
@ApiOperation(value="编辑订单")
@PostMapping("edit")
public Result edit() {
return new Result<>().message("order:edit success");
}
@ApiOperation(value="查看订单")
@GetMapping("view")
public Result view() {
return new Result<>().message("order:view success");
}
}
测试
测试超级管理员(admin)
启动项目,访问:http://localhost:8080/doc.html
1.测试登录
- 登录成功
- 可以看到,会返回来一个Set-Cookie头,值是token。
2.测试有资源权限的接口
本处测试增加产品接口。
- 成功访问。
- 在请求时会传递Cookie
我使用标准的:Set-Cookie,Cookie来做认证的。若是自定义的header,需要手动写入:
3.测试登出
4.再次访问接口
- 访问成功。
- 因为token还没过期,浏览器也还会将其发给服务端,所以成功。
测试产品管理员(productManager)
启动项目,访问:http://localhost:8080/doc.html
1.测试登录
- 登录成功
- 可以看到,会返回来一个Set-Cookie头,值是token。
2.测试有资源权限的接口
本处测试增加产品接口。
- 成功访问。
- 在请求时会传递Cookie
3.测试无资源权限的接口
本处测试增加订单接口。
- 访问失败。
- 在请求时会传递Cookie
- 有一处细节:提示是红色的。
点进去看,可以看到状态码是我指定的:403
重启服务再请求
1.登录
登录成功
2.重启服务器
重启Idea启动的应用。
3.访问有权限的接口
本处访问产品增加接口。
- 可以看到,访问成功。
超时后再请求
1.修改配置文件,暂时将token过期时间改短(本处改为10秒)
application.yml
2.登录
3.等待大于10秒之后再请求
请求失败。
我代码里指定这种错误为401,点进去验证下:
其他网址
Shiro实例系列
Shiro—SpringBoot—Session—使用/用法/实例/示例_IT利刃出鞘的博客-CSDN博客
Shiro—SpringBoot—shiro-redis—使用/用法/实例/示例_IT利刃出鞘的博客-CSDN博客
Shiro—SpringBoot—jwt—使用/用法/实例/示例_IT利刃出鞘的博客-CSDN博客
Shiro—SpringBoot—jwt—url路径(PathMatchingFilter)/通过url路径控制权限—使用/用法/实例/示例_IT利刃出鞘的博客-CSDN博客
还没有评论,来说两句吧...