SpringBoot入门三十三,整合Shiro+JWT实现权限控制
  PDnN7dVP3WHp 2023年11月02日 34 0

引言

Spring Boot是一款快速开发的Java框架,而Shiro是一款强大的安全框架,JWT(JSON Web Token)是一种用于身份验证和授权的开放标准。本文将介绍如何使用Spring Boot整合Shiro和JWT,实现权限控制的功能。通过本文的学习,新手可以了解到如何搭建一个安全可靠的Web应用。

一、搭建Spring Boot项目

参考《SpringBoot入门一,使用MyEclipse新建一个SpringBoot项目》,使用MyEclipse新建一个SpringBoot项目即可。现在来给项目添加shiro支持,数据暂时硬编码,不连接数据库,SpringBoot的版本采用2.4.0。

二、添加pom.xml信息

pom.xml添加以下配置信息,引入fastjson是以为有使用到fastjson的地方,如果使用其他json包,替换即可

<!-- 8.引入fastjson支持 -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>2.0.40</version>
</dependency>
		
<!-- 9.开启shiro依赖 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.7.0</version>
</dependency>

<!-- 10.引入jjwt依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

pom.xml完整文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.qfx.springboot</groupId>
	<artifactId>qfxSpringbootShiroJwtDemo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>qfxSpringbootShiroJwtDemo</name>
	<description />

	<!-- 设置父类,整合第三方常用框架依赖信息(各种依赖信息) -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.4.0</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

	<!-- 设置公共参数 -->
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<!-- Maven install 时,测试环境@Test中如果有中文输出是乱码,加上这句话试试 -->
		<argLine>-Dfile.encoding=UTF-8</argLine>
		<!-- Maven编译时的编码 -->
		<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
		<!-- 编译打包时关掉单元测试 -->
		<skipTests>true</skipTests>
		
		<!-- jdk版本 -->
		<java.version>1.8</java.version>
	</properties>


	<dependencies>
		<!-- 1.开启springboot核心包,整合SpringMVC Web组件 -->
		<!-- 实现原理:Maven依赖继承关系,相当于把第三方常用Maven依赖信息,在parent项目中已经封装好了 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- 2.引入fastjson支持 -->
		<dependency>
		   <groupId>com.alibaba</groupId>
		   <artifactId>fastjson</artifactId>
		   <version>2.0.40</version>
		</dependency>
		
		<!-- 3.开启shiro依赖 -->
 		<dependency>
		    <groupId>org.apache.shiro</groupId>
		    <artifactId>shiro-spring-boot-starter</artifactId>
		    <version>1.7.0</version>
		</dependency>
		
		<!-- 4.引入jjwt依赖 -->
		<dependency>
		    <groupId>io.jsonwebtoken</groupId>
		    <artifactId>jjwt</artifactId>
		    <version>0.9.1</version>
		</dependency>
	</dependencies>
	
	<build>
		<!-- 指定war包名称,以此处为准,否则会带上版本号 -->
		<finalName>${project.name}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId> 
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
					<!-- spring热部署 -->
					<dependency>
						<groupId>org.springframework</groupId>
						<artifactId>springloaded</artifactId>
						<version>1.2.8.RELEASE</version>
					</dependency>
				</dependencies>
            </plugin>
        </plugins>
    </build>
    
</project>

三、模拟用户及权限信息(可选)

本示例将用户信息和权限信息进行硬编码,实际应用是应从数据库或缓存中进行获取

3.1 存放用户缓存数据

import java.util.HashMap;
import java.util.Map;

import com.qfx.demo.vo.SysUser;

/**
 * @功能描述: 存放用户缓存数据
 */
public class UserCache {
	// 存放公共信息缓存数据
	public static Map<String, SysUser> userCacheMap = new HashMap<String, SysUser>();

	static{
		System.out.println("初始化用户信息开始...");
		
		// 默认密码为 111111
		SysUser user = new SysUser();
		user.setUserName("zhangsan");
		user.setPassWord("111111");
		user.setRoleName("超级管理员");
		
		// 默认密码为 111111
		SysUser user2 = new SysUser();
		user2.setUserName("lisi");
		user2.setPassWord("111111");
		user2.setRoleName("游客");
		
		// 默认密码为 111111
		SysUser user3 = new SysUser();
		user3.setUserName("wangwu");
		user3.setPassWord("111111");
		user3.setRoleName("网管"); 
		
		// 默认密码为 111111
		SysUser user4 = new SysUser();
		user4.setUserName("linlin");
		user4.setPassWord("111111");
		user4.setRoleName("游客"); 
		
		setUserCacheMap(user.getUserName(), user);
		setUserCacheMap(user2.getUserName(), user2);
		setUserCacheMap(user3.getUserName(), user3);
		setUserCacheMap(user4.getUserName(), user4);
		
		System.out.println("初始化用户信息完毕!");
	}

	public static SysUser getUserCacheMap(String key) {
		return userCacheMap.get(key);
	}

	public static void setUserCacheMap(String key, SysUser user) {
		userCacheMap.put(key, user);
	}
}

3.2 存放菜单角色信息缓存数据

这里有两个菜单角色信息缓存数据的文件,是因为模拟动态刷新权限时可以使用到,可以更好的测试动态刷新权限是否成功。

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_springboot

3.2.1 菜单角色信息缓存数据01

import java.util.HashMap;
import java.util.Map;

import com.qfx.demo.vo.SysMenuRole;

/**
 * @功能描述: 存放菜单角色信息缓存数据
 */
public class MenuRoleCache {
	// 存放公共信息缓存数据
	public static Map<String, SysMenuRole> menuRoleCacheMap = new HashMap<String, SysMenuRole>();

	static{
		System.out.println("初始化菜单角色信息...");
		
		SysMenuRole menuRole = new SysMenuRole();
		menuRole.setMenuName("/user/**");
		menuRole.setRoleNames("\"游客,超级管理员\"");
		
		SysMenuRole menuRole2 = new SysMenuRole();
		menuRole2.setMenuName("/role/**");
		menuRole2.setRoleNames("\"网管,超级管理员\"");
		
		setMenuRoleCacheMap(menuRole.getMenuName(), menuRole);
		setMenuRoleCacheMap(menuRole2.getMenuName(), menuRole2);
		
		System.out.println("初始化菜单角色信息完毕!");
	}

	public static SysMenuRole getMenuRoleCacheMap(String key) {
		return menuRoleCacheMap.get(key);
	}

