引言
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 存放菜单角色信息缓存数据
这里有两个菜单角色信息缓存数据的文件,是因为模拟动态刷新权限时可以使用到,可以更好的测试动态刷新权限是否成功。
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字符串返回给客户端
。请注意,该代码片段中的其他部分,如导入的包和使用的其他自定义对象(如
SysUser
和MessageBean
等),与功能实现无关,需要根据实际情况进行修改或删除。
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和对应的角色,构建了过滤器链中的映射关系。需要注意的是,自定义对象(如SysMenuRole
和MenuRoleCache
等),与功能实现无关,需要根据实际情况进行修改或删除。
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;
5.4.3.2 设置URL认证方式
如果没有定义CustomRolesFilter过滤器或者配置中注释掉了filter.put("roles", new CustomRolesFilter()),那么其实无论使用哪种方式配置,都不会进行具体的角色验证。因此我们只需要以下的默认配置就可以了。
// 所有url认证通过即可访问,必须放在最后
filterChainDefinitionMap.put("/**", "jwt");
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;
}
}
八、测试
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 登录
用户信息
请求地址
http://localhost:8081/qfxSpringbootShiroJwtDemo/login/login?userName=wangwu&passWord=111111
我们使用wangwu登录,角色是”网管“,登录成功
8.2.1 测试JWT验证是否生效
请求地址
http://localhost:8081/qfxSpringbootShiroJwtDemo/login/test
8.2.1.1 不添加token
提示未登录
8.2.1.2 添加上token
正常返回信息,默认后续测试请求中都已添加token信息
8.2.2 测试使用注解权限验证
请求地址
http://localhost:8081/qfxSpringbootShiroJwtDemo/login/test02
当前登录用户是”wangwu“,角色是”网管“,因此成功请求
将登录换成”linlin“,角色是”游客“,可以看到返回了错误信息,权限验证失败,说明使用注解进行权限验证的功能成功了。
后台错误信息如下:
8.2.3 测试动态刷新Shiro权限
修改配置,开启权限验证,参考==>5.4.3 开启权限验证支持,修改完毕重启服务。
8.2.3.1 权限刷新接口
请求地址
http://localhost:8081/qfxSpringbootShiroJwtDemo/roleMenu/initChain
8.2.3.2 获取用户列表接口
请求地址
http://localhost:8081/qfxSpringbootShiroJwtDemo/user/list
此时无权限访问,我们调用一下权限动态刷新接口
调用成功,再次调用用户列表接口,可以看到已经有权限了,这说明我们的动态刷新Shiro权限的功能生效了。
后台输出调用信息
测试结束!