如何整合Shiro+Jwt+Redis,以及为什么要这么做
我个人认为
①为什么用shiro:“Shiro+Jwt+Redis”模式和“单纯的shiro”模式相比,主要用的是shiro里面的登录认证和权限控制功能
②为什么用jwt:“Shiro+Jwt”模式和“Shiro+Cookie”模式相比,后者的用户登录信息是存储在服务器的会话里面的,也就是后端服务器的缓存里面,这样的话就没办法分布式(多个后端),解决办法是把登录信息以及过期时间直接存储在一段字符串中,然后由前端保存,后端只需根据生成token时定义的秘钥去验证jwt是否正确即可,如果正确就允许接下来的操作。
③为什么用Redis:“Shiro+Jwt+Redis”模式和“Shiro+Jwt”模式相比,前者可以实现分布式环境下的会话共享,这么说有点抽象,通俗一点就是:在分布式系统中,用户的会话信息需要在多个服务器之间共享,而我可以把用户的一些前端经常请求的用户信息或者其他信息存储到redis里面,这样就不用去经常查询数据库信息了。
所以综上所述,我们使用Shiro+Jwt+Redis的模式。
Jwt
需要了解一门技术,首先从为什么产生开始说起是最好的。JWT 主要用于用户登录鉴权,所以我们从最传统的 session 认证开始说起。
前置知识
**会话:**每个用户的一次登录到登出之间叫做一个会话。
登录状态:“无状态”和“有状态”是指对于服务器而言的两种不同的处理方式:
- 无状态(Stateless):在无状态的认证机制中,服务器不需要保存任何关于客户端的状态信息。每次客户端发送请求时,服务器只需要对请求进行处理,而无需考虑之前的请求状态。这意味着服务器可以更容易地进行水平扩展,因为不需要担心请求会被路由到特定的服务器上。
- 有状态(Stateful):相比之下,在有状态的认证机制中,服务器需要保存客户端的状态信息,通常通过会话对象或其他方式来记录客户端的状态。这意味着服务器需要在多个请求之间共享状态信息,可能需要使用特定的机制来保证状态的一致性和可靠性。
session认证
众所周知,http 协议本身是无状态的协议(http 是一种无状态协议,就是说每次用户进行用户名和密码认证之后,http 不会留下记录,下一次请求还需要进行认证。因为http 不知道每次请求是哪一个用户发出的)。
session认证就是说用户登录后把将此用户的登录状态存储到服务器的内存中。
session 认证的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。
token认证
token认证的过程就是在用户第一次登录的时候根据秘钥(一般秘钥中会包括此用户的唯一标志,比如账号)生成此次会话的token,然后之后前端每次访问后端都携带token,后端再根据秘钥解析,如果解析成功就说明token有效,进而可以信任此次请求进行接下来的操作。
基于 token 的认证方式是一种服务端无状态的认证方式,服务端不存储 token 数据,适合分布式系统。
什么是JWT
而JWT(全称:Json Web Token)是一种特殊的Token,它采用了JSON格式来对Token进行编码和解码,并携带了更多的信息,例如用户ID、角色、权限等。它包含了三部分:头部(Header)、数据(Payload)和签名(Signature)。其中,头部和数据都是经过Base64编码的JSON字符串,而签名是对头部和数据进行签名后得到的字符串。
Springboot使用JWT实现登录认证以及请求拦截
主要是两步:配置拦截器、配置要拦截哪些接口
<!-- jwt工具--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version></dependency>
package com.hebut.demo.common.utils;import cn.hutool.core.codec.Base64;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;// JWT工具类
@Configuration
public class JwtUtil {@Value("${shiro.jwt.secret}")private static String secret;@Value("${shiro.jwt.expire}")private static Long expire;@Value("${shiro.jwt.header.alg}")private static String headerAlg;@Value("${shiro.jwt.header.typ}")private static String headerTyp;/*** 生成token*/public static String getToken(String account) {// 设置秘钥StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(account).append(secret);// 设置jwt头headerMap<String, Object> headerClaims = new HashMap<>();headerClaims.put("alg", headerAlg); // 签名算法headerClaims.put("typ", headerTyp); // token 类型// 设置jwt的header,负载paload以及加密算法String token = JWT.create().withHeader(headerClaims).withClaim("account" ,account).withClaim("expire", System.currentTimeMillis()+expire).sign(Algorithm.HMAC256(stringBuilder.toString()));return token;}/*** 无需秘钥就能获取其中的信息* 解析token.* {* "account": "account",* "timeStamp": "134143214"* }*/public static Map<String, String> parseToken(String token) {HashMap<String, String> map = new HashMap<String, String>();// 解码 JWTDecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");Claim expire = decodedJwt.getClaim("expire");map.put("account", account.asString());map.put("expire", expire.asLong().toString());return map;}/*** 解析token获取账号.*/public static String getAccount(String token) {HashMap<String, String> map = new HashMap<String, String>();DecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");return account.asString();}/*** 校验token是否正确* @param token Token* @return boolean 是否正确*/public static boolean verify(String token) {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(getAccount(token)).append(secret);// 帐号加JWT私钥解密Algorithm algorithm = Algorithm.HMAC256(stringBuilder.toString());JWTVerifier verifier = JWT.require(algorithm).build();try {verifier.verify(token);return true; // 验证成功} catch (JWTVerificationException e) {return false; // 验证失败}}}
import com.demo.util.JWTUtils;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;// 配置拦截器
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("token");if(StringUtils.isEmpty(token)){throw new Exception("token不能为空");}try {//在这里调用了 JWTUtils工具类的方法 验证传入token的合法性,你可以传token:111 试试JWTUtils.verify(token);} catch (SignatureVerificationException e) {log.error("无效签名! 错误 ->", e);return false;} catch (TokenExpiredException e) {log.error("token过期! 错误 ->", e);return false;} catch (AlgorithmMismatchException e) {log.error("token算法不一致! 错误 ->", e);return false;} catch (Exception e) {log.error("token无效! 错误 ->", e);return false;}return true;}
}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 配置要拦截哪些接口
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new JWTInterceptor())//拦截的路径.addPathPatterns("/**")//排除登录接口 /test/login 表示你给控制器起的名称/控制器下的方法,如login.excludePathPatterns("/test/login");}
}
Shiro
Shiro提供了哪些功能呢?
- **登录认证(Authentication):**Shiro可以对用户进行身份验证,确保用户是合法的。它支持多种认证方式,包括用户名/密码、基于证书的认证、第三方登录等。
- **访问授权(Authorization):**Shiro可以对用户进行授权,确定用户是否有权限执行某个操作或访问某个资源。它支持基于角色的访问控制和基于权限的访问控制,可以定义细粒度的权限规则。
- **会话管理(Session Management):**Shiro可以管理用户的会话,包括跟踪用户的登录状态、管理会话的生命周期、实现单点登录等功能。
- **密码加密(Password Encryption):**Shiro可以帮助应用程序安全地存储和验证用户密码,它提供了多种加密算法和技术,如哈希算法、加盐、散列迭代等。
- **RememberMe功能:**Shiro提供了RememberMe功能,可以在用户登录后记住用户的身份,下次访问时自动登录。
- **Web支持:**Shiro提供了与Web应用程序集成的支持,可以轻松地保护Web资源、处理表单登录、实现注销等功能。
- **缓存支持:**Shiro支持将重要数据(如用户信息、权限信息)缓存在内存中,提高系统的性能和响应速度。
不用害怕,因为我们就用到了**“登录认证”和"访问授权"**。本篇文章主要讲解“认证”、“授权”的功能。
主要模块讲解
①Realm用于获取用户信息,在这里可以给登录认证以及访问授权这两个事务查询用户相关数据,查询完用户数据之后,返回一个SimpleAuthorizationInfo类型的对象,交给SecurityManager管理。
②SecurityManager将从Realm得到的信息赋值给对应的subject用于进行登录认证或者访问授权。
③在一个用户登录到退出的整个过程,SecurityManager会一直为此用户保持一个会话session,会话信息存储在内存中,以便期间各种访问。
登录认证
shiro提供了方便的登录认证,可以通过subject.login(token)进行登录操作。
整体流程
①前端用户输入账号密码
②后端通过账号密码生成Shiro提供的UsernamePasswordToken类型的token
③调用shiro用户对象的登录方法subject.login(token)
④subject.login(token)会去调用多个方法,其中两个是public boolean supports(AuthenticationToken authenticationToken)
和protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
,前者判断所传入的token是不是shiro所支持的token也就是是不是UsernamePasswordToken类型的,后者用于获取用户信息。
⑤protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
方法讲解,在这个方法里面有两步,一步是根据token解析出来用户principal(也就是账号),然后使用principal去数据库或者其他数据源拿此用户对应的唯一凭证credentials(也就是密码),拿到之后创建SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
对象,传入三个参数,前两个是账号密码,最后一个是你自定义的realm类的名字。
⑥subject.login(token)就会比对realm返回的用户账号密码是否一致。
⑦除此之外,shiro还提供加密功能,比如用户的密码使用了md5加密,那么在配置类里面就可以声明加密的算法,之后用户调用subject.login(token)方法的时候就会自动给前端传给后端的密码加密,进而直接和realm中获取的数据进行比对。
代码实现
需要写两个类,一个shiroconfig配置类,一个realm的方法重写。
UserRealm
// 需要重写两个方法
public class UserRealm extends AuthorizingRealm
{/*** 授权:这里先不实现*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0){return null;}/*** 登录认证,自定义登录认证方式:账号是否存在,token是否过期* 重写之后,如果需要登录认证的接口就会自动调用此接口进行登录认证*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;String account = usernamePasswordToken.getUsername();// 然后根据账号从数据库或者其他数据源查新密码String password = select(account)// 这一行是伪代码,select换成自己的查询逻辑就行return new SimpleAuthenticationInfo(account, password, getName());// getName是AuthorizingRealm实现的方法,将会返回此类的名字,比如在这里就是“UserRealm”}}
ShiroConfig
@Configuration
public class ShiroConfig {// 初始化SecurityManager,把自定义的Realm交给SecurityManager管理@Bean(value = "defaultWebSecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();UserRealm userRealm = new UserRealm();defaultWebSecurityManager.setRealm(userRealm);return defaultWebSecurityManager;}
}
访问授权
访问授权就是通过配置shiro的拦截器拦截前端访问请求,然后再通过Realm获取用户的权限信息,如果访问用户有此权限就通过拦截器。
整体流程
①设置要拦截哪些路径或者接口,大概有两个方法:注解形式和拦截器中配置
②用户携带token访问后端接口
③shiro拦截器拦截,然后调用Realm的protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0)
方法通过查询数据库或者其他数据源获得此用户的权限信息,将权限信息添加到SimpleAuthorizationInfo info = new SimpleAuthorizationInfo()
实例中并返回。
代码实现
UserRealm
// 需要重写两个方法
public class UserRealm extends AuthorizingRealm
{/*** 授权:这里先不实现*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0){return null;}/*** 登录认证,自定义登录认证方式:账号是否存在,token是否过期* 重写之后,如果需要登录认证的接口就会自动调用此接口进行登录认证*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;String account = usernamePasswordToken.getUsername();// 然后根据账号从数据库或者其他数据源查新密码String password = select(account)// 这一行是伪代码,select换成自己的查询逻辑就行return new SimpleAuthenticationInfo(account, password, getName());// getName是AuthorizingRealm实现的方法,将会返回此类的名字,比如在这里就是“UserRealm”}}
ShiroConfig
@Configuration
public class ShiroConfig {// 初始化SecurityManager,把自定义的Realm交给SecurityManager管理@Bean(value = "defaultWebSecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();UserRealm userRealm = new UserRealm();defaultWebSecurityManager.setRealm(userRealm);return defaultWebSecurityManager;}/*** 添加自己的过滤器,自定义url规则,* Filter工厂,设置对应的过滤条件和跳转条件* Shiro自带拦截器配置规则* 详情见文档 http://shiro.apache.org/web.html#urls-** @date 2018/8/31 10:57*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 添加自己的过滤器并且取名为jwtMap<String, Filter> filterMap = new HashMap<>();filterMap.put("jwt", new JwtFilter());shiroFilterFactoryBean.setFilters(filterMap);Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// // 登出
// filterChainDefinitionMap.put("/logout", "logout");// 登录页面可以匿名访问filterChainDefinitionMap.put("/sys/login", "anon");
// // 首页需要身份验证后才能访问
// filterChainDefinitionMap.put("/index", "authc");
// // 错误页面,认证不通过跳转
// filterChainDefinitionMap.put("/error", "authc");
// // 其他页面需要具有 admin 角色才能访问
// filterChainDefinitionMap.put("/admin/**", "roles[admin]");
// // 其他页面需要具有 user:create 权限才能访问
// filterChainDefinitionMap.put("/sys/login", "perms[sys:login]");
// // 其他页面需要具有 user:update 和 user:delete 权限才能访问
// filterChainDefinitionMap.put("/user/manage", "perms[\"user:update,user:delete\"]");filterChainDefinitionMap.put("/**", "jwt"); // /**,一般放在最下,表示对所有资源起作用,使用JwtFiltershiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}/*开启注解的权限控制@RequiresAuthentication(标识用户必须在当前会话中进行了身份验证(登录)才能访问被注解的方法或类)@RequiresUser(标识用户必须在应用程序中进行了身份验证(不一定是当前会话)才能访问被注解的方法或类)@RequiresGuest(标识用户必须是一个“guest”(未经身份验证)才能访问被注解的方法或类)@RequiresRoles(标识用户必须具有指定的角色才能访问被注解的方法或类。可以指定一个或多个角色,如 @RequiresRoles("admin") 或 @RequiresRoles({"admin", "user"}))@RequiresPermissions(标识用户必须具有指定的权限才能访问被注解的方法或类。可以指定一个或多个权限,如 @RequiresPermissions("user:create") 或 @RequiresPermissions({"user:create", "user:update"}))@RequiresGuest(标识用户必须是一个“guest”(未经身份验证)才能访问被注解的方法或类)*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}
}
Redis
那为什么还要用Redis呢,从Shiro章节可以得知,如果要实现每次访问后端接口进行登录认证拦截的话,都要调用Realm中的登录认证方法,这样的话每次都要查询数据库,数据库压力太大,所以我们使用Redis来存储用户登录信息来解决这个问题,除此之外Redis里面还能存储更多前端经常要访问到的用户信息,省的经常去数据库里面查询了。
Springboot整合Shiro+Jwt+Redis
数据流向和项目结构
数据流向
项目结构
Redis配置
配置文件,配置redis地址
spring:redis:host: 10.1.40.83port: 6379password:database: 0timeout: 5000lettuce:pool:max-idle: 16max-active: 32min-idle: 8
配置Redis的常量
/*** 常量* @author dolyw.com* @date 2018/9/3 16:03*/
public class RedisConstant {private RedisConstant() {}public static final String PREFIX_REFRESH_TOKEN = "refresh_token";public static final String PREFIX_ACCESS_TOKEN = "access_token";public static final String PREFIX_SHIRO_EXPIRE = "access_token";public static final String PREFIX_SHIRO_JWT = "shiro:jwt:";}
config自动注入
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Bean(name = "redisTemplate")public RedisTemplate<String, Object> getRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {// 设置序列化Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 配置redisTemplateRedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(lettuceConnectionFactory);RedisSerializer<?> stringSerializer = new StringRedisSerializer();// key序列化redisTemplate.setKeySerializer(stringSerializer);// value序列化redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// Hash key序列化redisTemplate.setHashKeySerializer(stringSerializer);// Hash value序列化redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
import com.alibaba.excel.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** @className: RedisUtil* @description:* @author: sh.Liu* @date: 2022-03-09 14:07*/
// TODO: 2023/7/25 此工具类可以进一步优化
@Component
public class RedisUtil {// 使用jwt的过期时间毫秒private final long defaultTimeout = 1*24*60*60*1000;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 是否存在指定的key** @param key* @return*/public boolean hasKey(String key) {return Boolean.TRUE.equals(redisTemplate.hasKey(key));}/*** 删除指定的key** @param key* @return*/public boolean delete(String key) {return Boolean.TRUE.equals(redisTemplate.delete(key));}//- - - - - - - - - - - - - - - - - - - - - String类型 - - - - - - - - - - - - - - - - - - - -/*** 根据key获取值** @param key 键* @return 值*/public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);}/*** 将值放入缓存** @param key 键* @param value 值* @return true成功 false 失败*/public void set(String key, String value) {set(key, value, defaultTimeout);}/*** 将值放入缓存并设置时间** @param key 键* @param value 值* @param time 时间(秒) -1为无期限* @return true成功 false 失败*/public void set(String key, String value, long time) {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(key, value, defaultTimeout, TimeUnit.SECONDS);}}//- - - - - - - - - - - - - - - - - - - - - object类型 - - - - - - - - - - - - - - - - - - - -/*** 根据key读取数据*/public Object getObject(final String key) {if (StringUtils.isBlank(key)) {return null;}try {return redisTemplate.opsForValue().get(key);} catch (Exception e) {e.printStackTrace();}return null;}/*** 写入数据*/public boolean setObject(final String key, Object value) {if (StringUtils.isBlank(key)) {return false;}try {setObject(key, value , defaultTimeout);return true;} catch (Exception e) {e.printStackTrace();}return false;}public boolean setObject(final String key, Object value, long time) {if (StringUtils.isBlank(key)) {return false;}if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(key, value, defaultTimeout, TimeUnit.SECONDS);}return true;}
}
JWT配置
依赖
<!--jwt--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.19.0</version></dependency>
配置类
shiro:jwt:# 加密秘钥secret: f4e2e52034348f86b67cde581c0f9eb5# token有效时长,7天,单位毫秒expire: 604800000header:# 加密算法alg: HS256# token类型typ: JWT
JWT工具类
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;@Component
public class JwtUtil {@Value("${shiro.jwt.secret}")private String secret;@Value("${shiro.jwt.expire}")private Long expire;@Value("${shiro.jwt.header.alg}")private String headerAlg;@Value("${shiro.jwt.header.typ}")private String headerTyp;/*** 生成token*/public String getToken(String account, long currentTimeMillis) {// 设置秘钥StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(account).append(secret);// 设置jwt头headerMap<String, Object> headerClaims = new HashMap<>();headerClaims.put("alg", headerAlg); // 签名算法headerClaims.put("typ", headerTyp); // token 类型// 设置jwt的header,负载paload以及加密算法String token = JWT.create().withHeader(headerClaims).withClaim("account" ,account).withClaim("expire", currentTimeMillis + expire).sign(Algorithm.HMAC256(stringBuilder.toString()));return token;}/*** 无需秘钥就能获取其中的信息* 解析token.* {* "account": "account",* "timeStamp": "134143214"* }*/public Map<String, String> parseToken(String token) {HashMap<String, String> map = new HashMap<String, String>();// 解码 JWTDecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");Claim expire = decodedJwt.getClaim("expire");map.put("account", account.asString());map.put("expire", expire.asLong().toString());return map;}/*** 解析token获取账号.*/public String getAccount(String token) {HashMap<String, String> map = new HashMap<String, String>();DecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");return account.asString();}/*** 校验token是否正确* @param token Token* @return boolean 是否正确*/public boolean verify(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim account = decodedJwt.getClaim("account");Claim expire = decodedJwt.getClaim("expire");StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(account.asString()).append(secret);// 验证JWT的签名和有效性Algorithm algorithm = Algorithm.HMAC256(stringBuilder.toString());JWTVerifier verifier = JWT.require(algorithm).build();try {verifier.verify(token);return true; // 验证通过} catch (JWTVerificationException e) {return false; // 验证失败}}/*** 校验token是否过期* @param token Token* @return boolean 是否正确*/public boolean isExpired(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim expire = decodedJwt.getClaim("expire");// 验证过期时间Long expireTime = expire.asLong();if (System.currentTimeMillis() > expireTime) {return true;}return false;}/*** 获取token过期时间* @param token Token* @return boolean 是否正确*/public long getExpiredTime(String token) {DecodedJWT decodedJwt = JWT.decode(token);Claim expire = decodedJwt.getClaim("expire");return expire.asLong();}}
Shiro配置
ShiroConfig
@Configuration
public class ShiroConfig {/*** 添加自己的过滤器,自定义url规则,* Filter工厂,设置对应的过滤条件和跳转条件* Shiro自带拦截器配置规则* 详情见文档 http://shiro.apache.org/web.html#urls-** @date 2018/8/31 10:57*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 添加自己的过滤器并且取名为jwtMap<String, Filter> filterMap = new HashMap<>();filterMap.put("jwt", new JwtFilter());shiroFilterFactoryBean.setFilters(filterMap);Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// // 登出
// filterChainDefinitionMap.put("/logout", "logout");// 登录页面可以匿名访问filterChainDefinitionMap.put("/sys/login", "anon");
// // 首页需要身份验证后才能访问
// filterChainDefinitionMap.put("/index", "authc");
// // 错误页面,认证不通过跳转
// filterChainDefinitionMap.put("/error", "authc");
// // 其他页面需要具有 admin 角色才能访问
// filterChainDefinitionMap.put("/admin/**", "roles[admin]");
// // 其他页面需要具有 user:create 权限才能访问
// filterChainDefinitionMap.put("/sys/login", "perms[sys:login]");
// // 其他页面需要具有 user:update 和 user:delete 权限才能访问
// filterChainDefinitionMap.put("/user/manage", "perms[\"user:update,user:delete\"]");filterChainDefinitionMap.put("/**", "jwt"); // /**,一般放在最下,表示对所有资源起作用,使用JwtFiltershiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}@Bean(value = "defaultWebSecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(userRealm);//关闭shiro的session(无状态的方式使用shiro)DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();defaultSessionStorageEvaluator.setSessionStorageEnabled(false);subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);defaultWebSecurityManager.setSubjectDAO(subjectDAO);return defaultWebSecurityManager;}// 将自己的验证方式加入容器@Beanpublic UserRealm userRealm() {return new UserRealm();}/*开启注解的权限控制@RequiresAuthentication(标识用户必须在当前会话中进行了身份验证(登录)才能访问被注解的方法或类)@RequiresUser(标识用户必须在应用程序中进行了身份验证(不一定是当前会话)才能访问被注解的方法或类)@RequiresGuest(标识用户必须是一个“guest”(未经身份验证)才能访问被注解的方法或类)@RequiresRoles(标识用户必须具有指定的角色才能访问被注解的方法或类。可以指定一个或多个角色,如 @RequiresRoles("admin") 或 @RequiresRoles({"admin", "user"}))@RequiresPermissions(标识用户必须具有指定的权限才能访问被注解的方法或类。可以指定一个或多个权限,如 @RequiresPermissions("user:create") 或 @RequiresPermissions({"user:create", "user:update"}))@RequiresGuest(标识用户必须是一个“guest”(未经身份验证)才能访问被注解的方法或类)*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}/*** 由Spring管理 Shiro的生命周期*/@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}@Bean@DependsOn("lifecycleBeanPostProcessor")public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();// 强制使用cglib,防止重复代理和可能引起代理出错的问题defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);return defaultAdvisorAutoProxyCreator;}
}
Realm配置
import com.hebut.demo.common.constant.RedisConstant;
import com.hebut.demo.common.utils.JwtUtil;
import com.hebut.demo.common.utils.RedisUtil;
import org.apache.shiro.authc.*;
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 org.springframework.beans.factory.annotation.Autowired;import java.util.HashSet;
import java.util.Set;/*** 自定义Realm 处理登录 权限* * @author ruoyi*/
public class UserRealm extends AuthorizingRealm
{@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate JwtUtil jwtUtil;// 这个方法要重写,debug源码得知shiro会判断token的类型是不是自己支持的类型,不重写的话会报错@Overridepublic boolean supports(AuthenticationToken authenticationToken) {return authenticationToken instanceof JwtToken;}/*** 授权*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0){// 获取第一个身份(用户信息),由于在doGetAuthenticationInfo方法中返回的对象中principal参数传入的是token,所以这里获得的也是tokenString token = (String) arg0.getPrimaryPrincipal();String account = jwtUtil.getAccount(token);// 角色列表Set<String> roles = new HashSet<String>();// 根据账号从数据库或者其他数据源获取角色信息(这个操作省略不写了)// 比如查询到用户有一个admin的角色,在这里添加roles.add("admin");// 功能权限Set<String> menus = new HashSet<String>();// 根据账号从数据库或者其他数据源获取权限信息(这个操作省略不写了)// 比如查询到用户有一个sys:select的权限,在这里添加menus.add("sys:select");SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();// 管理员拥有所有权限info.setRoles(roles);info.setStringPermissions(menus);return info;}/*** 登录认证,自定义登录认证方式:账号是否存在,token是否过期* 重写之后,如果需要登录认证的接口就会自动调用此接口进行登录认证*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{System.out.println("登录认证");// JwtToken中重写了这个方法了String token = (String) authenticationToken.getCredentials();// 判断token是否有效(这里只是验证了签名是否有效)if (!jwtUtil.verify(token)){return null;}String account = jwtUtil.getAccount(token);if (jwtUtil.isExpired(token)){// 如果过期了,去redis里面查看refreshtime(这里没有实现这个步骤,直接过期了就退出)return null;}else {// 没有过期则判断token是否和redis里面存储的相等// 获取accessTokenString accessToken = redisUtil.getObject(RedisConstant.PREFIX_SHIRO_JWT + account + RedisConstant.PREFIX_ACCESS_TOKEN).toString();if(token.equals(accessToken)){// 认证通过则返回认证信息return new SimpleAuthenticationInfo(token, token, getName());}return null;}}}
重写AuthenticationToken
import org.apache.shiro.authc.AuthenticationToken;/*** @author: lhy* 自定义的shiro接口token,可以通过这个类将string的token转型成AuthenticationToken,可供shiro使用* 注意:需要重写getPrincipal和getCredentials方法,因为是进行三件套处理的,没有特殊配置shiro无法通过这两个 方法获取到用户名和密码,需要直接返回token,之后交给JwtUtil去解析获取。(当然了,可以对realm进行配 置HashedCredentialsMatcher,这里就不这么处理了)*/
public class JwtToken implements AuthenticationToken {private String token;public JwtToken(String token) {this.token = token;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}
}
继承BasicHttpAuthenticationFilter并重写,用于每次访问后端的时候做登录认证
import com.hebut.demo.common.shiro.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;/*** @author: lhy* jwt过滤器,作为shiro的过滤器,对请求进行拦截并处理跨域配置不在这里配了,我在另外的配置类进行配置了,这里把重心放在验证上*/
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter{/*** 过滤器拦截请求的入口方法*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {try {return executeLogin(request, response); //token验证} catch (Exception e) {e.printStackTrace();return false;}}/*** 进行token的验证*/@Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {//在请求头中获取tokenHttpServletRequest httpServletRequest = (HttpServletRequest) request;String token = httpServletRequest.getHeader("Authorization"); //前端命名Authorization//token不存在if(token == null || "".equals(token)){return false;}//token存在,进行验证JwtToken jwtToken = new JwtToken(token);getSubject(request, response).login(jwtToken); //通过subject,提交给myRealm进行登录验证return true;}/*** isAccessAllowed()方法返回false,即认证不通过时进入onAccessDenied方法*/
// @Override
// protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// return super.onAccessDenied(request, response);
// }/*** token认证executeLogin成功后,进入此方法,可以进行token更新过期时间*/
// @Override
// protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {// }
}
controller层异常拦截
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;// 用于拦截controller中抛出的异常
// 使用样例:@ControllerAdvice(basePackages="org.my.pkg")扫描此包下面所有的controller
@ControllerAdvice
public class GlobalExceptionHandler {// 拦截AuthorizationException异常@ExceptionHandler(AuthorizationException.class)@ResponseBodypublic String handleAuthorizationException(AuthorizationException e) {return "没有通过权限验证!";}
}