	public static void setMenuRoleCacheMap(String key, SysMenuRole menu) {
		menuRoleCacheMap.put(key, menu);
	}
}

3.2.2 菜单角色信息缓存数据02

import java.util.HashMap;
import java.util.Map;

import com.qfx.demo.vo.SysMenuRole;

/**
 * @功能描述: 存放菜单角色信息缓存数据
 */
public class MenuRoleCache2 {
	// 存放公共信息缓存数据
	public static Map<String, SysMenuRole> menuRoleCacheMap = new HashMap<String, SysMenuRole>();

	static{
		System.out.println("初始化菜单角色信息...");
		
		SysMenuRole menuRole = new SysMenuRole();
		menuRole.setMenuName("/user/**");
		menuRole.setRoleNames("\"网管,超级管理员\"");
		
		SysMenuRole menuRole2 = new SysMenuRole();
		menuRole2.setMenuName("/role/**");
		menuRole2.setRoleNames("\"游客,超级管理员\"");
		
		setMenuRoleCacheMap(menuRole.getMenuName(), menuRole);
		setMenuRoleCacheMap(menuRole2.getMenuName(), menuRole2);
		
		System.out.println("初始化菜单角色信息完毕!");
	}

	public static SysMenuRole getMenuRoleCacheMap(String key) {
		return menuRoleCacheMap.get(key);
	}

	public static void setMenuRoleCacheMap(String key, SysMenuRole menu) {
		menuRoleCacheMap.put(key, menu);
	}
}

四、添加通用工具类

4.1 JWT工具类

import java.security.Key;
import java.util.Date;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;

public class ToolToken {

	/**
	 * 签名秘钥
	 * 可以换成 秘钥 注入,长度最好不要小于50,否则可能会报"The signing key's size is 24 bits which is not secure enough for the HS256 a"的异常
	 */
	public static final String SECRET = "your-secret-key";
	/**
	 * 发行者信息
	 */
	public static final String ISSUER = "your-issuer-info";
	/**
	 * 有效期(秒)
	 */
//	public static final long TTL_MILLIS = 3600 * 24 * 30;
	public static final long TTL_MILLIS = -1;	// 默认永久有效

	/**
	 * 生成token
	 *
	 * @param id 一般传入userName
	 * @param subject 该JWT所面向的用户
	 * @return
	 */
	public static String createJwtToken(String id, String subject) {
		return createJwtToken(id, ISSUER, subject, TTL_MILLIS);
	}

  	/**
	 * 生成token
	 *
	 * @param id 一般传入userName
	 * @return
	 */
	public static String createJwtToken(String id) {
		return createJwtToken(id, ISSUER, "", TTL_MILLIS);
	}

	/**
	 * 生成Token
	 *
	 * @param id        编号
	 * @param issuer    该JWT的签发者,是否使用是可选的
	 * @param subject   该JWT所面向的用户,是否使用是可选的;
	 * @param ttlMillis 有效时间(秒,过期会报错)
	 * @return token String
	 */
	public static String createJwtToken(String id, String issuer, String subject, long ttlMillis) {

		// 签名算法 ,将对token进行签名
		SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

		// 生成签发时间
		long nowMillis = System.currentTimeMillis();
//		Date now = new Date(nowMillis);

		// 通过秘钥签名JWT
		byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
		Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

		// 让我们设置JWT声明
		JwtBuilder builder = Jwts.builder().setId(id)
//				.setIssuedAt(now)	//暂不开放签发时间
				.setSubject(subject).setIssuer(issuer)
				.signWith(signatureAlgorithm, signingKey);

		// 如果指定了有效期,那么让我们添加过期时间
		if (ttlMillis >= 0) {
			long expMillis = nowMillis + ttlMillis * 1000;
			Date exp = new Date(expMillis);
			builder.setExpiration(exp);
			
			System.out.println("JWT有效期至:" + ToolDate.dateTimeToString(exp));
		}

		// 构建JWT并将其序列化为一个紧凑的url安全字符串
		return builder.compact();

	}

	/**
	 * Token解析方法
	 * 
	 * @param jwt Token
	 * @return
	 */
	public static Claims parseJWT(String jwt) {
		Claims claims = null;
		// 如果这行代码不是签名的JWS(如预期),那么它将抛出异常
		try {
			claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SECRET)).parseClaimsJws(jwt).getBody();
			String issuer 	= claims.getIssuer();
			if (!ISSUER.equals(issuer)) {
				claims = null;
				System.out.println("JWT的签发地不在范围内");
			}
		} catch(ExpiredJwtException e) {
			System.out.println("JWT 已过期!");
		} catch (Exception e) {
			System.out.println("JWT解析失败," + e.getMessage());
		}
		return claims;
	}
	
    /**
     * 验证是否通过
     * @param jwt
     * @return
     */
    public static boolean validateToken(String jwt) {
        try {
        	Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SECRET)).parseClaimsJws(jwt).getBody();
            return true;
        } catch (SignatureException ex) {
            System.out.println("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            System.out.println("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            System.out.println("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            System.out.println("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            System.out.println("JWT claims string is empty.");
        }
        return false;
    }
	
//	public static void main(String[] args) {
//		String token = ToolToken.createJwtToken("test","ceshi");
//		System.out.println(token);
//		Claims claims = ToolToken.parseJWT(token);
//		System.out.println("--1" + claims.getSubject());
//		System.out.println("--2" + claims);
//	}
}

4.2 接口通用返回结构类

import java.util.Date;

/**
 * @功能描述: JSON模型 用户后台向前台返回的JSON对象
 */
public class MessageBean {
	private int code = 200;	// 返回编码
	private String message = "";	// 提示信息
	private Object data = null;		// 其他信息
	private Long time = new Date().getTime();
	
	public MessageBean() {
		super();
	}

	public MessageBean(String message) {
		super();
		this.message = message;
	}

	public MessageBean(Object data) {
		super();
		this.data = data;
	}

	public MessageBean(int code, String message) {
		super();
		this.code = code;
		this.message = message;
	}

	public MessageBean(String message, Object data) {
		super();
		this.message = message;
		this.data = data;
	}

	public MessageBean(int code, String message, Object data) {
		super();
		this.code = code;
		this.message = message;
		this.data = data;
	}

	public int getCode() {
		return code;
	}

