Shiro 是 Apache下的一个安全框架。主要用于认证、授权、加密、会话管理等功能。这是摘自官网的一个截图:

针对授权,说明一点:Shiro 不会去维护用户以及维护权限。因此这些需要我们自己去设计和提供。通过相应的接口注入给 Shiro 即可。
这里同样摘自官网的一个架构图:

其中,ApplicationCode指的是我们的应用程序。也就是说,Shiro 架构中,有三个主要的组成部分:
Subject:主体,代表着当前的用户信息。 从图中可以发现,它也是直接和我们程序进行交互的一个对象。SecurityManager:Shiro的一个核心,所有的交互都通过它来控制。例如:管理所有的Subject,负责认证、授权、会话管理。Realm:可以当做一个元数据,当Shiro需要和一些安全相关的数据打交道的时候,就需要通过Realm来获得相关的信息。上文说到过我们自己去设计和提供用户的权限并注入给Shiro。而Realm本质上是一个特定于安全的DAO。封装了一些底层的数据操作。
然后我们再看看SecurityManager里面又有哪些东西:

SessionManager:会话管理器,用于管理Session的。SessionDAO:封装底层操作API,比如我们可以通过它把Session存储到数据库。CacheManager:缓存控制器,管理用户、权限的缓存。Authentication:身份认证。Authorization:用于权限验证,判断某个用户是否有某个操作的权限。先来说下大概的架构设计,由于这个项目是前后端分离的一个微服务项目, 因此需要采取无状态登录。 那么我们就不能使用Shiro来做登录的一个拦截,原因如下:
Shiro这个框架,他是可以进行登录状态的一个拦截的。但是它的一个校验机制是基于Session来实现的。Shiro默认的拦截器都是跳转URL页面,前后端分离之后,后端就无法干涉前端了。那么无状态怎么实现?就应该使用JWT来完成,而且对于微服务而言,JWT也是一个很好地选择。
Token,返回给前端,前端自己写入到Cookie里面也好,本地也好。Token即可正常访问。即无状态的登录。那么对于Shiro和JWT的整合,它们的角色也就可以定下来了:
Shiro:用于权限的校验。比如管理员权限、游客权限呀等等。同时支持对Token的封装(AuthenticationToken)。JWT:用于无状态登录。那么,怎么把两者整合起来呢?
JWT的使用中,有一个拦截器或者一个过滤器。就是用来判断你的请求是否包含了对应的Token。Shiro的配置中,加入JWT的拦截器。如果没带Token,就直接抛异常,提示必须携带Token。Token了,是不是就应该校验Token的有效性啦?这一步就可以交给Shiro的Realm来完成校验或者是授权。那么接下来就可以从这些角度来开始编写代码了,大家可以在这个项目的基础上做一个修改:Java - SpringBoot整合JWT
然后添加pom依赖:
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.9.0version>
dependency>
我们可以写一个配置类ShiroConfig:
@Configuration
public class ShiroConfig {
}
有哪些内容需要写呢?
SecurityManager:它是Shiro的核心啊,少了它的配置,咋玩Shiro。这里面配置对应的Realm。同时我们还需要注意关闭Session。因为我们要搞无状态。ShiroFilterFactoryBean:Shiro过滤器的一些配置,这里可以塞入我们的JWT过滤器。以及哪些路径不需要校验。备注:后面会一次性贴完代码。
因为SecurityManager里面需要配置我们自定义的Realm,这里我先写一个没有任何实现的JwtRealm。
public class JwtRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
紧接着回到我们的ShiroConfig,我们添加如下配置:
Realm。ShiroDao功能。getSession功能。我们就应该自定义一个SubjectFactory,不创建Session功能。1.首先自定义JwtDefaultSubjectFactory:
public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 不创建 session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
2.实现上面三种效果:
/**
* 声明不要使用默认的 DefaultWebSubjectFactory 创建对象,因为我们不希望使用Session相关的功能
*/
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultSubjectFactory();
}
/**
* 自定义的Shiro Realm
*/
@Bean
public Realm realm() {
return new JwtRealm();
}
/**
* Shiro 核心ecurityManager的配置
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置我们自定义的JwtRealm
securityManager.setRealm(realm());
// 关闭ShiroDao、Session功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// 不能使用Subject的getSession函数
securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
我们先自定义一个Jwt的过滤器:JwtFilter:具体实现后面说
public class JwtFilter extends AccessControlFilter {
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
return false;
}
}
Shiro拦截器配置:
/**
* Shiro 拦截器配置
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/login");
// 拦截器设置
Map<String, Filter> filterMap = new HashMap<>();
// jwt的相关过滤器
filterMap.put("jwt", new JwtFilter());
// 这两个是默认的实现,可以不填,但是可以结合下面的来看,anno代表无需认证即可访问
filterMap.put("anon", new AnonymousFilter());
// 登出也是无需进入拦截器
filterMap.put("logout", new LogoutFilter());
shiroFilter.setFilters(filterMap);
// 配置相关的连接和对应的拦截器,按顺序执行
Map<String, String> filterRuleMap = new LinkedHashMap<>();
// 登录不走拦截,登出走登出的默认实现。其余所有请求都走jwt对应设置的过滤器。
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
filterRuleMap.put("/**", "jwt");
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
我们的项目里面需要用到AOP,因为需要用到RequiresRoles相关的注解,用来做Shiro的权限校验。
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
// 设置代理类
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
/**
* 开启aop注解支持,这样就可以用RequiresRoles、RequiresPermissions等注解
*
* @param securityManager
* @return
*/
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
// Shiro生命周期处理器,让Shiro管理对应的Bean
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
import com.pro.config.jwt.JwtDefaultSubjectFactory;
import com.pro.config.jwt.JwtFilter;
import com.pro.config.jwt.JwtRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SubjectFactory;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
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.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Zong0915
* @date 2022/11/11 下午7:33
*/
@Configuration
public class ShiroConfig {
/**
* 声明不要使用默认的 DefaultWebSubjectFactory 创建对象,因为我们不希望使用Session相关的功能
*/
@Bean
public SubjectFactory subjectFactory() {
return new JwtDefaultSubjectFactory();
}
/**
*/
@Bean
public Realm realm() {
return new JwtRealm();
}
/**
* Shiro 核心ecurityManager的配置
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置我们自定义的JwtRealm
securityManager.setRealm(realm());
// 关闭ShiroDao、Session功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// 不能使用Subject的getSession函数
securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
/**
* Shiro 拦截器配置
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/login");
// 拦截器设置
Map<String, Filter> filterMap = new HashMap<>();
// jwt的相关过滤器
filterMap.put("jwt", new JwtFilter());
// 这两个是默认的实现,可以不填,但是可以结合下面的来看,anno代表无需认证即可访问
filterMap.put("anon", new AnonymousFilter());
// 登出也是无需进入拦截器
filterMap.put("logout", new LogoutFilter());
shiroFilter.setFilters(filterMap);
// 配置相关的连接和对应的拦截器,按顺序执行
Map<String, String> filterRuleMap = new LinkedHashMap<>();
// 登录不走拦截,登出走登出的默认实现。其余所有请求都走jwt对应设置的过滤器。
filterRuleMap.put("/login", "anon");
filterRuleMap.put("/logout", "logout");
filterRuleMap.put("/**", "jwt");
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
// 设置代理类
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
/**
* 开启aop注解支持,这样就可以用 RequiresRoles、RequiresPermissions等注解
*
* @param securityManager
* @return
*/
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
// Shiro生命周期处理器
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
我们项目里面原本还有一个拦截器TokenInterceptor,那么我们这里整合了Shiro了,就不再使用它了,可以把它删除。改为JwtFilter。JwtFilter里面我们要做什么事情?继承AccessControlFilter,那么一般实现两个接口:
isAccessAllowed:调用链中,会先执行这个函数,如果返回true,代表允许访问。如果返回false,则进入下面的onAccessDenied函数。我们可以写一些自定义的注解,让某些操作跳过这个Shiro校验,不需要登录。onAccessDenied:返回false代表不允许访问。反之可以访问。这里面一般用来判断Token的合法性,然后进行登录。import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
* @author Zong0915
* @date 2022/11/11 下午8:34
*/
@Slf4j
public class JwtFilter extends AccessControlFilter {
/**
* 返回false,则进入onAccessDenied函数处理
* 返回true,则该请求被允许访问。
* 一般这里可以判断下是否携带Token,如果没携带,直接返回false即可
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object o) throws Exception {
// 返回false,走onAccessDenied()
return false;
}
/**
* 返回true代表登录通过,这里可以用来校验JWT Token的合法性
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("token");
// 封装一下AuthenticationToken
JwtToken jwtToken = new JwtToken(jwt);
log.info("获取到请求头中的token:{}", jwt);
try {
// 委托 realm 进行登录认证 ,一种固定写法,这里调用的就是我们自定义的JwtRealm的实现
// 这里登录成功之后,后面就可以通过SecurityUtils.getSubject()来获取对应的Subject了
// Subject就包含了相关的JWTToken信息。
getSubject(servletRequest, servletResponse).login(jwtToken);
} catch (Exception e) {
e.printStackTrace();
//调用下面的方法向客户端返回错误信息
return false;
}
return true;
}
}
我的理解是:
JwtFilter中,对Token进行了拦截。getSubject(servletRequest, servletResponse).login(jwtToken);函数进行登录。login这个函数呢,相当于把这次校验的任务,委派给Realm来执行,而我们之前ShiroConfig中配置了自定义的JwtRealm,因此实际上它会调用JwtRealm中重写的方法。SecurityUtils.getSubject();拿到我们的登录信息。备注:
getSubject(servletRequest, servletResponse).login(jwtToken);
// getSubject 的源码:
protected Subject getSubject(ServletRequest request, ServletResponse response) {
return SecurityUtils.getSubject();
}
// 等同于
SecurityUtils.getSubject().login(jwtToken)
JwtToken需要实现AuthenticationToken接口,主要就是把Principal、Credentials的值都和Token绑定。
public class JwtToken implements AuthenticationToken {
private String jwt;
public JwtToken(String jwt) {
this.jwt = jwt;
}
// 类似是用户名
@Override
public Object getPrincipal() {
return jwt;
}
// 类似密码
@Override
public Object getCredentials() {
return jwt;
}
}
自定义JwtRealm需要继承AuthenticatingRealm,我们需要注意以下几点:
doGetAuthenticationInfo函数,这个函数是我们进行校验的一个核心实现。JwtToken,因此我们需要重写supports函数,让其支持我们自定义的JwtToken。否则会报错。如图:
最终代码如下:
@Slf4j
public class JwtRealm extends AuthenticatingRealm {
@Autowired
private JwtUtil jwtUtil;
/*
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
* 必须重写此方法,不然Shiro会报错
* */
@Override
public boolean supports(AuthenticationToken token) {
//这个token就是从过滤器中传入的jwtToken
return token instanceof JwtToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwt = (String) token.getPrincipal();
if (jwt == null) {
throw new SignatureException("Token不能为空!");
}
// 判断
Long userId = jwtUtil.getSubject(jwt);
TokenContext.setUserId(userId);
return new SimpleAuthenticationInfo(jwt, jwt, "JwtRealm");
}
}
ThreadLocal来存储了一个userId。也就是TokenContext。但是现在整合了Shiro,可以直接通过SecurityUtils.getSubject()来获取对应的属性了。jjwt和java-jwt。两个差不多,但是jjwt这个依赖已经停止维护很久了,建议使用java-jwt。1.创建一个SpringBeanUtil类,用来获取Bean的:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class SpringBeanUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringBeanUtil.applicationContext == null) {
SpringBeanUtil.applicationContext = applicationContext;
}
log.info("\r\n----------加载applicationContext成功-----------------");
}
// 获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
// 通过class获取Bean.
public static <T> T getBean(Class<T> clazz) {
try {
char[] cs = clazz.getSimpleName().toCharArray();
cs[0] += 32;// 首字母大写到小写
return (T) getApplicationContext().getBean(String.valueOf(cs));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
2.将配置文件配置类和JwtUtil分离开,创建一个AppConfig:
@Component
@ConfigurationProperties(prefix = "config.jwt")
public class AppConfig {
private String secret;
private int expire;
public static String GetSecret() {
AppConfig configBean = SpringBeanUtil.getBean(AppConfig.class);
String secretKey = configBean.getSecret();
return secretKey == null ? StringUtils.EMPTY : secretKey;
}
public static int GetExpire() {
AppConfig configBean = SpringBeanUtil.getBean(AppConfig.class);
int expire = configBean.getExpire();
return expire;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public void setExpire(int expire) {
this.expire = expire;
}
public int getExpire() {
return expire;
}
}
3.创建常量类JwtConstant:
public class JwtConstant {
public static final String USER_ID = "userId";
}
4.核心实现,Java-jwt版本的JWT实现JwtUtil:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.pro.constant.JwtConstant;
import io.jsonwebtoken.*;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.tomcat.util.codec.binary.Base64;
import java.util.Calendar;
import java.util.Map;
import java.util.UUID;
/**
* @author Zong0915
* @date 2022/11/11 下午7:16
*/
public class JwtUtil {
private static final SignatureAlgorithm ALGORITHM = SignatureAlgorithm.HS256;
// 生成Jwt Token
public static String buildToken(Long userId, Map<String, Object> claims) {
Calendar expires = Calendar.getInstance();
JwtBuilder jwtBuilder = Jwts
.builder()
.setClaims(claims)
// JWT唯一标识
.setId(UUID.randomUUID().toString())
// 签发时间
.setIssuedAt(expires.getTime())
// Subject主体,存我们的userId
.setSubject(userId + "")
// 签名算法和对应的秘钥
.signWith(ALGORITHM, AppConfig.GetSecret());
// 设置过期时间
expires.add(Calendar.SECOND, AppConfig.GetExpire());
jwtBuilder.setExpiration(expires.getTime());
// 生成Token
return jwtBuilder.compact();
}
public static Long getUserId() {
Subject subject = SecurityUtils.getSubject();
if (subject == null) {
return null;
}
// 如果校验成功,即getSubject(servletRequest, servletResponse).login(jwtToken);调用成功
// 那么这里就可以拿到对应的Token,因为我们重写了JwtToken,它的Principals值就是我们的Token
String token = (String) subject.getPrincipals().getPrimaryPrincipal();
return getUerIdFromClaim(token);
}
// 根据Token 拿到我们的userId
public static Long getUerIdFromClaim(String token) {
Object o = Jwts
.parser()
.setSigningKey(AppConfig.GetSecret())
.parseClaimsJws(token)
.getBody()
.get(JwtConstant.USER_ID);
if (o != null) {
return Long.parseLong(o + "");
}
return null;
}
public static boolean isVerify(String jwtToken) {
// 这里一定要经过Base64解码
Algorithm algorithm = Algorithm.HMAC256(Base64.decodeBase64(AppConfig.GetSecret()));
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(jwtToken); // 校验不通过会抛出异常
return true;
}
}
5.JwtFilter修改(因为还是用的老一套的JWT,不再使用注入了,直接静态函数调用)
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Zong0915
* @date 2022/11/11 下午8:34
*/
@Slf4j
public class JwtFilter extends AccessControlFilter {
/**
* 判断是否携带了有效的JwtToken
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object o) throws Exception {
//
return false;
}
/**
* 返回true代表登录通过,这里可以用来校验JWT Token的合法性
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("token");
JwtToken jwtToken = new JwtToken(jwt);
log.info("获取到请求头中的token:{}", jwt);
try {
// 委托 realm 进行登录认证 ,一种固定写法,这里调用的就是我们自定义的JwtRealm的实现
// 这里登录成功之后,后面就可以通过SecurityUtils.getSubject()来获取对应的Subject了
// Subject就包含了相关的JWTToken信息。
getSubject(servletRequest, servletResponse).login(jwtToken);
} catch (Exception e) {
onLoginFail(servletResponse, e);
log.error(e.getMessage());
//调用下面的方法向客户端返回错误信息
return false;
}
return true;
}
private void onLoginFail(ServletResponse response, Exception e) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 这里你自己自定义一个返回报文就好了,这里单纯的就是展示报错信息
httpResponse.getWriter().write(JSON.toJSONString(e.getMessage()));
}
}
6.JwtRealm修改(同理):
import com.pro.config.JwtUtil;
import io.jsonwebtoken.SignatureException;
import lombok.extern.slf4j.Slf4j;
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.realm.AuthenticatingRealm;
/**
* @author Zong0915
* 自定义的Shiro Realm
* @date 2022/11/11 下午8:14
*/
@Slf4j
public class JwtRealm extends AuthenticatingRealm {
/*
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
* 必须重写此方法,不然Shiro会报错
* */
@Override
public boolean supports(AuthenticationToken token) {
//这个token就是从过滤器中传入的jwtToken
return token instanceof JwtToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwt = (String) token.getPrincipal();
if (jwt == null) {
throw new SignatureException("Token不能为空!");
}
// 校验JWT,如果不通过的话,就会抛出异常,然后被JwtFilter捕捉
JwtUtil.isVerify(jwt);
return new SimpleAuthenticationInfo(jwt, jwt, "JwtRealm");
}
}
7.Controller修改:
import com.pro.config.JwtUtil;
import com.pro.constant.JwtConstant;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class UserController {
@PostMapping("/login")
public String login(@RequestParam("userId") Long userId) {
Map<String, Object> chaim = new HashMap<>();
chaim.put(JwtConstant.USER_ID, userId);
String token = JwtUtil.buildToken(userId, chaim);
return "Success, Jwt Token : " + token;
}
@PostMapping("/getUser")
public String getUser() {
Long userId = JwtUtil.getUserId();
Subject currentUser = SecurityUtils.getSubject();
if (userId != null) {
return "成功拿到用户信息: " + currentUser.getPrincipal();
}
return "用户信息为空";
}
}
1.登录操作:

2.携带Token访问:

3.不携带Token访问:

先说下一个调用链:
JWT了。Token里面,因为默认是Base64编码。Token返回给客户端。客户端后续只需要每次请求都携带这个Token即可。核心操作:

第一点:我们需要使用JWT来实现无状态的登录。即携带Token访问接口。而不是Session。Session存储于服务器,Shiro默认是开启Session功能的,因此我们要把它关闭。
JwtDefaultSubjectFactory替代默认的DefaultWebSubjectFactory。ShiroDao以及禁用Session第二点:配置自定义的Realm和过滤器。
ShiroFilterFactoryBean:配置拦截器、哪些路径不需要拦截、哪些需要拦截(配置自定义的JwtFilter)Realm:
第三点:开启注解支持(详细的看ShiroConfig代码)。
携带Token访问流程:
JwtFilter拦截。调用getSubject(servletRequest, servletResponse).login(jwtToken);委派JwtRealm来完成登录校验。JwtRealm中调用JWT相关API完成校验。JwtUtil.isVerify(jwt);JwtRealm需要注意的三点:
supports,需要支持我们自定义的Token:JwtToken。一定要重写。doGetAuthorizationInfo()函数,一般这里需要读数据库中我们定义的权限,然后将权限注入,这样结合@RequiresRoles注解,就完成了权限的控制。 不在本篇文章的编码范围内。doGetAuthenticationInfo()函数。主要是调用JWT相关的API。最后,本文其实还留下几个功能点没有开发:
Realm中的权限验证(比如Admin、User这种权限的校验),还需要配合注解来完成,这里还涉及到查表。因为需要我们自己去维护权限数据。isAccessAllowed()函数中,增加一个注解校验,跳过权限校验。Shiro框架就自带一个缓存功能。上面的内容会继续更新~