	public void setCode(int code) {
		this.code = code;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	public Object getData() {
		return data;
	}

	public void setData(Object data) {
		this.data = data;
	}

	public Long getTime() {
		return time;
	}

	public void setTime(Long time) {
		this.time = time;
	}
}

五、整合shiro+jwt

5.1 新增自定义token实体类

import org.apache.shiro.authc.AuthenticationToken;

/**
 * 自定义token实体类
 *
 */
public class JwtToken implements AuthenticationToken {
	
	private static final long serialVersionUID = 1L;
	
	private String token;
	
	public JwtToken(String token) {
        this.token = token;
    }

	@Override
	public Object getPrincipal() {
		return token;
	}

	@Override
	public Object getCredentials() {
		return token;
	}
}

5.2 新增JWT过滤器

5.2.1 代码解释

这段代码实现了一个基于JWT的身份验证过滤器,它扩展了Shiro框架的BasicHttpAuthenticationFilter类。该过滤器用于验证请求是否携带有效的JWT令牌,并执行基本身份验证逻辑。在这段代码中,主要包含以下几个方法:

isAccessAllowed方法:该方法用于判断当前用户是否有权访问指定的URL或资源。它首先验证请求头中是否携带了有效的Authorization信息(即JWT令牌),如果携带了有效的令牌,则执行基本身份验证登录逻辑(executeLogin方法),返回true表示允许访问;否则,返回false表示禁止访问

executeLogin方法:该方法执行基本身份验证登录逻辑。它从HTTP头中获取Authorization头的值(即JWT令牌),创建JwtToken对象,并将该令牌提交给Shiro的Realm进行登录校验。如果校验成功,返回true表示登录成功;否则,将抛出异常

此外,代码中还包含了一些处理异常和返回响应的逻辑,例如在isAccessAllowed方法中,如果请求头中没有携带有效的Authorization信息(即JWT令牌),则返回一个包含未登录提示信息的JSON响应

注释部分的代码是关于跨域支持的一些配置,根据需要可以取消注释或使用其他方式实现跨域。

5.2.2 示例代码

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.util.StringUtils;

import com.alibaba.fastjson.JSONObject;
import com.qfx.demo.vo.MessageBean;

public class CustomJwtFilter extends BasicHttpAuthenticationFilter {

	/**
	 * 该方法用于判断当前用户是否有权访问指定的 URL 或资源。如果返回 true,则表示允许访问;否则,表示禁止访问
	 * 1. 返回true,shiro直接通过验证
     * 2. 返回false,shiro会根据onAccessDenied的方法的返回值决定是否允许访问url
	 */
	@Override
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
		System.out.println("--------------------- 第一步、验证是否携带token信息 ---------------------");
		response.setContentType("application/json;charset=utf-8");
		MessageBean messageBean = new MessageBean();
		
		try {
			// 1.从HTTP头中获取Authorization头的值
			String token = getAuthzHeader(request);
	        
			// 2.验证请求头是否携带"Authorization"信息
			if(StringUtils.hasLength(token) && ! "null".equals(token.toLowerCase())) {
				// 3.执行基本身份验证登录逻辑
				boolean executeLogin = executeLogin(request, response);
				return executeLogin;
			} else {
				messageBean.setCode(HttpServletResponse.SC_UNAUTHORIZED);
				messageBean.setMessage("您还没有登录,请进行登录!");
				
				response.getWriter().print(JSONObject.toJSONString(messageBean));
			}
		} catch (Exception e) {
			try {
				messageBean.setCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
				messageBean.setMessage(e.getMessage());
				response.getWriter().print(JSONObject.toJSONString(messageBean));
			} catch (Exception e2) {
				e2.printStackTrace();
			}
		}
		return false;
	}

	/**
	 * 执行基本身份验证登录逻辑
	 */
	@Override
	protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
		// 从HTTP头中获取Authorization头的值
		String tokenStr = getAuthzHeader(request);
		
		JwtToken jwtToken = new JwtToken(tokenStr);
        // 提交给realm进行登录校验,将会调用JwtRealm中的doGetAuthenticationInfo方法进行登录验证
        // 如果校验失败会抛出异常,而异常会在我们的全局异常捕获类ExceptionController中捕获
        getSubject(request, response).login(jwtToken);
        
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
	}

//	/**
//	 * 添加跨域支持,也可以@CrossOrigin注解等方式来实现,或者采用配置类的方式来实现
//	 */
//	@Override
//	protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
//		HttpServletRequest httpRequest = (HttpServletRequest) request;
//	    HttpServletResponse httpResponse = (HttpServletResponse) response;
//
//	    // 获取请求头部信息
//	    String origin = httpRequest.getHeader("Origin");
//	    String headers = httpRequest.getHeader("Access-Control-Request-Headers");
//	    String methods = httpRequest.getHeader("Access-Control-Request-Method");
//
//	    // 判断是否为预检请求(Preflight Request)
//	    if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
//	        // 允许跨域请求
//	        httpResponse.setHeader("Access-Control-Allow-Origin", origin);
//	        httpResponse.setHeader("Access-Control-Allow-Methods", methods);
//	        httpResponse.setHeader("Access-Control-Allow-Headers", headers);
//
//	        return true; // 直接返回,不进行身份验证
//	    }
//
//	    // 设置响应头部信息
//	    httpResponse.setHeader("Access-Control-Allow-Origin", origin);
//	    httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
//	    httpResponse.setHeader("Access-Control-Allow-Headers", methods);
//
//	    // 处理身份验证
//	    return super.preHandle(httpRequest, httpResponse);
//	}
}

5.3 自定义权限过滤器(可选)

如果不需要权限验证,可忽略此步骤,当使用注解权限验证时可也不需要(可参考 ==>8.2.2 测试使用注解权限验证)。

5.3.1 代码解释

这段代码的作用是实现了一个角色过滤器,用于根据角色进行权限验证和控制。自定义角色过滤器可以替换Shiro默认的roles拦截规则,改为"或者(or)"的关系。这样,只需满足一个角色条件即可进行访问。在这段代码中,主要包含以下几个方法:

isAccessAllowed方法:在该方法中进行权限验证。首先从请求中获取当前请求地址,然后获取当前用户信息。接着,获取当前请求地址所需要的角色信息。如果没有角色限制,则直接返回true。然后,循环验证用户是否拥有该角色,如果拥有任何一个角色,则返回true。如果以上验证都不通过,则返回false

onAccessDenied方法:该方法在权限验证失败后触发。在该方法中,创建一个返回403错误信息的MessageBean对象,并将其转换为JSON字符串返回给客户端

请注意,该代码片段中的其他部分,如导入的包和使用的其他自定义对象(如SysUserMessageBean等),与功能实现无关,需要根据实际情况进行修改或删除。

5.3.2 示例代码

import java.io.IOException;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;

import com.alibaba.fastjson.JSONObject;
import com.qfx.demo.vo.MessageBean;
import com.qfx.demo.vo.SysUser;

/**
 * @功能描述:拦截控制,用于替换shiro默认的roles拦截规则,改"并且(and)"为"或者 (or)"
 */
public class CustomRolesFilter extends AuthorizationFilter{

	private final Logger logger = LogManager.getLogger(getClass()); 
	
	/**
	 * Overriding
	 * @功能描述:设置同一个URL配置多个角色为"或者"的关系,默认为"并且",
	 * 			如:/user/** = Role["admin,user"],默认必须满足"admin","user"条件,
	 * 			      改为"或者"之后只需要满足一个条件即可(Ini.Section中有此url,会走此方法)
	 *
	 * @param request
	 * @param response
	 * @param obj 当前请求url所需要的权限信息,从ShiroConfig.shiroFilter方法的filterChainDefinitionMap中获取
	 * @return
	 * @throws Exception
	 */
	@Override
	protected boolean isAccessAllowed(ServletRequest request,ServletResponse response, Object obj) throws Exception {
		System.out.println("--------------------- 第三步、权限验证 ---------------------");
		// 获取请求地址
		HttpServletRequest hsq = (HttpServletRequest) request;
		String requestUrl = hsq.getServletPath();
		
		System.out.println("-------- 请求[" + requestUrl + "]权限验证开始 --------");
      
		Subject subject = getSubject(request, response);
		// 验证是否登录
		if (null == subject.getPrincipals()) {
			return false;
		}
        // 获取用户信息,这里返回的对象类型与登录验证时
        // new SimpleAuthenticationInfo(user, pwd, this.getName())中的第一个参数的类型需要保持一致
        SysUser user = (SysUser)subject.getPrincipals().getPrimaryPrincipal();
        
        System.out.println("--------1.开启用户["+user.getUserName()+"]访问["+requestUrl+"]的角色过滤--------");
        
        // 获取角色信息
        String[] rolesArray = (String[]) obj;
        if (rolesArray == null || rolesArray.length == 0) { //没有角色限制,有权限访问
        	System.out.println("--------3.用户["+user.getUserName()+"]访问["+requestUrl+"]的角色过滤结束--------");
        	logger.info("用户["+user.getUserName()+"]访问["+requestUrl+"]无角色限制,权限验证通过!");
            return true;
        }
        // 验证是否有当前请求地址的权限(循环验证)
        for (int i = 0; i < rolesArray.length; i++) {
        	// 可直接调用JwtRealm.doGetAuthorizationInfo方法进行权限验证(建议使用,通用性更好)
        	boolean hasRole = subject.hasRole(rolesArray[i]);
            if (hasRole) { //若当前用户是rolesArray中的任何一个,则有权限访问  
            	System.out.println("--------3.用户["+user.getUserName()+"]访问["+requestUrl+"]的角色过滤结束--------");
            	logger.info("用户["+user.getUserName()+"]访问["+requestUrl+"]权限验证通过!");
                return true;    
            }
            // 如果user中有权限信息,可以直接从这里与rolesArray进行比对(不建议使用,user必须要有角色信息方可使用)
            // 略...
        }
        System.out.println("--------3.用户["+user.getUserName()+"]访问["+requestUrl+"]的角色过滤结束--------");
        logger.info("用户["+user.getUserName()+"]访问["+requestUrl+"]权限验证失败,禁止访问!");
        
        System.out.println("--------------------- 请求[" + requestUrl + "]权限验证结束 ---------------------");
        
		return false;
	}
	
	/**
	 * isAccessAllowed验证失败后处理方法
	 * 这里返回403错误信息,而不是返回403错误页面
	 */
	@Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
		System.out.println("--------------------- 第五三步、权限验证失败处理 ---------------------");
		MessageBean messageBean = new MessageBean();
		messageBean.setCode(HttpServletResponse.SC_FORBIDDEN);
		messageBean.setMessage("权限验证失败,禁止访问!");
		
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        httpResponse.setContentType("application/json;charset=UTF-8");
        httpResponse.getWriter().write(JSONObject.toJSONString(messageBean));
        
        return false;
    }
}

5.4 自定义Shiro核心配置类

5.4.1 代码解释

这段代码是一个Shiro的配置类,用于配置Shiro的安全管理器和过滤器链等。主要包含以下几个方法:

  • securityManager方法:创建一个自定义的安全管理器对象。设置自定义的Realm和关闭Shiro自带的Session支持,确保不保存用户登录状态。
  • shiroFilter方法:创建一个Shiro的过滤器链。在过滤器链中定义了不同URL的访问权限和过滤条件。静态资源可以匿名访问,而其他的URL则需要通过JWT验证身份。使用了自定义的CustomJwtFilter过滤器,可以自定义JWT的验证逻辑。此外,还可以使用CustomRolesFilter过滤器对角色进行验证(如果需要)。
  • getDefaultAdvisorAutoProxyCreator方法:创建一个CGLIB代理创建器,强制使用CGLIB代理来创建代理类,而不是使用默认的JDK代理。这可以解决重复代理和可能引起代理出错的问题。

代码还使用了一个MenuRoleCache类,通过该类的menuRoleCacheMap对象动态加载权限配置。根据配置的URL和对应的角色,构建了过滤器链中的映射关系。需要注意的是,自定义对象(如SysMenuRoleMenuRoleCache等),与功能实现无关,需要根据实际情况进行修改或删除。

5.4.2 示例代码

import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.Filter;

import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
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 com.qfx.demo.cache.MenuRoleCache;
import com.qfx.demo.vo.SysMenuRole;

@Configuration
public class ShiroConfig {
    
    // 是否开启权限验证: 开启-true 不开启-false
	public static final boolean ENABLE_PERMISSION_VERIFICATION = false;
	
	/**
	 * <h5>功能:自定义realm认证类,继承自AuthorizingRealm,负责用户的认证和权限的处理</h5>
	 * 
	 * @author zhangpj	@date 2018年10月11日
	 * @param hashMatcher
	 * @return 
	 */
	@Bean
	public JwtRealm jwtRealm(){
		return new JwtRealm();
	}
	
	/**
     * <h5>功能:安全管理器</h5>
     * 权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类
	 * 
	 * @param jwtRealm
	 * @return 
	 */
	@Bean
    public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm){
		// 创建安全管理器对象
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        
        // 设置自定义Realm
        securityManager.setRealm(jwtRealm);
        
        // 关闭Shiro自带session,不保存用户登录状态
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        
        return securityManager;
    }
	
	/**
     * <h5>功能:自定义权限过滤器</h5>
     * 
     * @param securityManager
     * @return 
     */
    @Bean
	public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 调用我们配置的安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 配置我们的登录请求地址,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
//        shiroFilterFactoryBean.setLoginUrl("/error/401.html");
        // 设置无权限时跳转的URL
//        shiroFilterFactoryBean.setUnauthorizedUrl("/error/403.html");
        
        // 获取Shiro的默认过滤器
        Map<String, Filter> filter = shiroFilterFactoryBean.getFilters();
        // 将自定义的过滤器 jwt添加到shiroFilterFactoryBean中
        filter.put("jwt", new CustomJwtFilter());
        if (ENABLE_PERMISSION_VERIFICATION) {
        	// 注意:CustomRolesFilter是权限过滤器
        	filter.put("roles", new CustomRolesFilter());
		}
        // 设置过滤器链中的过滤器
        shiroFilterFactoryBean.setFilters(filter);
        
        // ========== 动态加载权限核心部分开始 ==========
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 对静态资源设置匿名访问,从resoutces/static后面开始写
        filterChainDefinitionMap.put("/css/**", "anon");
        // 可匿名访问的地址
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/index.jsp", "anon");
        filterChainDefinitionMap.put("/login/loginPage", "anon");
        filterChainDefinitionMap.put("/login/register", "anon");
        filterChainDefinitionMap.put("/login/login", "anon");
        // 请求 logout.do地址,shiro去清除session
        filterChainDefinitionMap.put("/logout", "logout");
        
        //循环url,逐个添加到section中。section就是filterChainDefinitionMap,
        //里面的键就是链接URL,值就是存在什么条件才能访问该链接(正式环境从数据库获取)
        Map<String, SysMenuRole> menuRoleMap = MenuRoleCache.menuRoleCacheMap;
        for (String key : menuRoleMap.keySet()) {
        	if (ENABLE_PERMISSION_VERIFICATION) { // 开启权限验证
        		// 设定生效的过滤器,这里使用jwt与roles过滤器,对应上面的filter.put("jwt", new CustomJwtFilter())与filter.put("roles", new CustomRolesFilter())
            	filterChainDefinitionMap.put(key, "jwt, roles["+menuRoleMap.get(key).getRoleNames()+"]");
        	} else { // 不开启权限验证
            	// 仅使用jwt过滤器,此时JwtRealm.doGetAuthorizationInfo方法返回null即可,此方法不会被调用
            	filterChainDefinitionMap.put(key, "jwt");
        	}
        	
        	System.out.println(key + "=roles["+menuRoleMap.get(key).getRoleNames()+"]");
		}
        
        // 所有url都必须认证通过并满足指定的角色才可以访问,必须放在最后.
        // 如果没有定义CustomRolesFilter过滤器或者配置中注释掉了filter.put("roles", new CustomRolesFilter()),
        // 那么其实无论使用哪种方式配置,都不会进行具体的角色验证
//        filterChainDefinitionMap.put("/**", "jwt, roles");
        
        // 所有url认证通过即可访问,必须放在最后
        filterChainDefinitionMap.put("/**", "jwt");
        // ========== 动态加载权限核心部分结束 ==========

        // 设置 Shiro 拦截器链
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        
        return shiroFilterFactoryBean;
    }

	/**
	 * <h5>功能:使用CGLIB代理来创建代理类,而不是使用默认的JDK代理,按需使用</h5> 
	 * 强制使用cglib,防止重复代理和可能引起代理出错的问题 zhuanlan.zhihu.com/p/29161098
	 * 
	 * @param securityManager
	 * @return
	 */
	@Bean
	public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
		DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
		autoProxyCreator.setProxyTargetClass(true);
		return autoProxyCreator;
	}
}

5.4.3 开启权限验证支持(可选)

开启权限验证,需先添加自定义权限过滤器,可参考-->4.3 自定义权限过滤器

5.4.3.1 开启权限过滤器

将常量ENABLE_PERMISSION_VERIFICATION设置为true即可

// 是否开启权限验证: 开启-true 不开启-false
public static final boolean ENABLE_PERMISSION_VERIFICATION = true;

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_shiro_02

5.4.3.2 设置URL认证方式

如果没有定义CustomRolesFilter过滤器或者配置中注释掉了filter.put("roles", new CustomRolesFilter()),那么其实无论使用哪种方式配置,都不会进行具体的角色验证。因此我们只需要以下的默认配置就可以了。

// 所有url认证通过即可访问,必须放在最后
filterChainDefinitionMap.put("/**", "jwt");

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_springboot_03

5.5 自定义JwtRealm

5.5.1 代码解释

这段代码实现了Shiro的自定义Realm类,用于JWT令牌的认证和授权。在认证过程中,通过解析JWT获取用户信息,并从缓存中获取用户信息进行验证。在授权过程中,根据用户信息获取用户的角色信息,并将角色信息设置到SimpleAuthorizationInfo对象中返回。通过这样的方式,可以实现基于JWT的身份验证和权限控制功能。在这段代码中,主要包含以下几个方法:

supports方法,该方法用于指定凭证匹配器,判断传入的AuthenticationToken是否为JwtToken类型。如果是JwtToken类型,则返回true,表示支持该类型的认证

doGetAuthorizationInfo方法,该方法用于进行权限验证,即根据用户信息获取用户的角色和权限信息。首先从PrincipalCollection中获取用户信息。然后创建一个Set集合对象,用于存放用户的角色信息。最后创建一个SimpleAuthorizationInfo对象,将角色信息设置到该对象中,并返回该对象。`如果不需要授权,此方法返回一个null即可`(shiro核心配置的过滤器链没有指定权限过滤器的话==>4.4.3 开启权限验证支持,这个方法是不会被调用的)

doGetAuthenticationInfo方法,该方法用于进行身份认证,即根据传入的AuthenticationToken进行用户的身份验证。首先从AuthenticationToken中获取JWT的token。然后使用工具类ToolToken解析JWT,获取其中的用户名称。根据用户名称从UserCache中获取用户信息。如果用户信息为空,则抛出AuthenticationException异常。最后创建一个SimpleAuthenticationInfo对象,将用户信息、token和Realm名称设置到该对象中,并返回该对象。

5.5.2 示例代码

import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 com.qfx.demo.cache.UserCache;
import com.qfx.demo.util.ToolToken;
import com.qfx.demo.vo.SysUser;

import io.jsonwebtoken.Claims;

public class JwtRealm extends AuthorizingRealm {

	/**
	 * 指定凭证匹配器。匹配器工作在认证后,授权前。
	 */
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof JwtToken;
	}

	/**
	 * 权限验证,在认证之后执行(如果不需要权限验证,直接返回null即可)
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		System.out.println("--------------------- 第四步、用户权限验证 ---------------------");
		// 1.获取用户信息
		// 这里principals.getPrimaryPrincipal()的返回的对象类型与登录验证时
		// new SimpleAuthenticationInfo(user, pwd, this.getName())中的第一个参数的类型需要保持一致
		SysUser user = (SysUser) principals.getPrimaryPrincipal();
		System.out.println("--------用户[" + user.getUserName() + "]进行权限验证--------");

		// 2.单独定一个集合对象放置角色信息
		Set<String> roles = new HashSet<String>();
		roles.add(user.getRoleName());

		// 3.查到权限数据,返回授权信息(要包括 上边的permissions)
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roles);

		return simpleAuthorizationInfo;
	}

	/**
	 * 登录认证
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
			throws AuthenticationException {
		System.out.println("--------------------- 第二步、用户token验证 ---------------------");
		String token = (String) authenticationToken.getCredentials();

		// Token解析
		Claims claims = ToolToken.parseJWT(token);
		if (null == claims) {
			throw new AuthenticationException("Token解析失败,可能被修改");
		}

		// 获取token所属用户名称
		String userName = claims.getSubject();

		// 根据用户名称获取用户信息
		SysUser sysUser = UserCache.getUserCacheMap(userName);

		if (sysUser == null) {
			throw new AuthenticationException("没有找到当前用户信息");
		}

		SimpleAuthenticationInfo authcInfo = new SimpleAuthenticationInfo(sysUser, token, getName());

		return authcInfo;
	}
}

六、跨域访问控制

6.1 代码解释

采用JWT必定少不了跨域访问,因此我们需要添加一个针对跨域访问的配置(JWT过滤器中如果没有添加跨域支持==> 4.2 新增JWT过滤器,则需要单独添加)。建议单独添加此配置信息。

6.2 示例代码

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * <h5>描述:跨域访问控制,前后分离必配</h5>
 */
@Configuration
public class CorsConfig {
	
	private CorsConfiguration buildConfig() {
		CorsConfiguration corsConfiguration = new CorsConfiguration();
		// 允许任何域名使用
		corsConfiguration.addAllowedOrigin("*");
		// 允许任何头
		corsConfiguration.addAllowedHeader("*");
		// 允许任何方法(post、get等)
		corsConfiguration.addAllowedMethod("*");
		return corsConfiguration;
	}

	@Bean
	public CorsFilter corsFilter() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		// 对接口配置跨域设置
		source.registerCorsConfiguration("/**", buildConfig());
		return new CorsFilter(source);
	}
}

至此,SpringBoot整合Shiro+JWT实现权限控制的核心内容就完成了,接下来我们只需要编写好对应的接口进行调用就可以了。

七、扩展

7.1 动态更新shiro权限

动态更新shiro权限(无需重启)

7.1.1 代码解释

代码实现了动态更新Shiro权限的功能。通过获取ShiroFilterFactoryBean对象,清空原有的权限配置,动态加载新的权限配置,并重新构建权限配置,实现了无需重启应用即可更新Shiro权限的功能。这样可以方便地根据实际需求动态调整权限配置,提高系统的灵活性和安全性。在这段代码中,主要包含以下方法:

updatePermission方法:

  • 该方法用于动态更新Shiro的权限配置,无需重启应用。
  • 首先获取ShiroFilterFactoryBean对象,并进行同步操作。
  • 然后获取AbstractShiroFilter对象和PathMatchingFilterChainResolver对象,用于操作权限配置。
  • 清空老的权限控制,即清空FilterChains和FilterChainDefinitionMap。
  • 动态加载权限核心部分:
  • 创建一个LinkedHashMap对象,用于存放URL和对应的权限配置。
  • 设置静态资源的匿名访问权限。
  • 设置可匿名访问的地址。
  • 设置登出地址的权限配置。
  • 根据具体需求,从缓存中获取URL和对应的角色信息,并设置权限配置。
  • 设置所有URL都需要进行JWT验证。
  • 将权限配置设置到ShiroFilterFactoryBean中。
  • 重新构建权限配置:
  • 获取FilterChainDefinitionMap,并遍历其中的权限配置。
  • 使用DefaultFilterChainManager创建新的权限配置。
  • 返回更新权限是否成功的标志。

本例中如果当前是使用的MenuRoleCache2的权限信息,就更新为MenuRoleCache的权限信息;如果是使用的MenuRoleCache的权限信息,就更新为MenuRoleCache2的权限信息。

想要调用此方法,只需要像正常的service一样通过接口调用就可以了。

7.1.2 示例代码

import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.qfx.demo.cache.MenuRoleCache;
import com.qfx.demo.cache.MenuRoleCache2;
import com.qfx.demo.vo.SysMenuRole;

/**
 * 动态更新shiro权限(无需重启)
 */
@Component
public class ShiroPermissionSer {

	@Autowired
	ShiroFilterFactoryBean shiroFilterFactoryBean;

	private int count = 1;

	/**
	 * <h5>功能:动态更新shiro权限(无需重启)</h5>
	 * 
	 * @return
	 */
	public boolean updatePermission() {
		boolean flag = false;
		synchronized (shiroFilterFactoryBean) {
			AbstractShiroFilter shiroFilter = null;
			try {
				shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
				PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
				DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();
				// 1. 清空老的权限控制
				manager.getFilterChains().clear();
				shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();

				// ========== 2. 动态加载权限核心部分开始 ==========
				// 后面这个可以直接从数据库里面获取
				Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
				// 对静态资源设置匿名访问,从resoutces/static后面开始写
				filterChainDefinitionMap.put("/css/**", "anon");
				// 可匿名访问的地址
				filterChainDefinitionMap.put("/", "anon");
				filterChainDefinitionMap.put("/index.jsp", "anon");
				filterChainDefinitionMap.put("/login/loginPage", "anon");
				filterChainDefinitionMap.put("/login/register", "anon");
				filterChainDefinitionMap.put("/login/login", "anon");
				// 请求 logout.do地址,shiro去清除session
				filterChainDefinitionMap.put("/logout", "logout");

				// 循环url,逐个添加到section中。section就是filterChainDefinitionMap,
				// 里面的键就是链接URL,值就是存在什么条件才能访问该链接(正式环境从数据库获取,这里模拟数据权限切换)
				if (count == 1) {
					Map<String, SysMenuRole> menuRoleMap = MenuRoleCache2.menuRoleCacheMap;
					for (String key : menuRoleMap.keySet()) {
						if (ShiroConfig.ENABLE_PERMISSION_VERIFICATION) { // 开启权限验证
			        // 设定生效的过滤器,这里使用jwt与roles过滤器,对应上面的filter.put("jwt", new CustomJwtFilter())与filter.put("roles", new CustomRolesFilter())
							filterChainDefinitionMap.put(key, "jwt, roles[" + menuRoleMap.get(key).getRoleNames() + "]");
						} else { // 不开启权限验证
							// 仅使用jwt过滤器,此时JwtRealm.doGetAuthorizationInfo方法返回null即可,此方法不会被调用
							filterChainDefinitionMap.put(key, "jwt");
						}
					}
					count = 0;
				} else {
					Map<String, SysMenuRole> menuRoleMap = MenuRoleCache.menuRoleCacheMap;
					for (String key : menuRoleMap.keySet()) {
						if (ShiroConfig.ENABLE_PERMISSION_VERIFICATION) { // 开启权限验证
			        // 设定生效的过滤器,这里使用jwt与roles过滤器,对应上面的filter.put("jwt", new CustomJwtFilter())与filter.put("roles", new CustomRolesFilter())
							filterChainDefinitionMap.put(key, "jwt, roles[" + menuRoleMap.get(key).getRoleNames() + "]");
						} else { // 不开启权限验证
							// 仅使用jwt过滤器,此时JwtRealm.doGetAuthorizationInfo方法返回null即可,此方法不会被调用
							filterChainDefinitionMap.put(key, "jwt");
						}
					}
					count = 1;
				}

				// 所有url都必须认证通过才可以访问,必须放在最后
				filterChainDefinitionMap.put("/**", "jwt");

				shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
				// ========== 2. 动态加载权限核心部分结束 ==========

				// 3. 重新构建生成
				Map<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
				for (Map.Entry<String, String> entry : chains.entrySet()) {
					String url = entry.getKey();
					String chainDefinition = entry.getValue().trim().replace(" ", "");
					manager.createChain(url, chainDefinition);
				}
				flag = true;
				System.out.println("更新权限成功");
			} catch (Exception e) {
				throw new RuntimeException("更新shiro权限出现错误!");
			}
		}
		return flag;
	}
}

7.2 添加一个自定义Shiro异常处理类

通过这个可以自定义Shiro异常时返回的结构信息

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import com.qfx.demo.vo.MessageBean;

/**
 * <h5>描述:全局异常处理类</h5>
 * 优先执行此异常处理类
 *  
 */
@ControllerAdvice
public class MyExceptionHandler {
	private static final Logger LOG = LoggerFactory.getLogger(MyExceptionHandler.class);
		
	/**
	 * <h5>功能:Shiro权限验证未通过异常</h5>
	 * @param request
	 * @param e
	 * @return 错误信息
	 */
	@ExceptionHandler(value =AuthorizationException.class)
    public ResponseEntity<?> authorizationExceptionHandler(HttpServletRequest request, AuthorizationException e){
		String requestUri =  request.getRequestURI();
		
		// 最后的e会在控制台输出异常的完整信息,如果不想显示完整的错误信息可以将e去掉
//		LOG.error("请求[{}]发生[{}]异常", requestUri, e.getMessage(), e);
		LOG.error("请求[{}]发生[{}]异常", requestUri, e.getMessage());
		
		// 返回错误信息,交给其他异常处理类处理
//		return e.getMessage();
		MessageBean messageBean = new MessageBean();
		messageBean.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
		messageBean.setMessage("权限验证未通过[" + e.getMessage() + "]");
		
		return ResponseEntity.ok().body(messageBean);
    }

	/**
	 * <h5>功能:全局异常处理方式二</h5>
	 * 返回视图用String或者ModelAndView,返回纯数据用ResponseEntity<?>,这里返回一个数据
	 * @param request
	 * @param e
	 * @return 错误信息
	 */
	@ExceptionHandler(value =Exception.class)
    public ResponseEntity<?> exceptionHandler(HttpServletRequest request, Exception e){
		Map<String, Object> paramsMap = getMaps(request);
		Map<String, Object> headersMap = getHeaders(request);
	
		String requestUri = request.getRequestURI();
	
		LOG.error("请求[{}]发生[{}]异常", requestUri, e.getMessage());
		LOG.error("参数[{}]", paramsMap);
		LOG.error("header[{}]", headersMap);
	
		MessageBean messageBean = new MessageBean(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(messageBean);
    }
	
	// =================== private method ===================
	
	/**
	 * <h5>功能:获取从request中传递过来的参数信息</h5>
	 * 
	 * @return Map<String, Object>
	 */
	private Map<String, Object> getMaps(HttpServletRequest request){
		Map<String, Object> paramMap = new HashMap<String, Object>();
		Enumeration<String> enume = request.getParameterNames();
		while (enume.hasMoreElements()) {
			String key = (String) enume.nextElement();
			String[] values = request.getParameterValues(key);
			paramMap.put(key, values.length == 1 ? request.getParameter(key).trim() : values);
		}
		
		return paramMap;
	}
	
	/**
	 * <h5>功能: 获取从request中传递过来的header信息</h5>
	 * 
	 * @return Map<String, Object>
	 */
	private Map<String, Object> getHeaders(HttpServletRequest request) {
		Map<String, Object> headerMap = new HashMap<String, Object>();
		Enumeration<?> er = request.getHeaderNames();//获取请求头的所有name值
		String headerName;
		while(er.hasMoreElements()){
			headerName = er.nextElement().toString();
			headerMap.put(headerName, request.getHeader(headerName));
		}
		
		return headerMap;
	}
}

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_springboot_04

八、测试

8.1 编写Controller与service

8.1.1 Service

LoginService

import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import com.qfx.demo.cache.UserCache;
import com.qfx.demo.util.ToolToken;
import com.qfx.demo.vo.MessageBean;
import com.qfx.demo.vo.SysUser;

@Service
public class LoginService {

	/**
	 * @功能描述:shiro+jwt登录验证,通过返回true,失败返回false
	 *
	 * @param request
	 * @param sysUser
	 * @return
	 */
	public MessageBean loginJwt(SysUser sysUser, HttpServletResponse response) {
		String message = "登录成功!";
		MessageBean messageBean = new MessageBean();

		System.out.println("用户登录:userName[" + sysUser.getUserName() + "],userPass[" + sysUser.getPassWord() + "]");

		// 获取用户信息
		SysUser user = UserCache.getUserCacheMap(sysUser.getUserName());
		if (ObjectUtils.isEmpty(user)) {
			message = "用户不存在!";
			messageBean.setMessage(message);
			System.out.println("用户[" + sysUser.getUserName() + "]进行登录验证失败,失败原因[" + message + "]");

			return messageBean;
		}

		if (!sysUser.getPassWord().equals(user.getPassWord())) {
			message = "密码错误!";
			messageBean.setMessage(message);
			System.out.println("用户[" + sysUser.getUserName() + "]进行登录验证失败,失败原因[" + message + "]");

			return messageBean;
		}

		// 获取系统时间戳
		long currentTimeMillis = System.currentTimeMillis();

		// 生成Token:userName + 时间戳 + salt
		String tokenStr = ToolToken.createJwtToken(sysUser.getUserName() + currentTimeMillis, sysUser.getUserName());
		if (ObjectUtils.isEmpty(tokenStr)) {
			message = "生成token发生错误!";
			messageBean.setMessage(message);
			System.out.println("用户[" + sysUser.getUserName() + "]进行登录验证失败,失败原因[" + message + "]");
			return messageBean;
		}

		System.out.println("用户[" + sysUser.getUserName() + "]登录认证通过");
		messageBean.setData(tokenStr);
    	messageBean.setMessage(message);

		// 将Token放入Response Header中
		response.addHeader("Authorization", tokenStr);

		return messageBean;
	}
}

8.1.2 Controller

8.1.2.1 loginController
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.qfx.demo.service.LoginService;
import com.qfx.demo.vo.MessageBean;
import com.qfx.demo.vo.SysUser;

@RestController
@RequestMapping("/login")
public class loginController {
	
	@Autowired
	LoginService loginService;
	
	@RequestMapping("/login")
    public ResponseEntity<?> login(SysUser sysUser, HttpServletResponse response) throws AuthenticationException {
        // shiro登录验证
        MessageBean messageBean = loginService.loginJwt(sysUser, response);
        return ResponseEntity.ok(messageBean);
    }
	
	@RequestMapping("/test")
	@ResponseBody
    public String test() {
        return "你好";
    }
	
	/**
	 * 使用注解添加权限验证
	 */
	@RequestMapping("/test02")
	@ResponseBody
	@RequiresRoles("网管")
	public String test02() {
		return "你也好";
	}
}
8.1.2.2 UserController
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.qfx.demo.cache.UserCache;
import com.qfx.demo.vo.SysUser;

@RestController
@RequestMapping("user")
public class UserController extends BaseController{
  
	/**
	 * @功能描述:获取用户信息
	 *
	 * @return
	 */
	@RequestMapping("list")
	public Map<String, SysUser> list(){
		return UserCache.userCacheMap;
	}
}
8.1.2.3 RoleMenuController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.qfx.demo.shiro.ShiroPermissionSer;

@RestController
@RequestMapping("roleMenu")
public class RoleMenuController extends BaseController{
	@Autowired
	ShiroPermissionSer shiroPermissionSer;

	/**
	 * 动态更新权限(无需重启系统)
	 */
	@RequestMapping("initChain")
	public String initChain() {
		boolean flag = shiroPermissionSer.updatePermission();
		return "权限更新" + (flag ? "成功!" : "失败,请重试!");
	}
}

8.2 测试请求

8.2.1 登录

用户信息

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_shiro_05

请求地址

http://localhost:8081/qfxSpringbootShiroJwtDemo/login/login?userName=wangwu&passWord=111111

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_shiro_06

我们使用wangwu登录,角色是”网管“,登录成功

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_shiro_07

8.2.1 测试JWT验证是否生效

请求地址

http://localhost:8081/qfxSpringbootShiroJwtDemo/login/test

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_shiro_08

8.2.1.1 不添加token

提示未登录

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_springboot_09

8.2.1.2 添加上token

正常返回信息,默认后续测试请求中都已添加token信息

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_jwt_10

8.2.2 测试使用注解权限验证

请求地址

http://localhost:8081/qfxSpringbootShiroJwtDemo/login/test02

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_springboot_11

当前登录用户是”wangwu“,角色是”网管“,因此成功请求

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_springboot_12

将登录换成”linlin“,角色是”游客“,可以看到返回了错误信息,权限验证失败,说明使用注解进行权限验证的功能成功了。

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_jwt_13

后台错误信息如下:

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_jwt_14

8.2.3 测试动态刷新Shiro权限

修改配置,开启权限验证,参考==>5.4.3 开启权限验证支持,修改完毕重启服务。

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_jwt_15

8.2.3.1 权限刷新接口

请求地址

http://localhost:8081/qfxSpringbootShiroJwtDemo/roleMenu/initChain

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_springboot_16

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_shiro_17

8.2.3.2 获取用户列表接口

请求地址

http://localhost:8081/qfxSpringbootShiroJwtDemo/user/list

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_jwt_18

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_jwt_19

此时无权限访问,我们调用一下权限动态刷新接口

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_springboot_20

调用成功,再次调用用户列表接口,可以看到已经有权限了,这说明我们的动态刷新Shiro权限的功能生效了。

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_shiro_21

后台输出调用信息

SpringBoot入门三十三,整合Shiro+JWT实现权限控制_shiro_22


测试结束!


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
PDnN7dVP3WHp