Shiro+Jwt+Redis

如何整合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 认证开始说起。

前置知识


**会话:**每个用户的一次登录到登出之间叫做一个会话。



登录状态:“无状态”和“有状态”是指对于服务器而言的两种不同的处理方式:

  1. 无状态(Stateless):在无状态的认证机制中,服务器不需要保存任何关于客户端的状态信息。每次客户端发送请求时,服务器只需要对请求进行处理,而无需考虑之前的请求状态。这意味着服务器可以更容易地进行水平扩展,因为不需要担心请求会被路由到特定的服务器上。
  2. 有状态(Stateful):相比之下,在有状态的认证机制中,服务器需要保存客户端的状态信息,通常通过会话对象或其他方式来记录客户端的状态。这意味着服务器需要在多个请求之间共享状态信息,可能需要使用特定的机制来保证状态的一致性和可靠性。

session认证

image-20240519190947325

​ 众所周知,http 协议本身是无状态的协议(http 是一种无状态协议,就是说每次用户进行用户名和密码认证之后,http 不会留下记录,下一次请求还需要进行认证。因为http 不知道每次请求是哪一个用户发出的)。

​ session认证就是说用户登录后把将此用户的登录状态存储到服务器的内存中。

​ session 认证的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。

token认证

image-20240519191004738

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提供了哪些功能呢?

  1. **登录认证(Authentication):**Shiro可以对用户进行身份验证,确保用户是合法的。它支持多种认证方式,包括用户名/密码、基于证书的认证、第三方登录等。
  2. **访问授权(Authorization):**Shiro可以对用户进行授权,确定用户是否有权限执行某个操作或访问某个资源。它支持基于角色的访问控制和基于权限的访问控制,可以定义细粒度的权限规则。
  3. **会话管理(Session Management):**Shiro可以管理用户的会话,包括跟踪用户的登录状态、管理会话的生命周期、实现单点登录等功能。
  4. **密码加密(Password Encryption):**Shiro可以帮助应用程序安全地存储和验证用户密码,它提供了多种加密算法和技术,如哈希算法、加盐、散列迭代等。
  5. **RememberMe功能:**Shiro提供了RememberMe功能,可以在用户登录后记住用户的身份,下次访问时自动登录。
  6. **Web支持:**Shiro提供了与Web应用程序集成的支持,可以轻松地保护Web资源、处理表单登录、实现注销等功能。
  7. **缓存支持:**Shiro支持将重要数据(如用户信息、权限信息)缓存在内存中,提高系统的性能和响应速度。

​ 不用害怕,因为我们就用到了**“登录认证”和"访问授权"**。本篇文章主要讲解“认证”、“授权”的功能。

主要模块讲解

image-20240521094154037

①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

数据流向和项目结构

数据流向

image-20240521214547303

项目结构

image-20240521145229144

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 "没有通过权限验证!";}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/16837.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

生命在于学习——Python人工智能原理(2.1)

二、机器学习 1、机器学习的定义 机器学习是指从有限的观测数据中学习出具有一般性的规律&#xff0c;并利用这些规律对未知数据进行预测的方法&#xff0c;通俗的讲&#xff0c;机器学习就是让计算机从数据中进行自动学习&#xff0c;得到某种知识。 传统的机器学习主要关注…

1分钟快速掌握JSON格式

文章目录 先说理论代码举例对象型数组型总结 先说理论 下面是JSON的几种简单数据类型: 数据类型描述数字型JavaScript中的双进度浮点类型&#xff0c;通常根据具体情况定义&#xff0c;这里是没有特殊的整形的。字符串型带双引号的Unicode&#xff0c;带反斜杠转义布尔型true…

图形学初识--双线性插值算法

文章目录 为什么需要双线性插值算法&#xff1f;双线性插值算法是什么&#xff1f;如何双线性插值&#xff1f;结尾&#xff1a;喜欢的小伙伴可以点点关注赞哦 为什么需要双线性插值算法&#xff1f; ChatGP回答&#xff1a; 双线性插值&#xff08;bilinear interpolation&am…

AI绘画图生图有什么用?

随着AI渗透到我们生活中的各个角落&#xff0c;AI绘画图生图的出现&#xff0c;更是在艺术领域引起了广泛的关注和讨论。那么&#xff0c;AI绘画图生图究竟有什么作用呢? 首先&#xff0c;AI绘画图生图能够极大地提高创作效率。传统的绘画过程需要艺术家们花费大量的时间和精力…

2024年怎么下载学浪app视频

想要在2024年紧跟潮流&#xff0c;成为一名优秀的学浪用户吗&#xff1f;今天就让我们一起探索如何下载学浪app视频吧&#xff01; 学浪视频下载工具打包 学浪下载工具打包链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;1234 --来自百度网盘超级会员V10的分享…

第14章-蓝牙遥控小车 手把手做蓝牙APP遥控小车 蓝牙串口通讯讲解

本文讲解手机蓝牙如何遥控小车&#xff0c;如何编写串口通信指令 第14章-手机遥控功能 我们要实现蓝牙遥控功能&#xff0c;蓝牙遥控功能要使用:1.单片机的串口、2.蓝牙通信模块 所以我们先调试好:单片机的串口->蓝牙模块->接到一起联调 14.1-电脑控制小车 完成功能…

网络工程师备考2——vlan

vlan 1、什么是VLAN&#xff1f; VLAN&#xff08;Virtual LAN&#xff09;&#xff0c;翻译成中文是“虚拟局域网”。LAN可以是由少数几台家用计算机构成的网络&#xff0c;也可以是数以百计的计算机构成的企业网络。VLAN所指的LAN特指使用路由器分割的网络——也就是广播域…

ABeam 德硕 Team Building | SDC Green Day——环保公益行动

山野好拾光 春日公益行 继上年度大连办公室Green Day活动的顺利举办&#xff0c;环保的理念更加深入到ABeam每一位员工的心中。春日天气晴好&#xff0c;西安办公室的小伙伴们也迫不及待来上一场说走就走的Green Day Outing活动。 本次环保公益行动主题为「夏日Go Green畅享山…

千亿级开源大模型Qwen110B部署实测

近日&#xff0c;通义千问团队震撼开源 Qwen1.5 系列首个千亿参数模型 Qwen1.5-110B-Chat。 千亿级大模型普通显卡是跑不了推理的&#xff0c;普通人一般也没办法本地运行千亿级大模型。 为了探索千亿级大模型到底需要计算资源&#xff0c;我用云计算资源部署了Qwen1.5-110B-…

谷歌AI搜索功能“翻车”,用户体验引担忧

近期&#xff0c;谷歌对其搜索引擎进行重大更新&#xff0c;推出了全新AI搜索功能“AI Overview”&#xff0c;试图通过人工智能技术提供更智能便捷的搜索体验&#xff0c;并追赶微软和OpenAI等竞争对手。然而事与愿违&#xff0c;这项备受期待的功能上线后却频频出错&#xff…

测试基础06:软件产品的运行环境dev、sit、test、fat、uat、pre、pro

​​​​​​​课程大纲 1、Dev开发环境 &#xff08;Development environment&#xff09; 使用者 开发人员使用。 用途 用于编程&#xff0c;版本变动很大。 外部能否访问 外部用户无法访问。 2、sit/ITE系统集成测试环境 &#xff08;System Integration Testing en…

30多万汉字词语押韵查询ACCESS\EXCEL数据库

押韵&#xff0c;也作“压韵”。作诗词曲赋等韵文时在句末或联末用同韵的字相押&#xff0c;称为押韵。诗歌押韵&#xff0c;使作品声韵和谐&#xff0c;便于吟诵和记忆&#xff0c;具有节奏和声调美。旧时押韵&#xff0c;要求韵部相同或相通&#xff0c;也有少数变格。现代新…

《开发问题解决》Window下7z解压:cannot create symbolic link : 客户端没有所需的特权

问题描述&#xff1a; 今天使用7z来解压东西的是突然出现这个问题。 问题解决&#xff1a; download直接下载到c盘中&#xff0c;由于所在文件夹有权限限制。无法进行正常解压。 7.zip解压时使用管理员权限进行解压&#xff0c;解压时使用管理员权限。即如图 使用管理员权限重…

【面试干货】找出一个偶数能够表示为两个素数之和的所有可能情况

【面试干货】找出一个偶数能够表示为两个素数之和的所有可能情况 1、实现思想2、代码实现 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1、实现思想 功能&#xff1a;通过循环遍历奇数&#xff0c;找出一个大于等于 6 的偶数能够表示为两…

【C++初阶】auto关键字

目录 1.auto简介 2.auto的使用 1.auto简介 在早期C/C中auto的含义是&#xff1a;使用auto修饰的变量&#xff0c;是具有自动存储器的局部变量&#xff0c;但遗憾的 是一直没有人去使用它&#xff0c;大家可思考下为什么&#xff1f; C11中&#xff0c;标准委员会赋予了auto全…

红队项目PinkysPalace格式字符串缓冲区溢出详解

简介 渗透测试-地基篇 该篇章目的是重新牢固地基&#xff0c;加强每日训练操作的笔记&#xff0c;在记录地基笔记中会有很多跳跃性思维的操作和方式方法&#xff0c;望大家能共同加油学到东西。 请注意&#xff1a; 本文仅用于技术讨论与研究&#xff0c;对于所有笔记中复现的…

视频白平衡没调好怎么补救 视频白平衡调整用哪些参数 会声会影视频制作教程

没有调不好的白平衡&#xff01;如果有&#xff0c;那就是你的方法没用对。无论你的视频发黄还是发蓝&#xff0c;只要掌握本文提供的方法&#xff0c;简单几步就能纠正色偏、校准白平衡。操作很简单&#xff0c;几乎所有人都能够轻松掌握。有关视频白平衡没调好怎么补救&#…

Android 布局中@NULL的使用和代码实现方式详解

文章目录 1、使用场景2、示例代码实现2.1、移除背景2.2 、移除文本2.3、移除布局宽度或高度2.4、移除提示文本2.5、移除图像资源 3、综合示例3.1、布局文件 activity_main.xml3.2、主活动文件 MainActivity.java3.4、资源文件3.5、运行结果 4、优点5、缺点6、综合分析6.1、适用…

.net core web项目部署IIS报错:HTTP 错误 413.1 - Request Entity Too Large

HTTP 错误 413.1 - Request Entity Too Large 解决办法 这个报错的原因是因为IIS配置问题&#xff0c;IIS最大默认配置只有30M&#xff0c;超过30M就会报错 解决办法 在程序中配置能接收最大字节大小 //配置请求头中能最大接收多少数据 //builder.WebHost.UseKestrel(option…

VGG论文解析—Very Deep Convolutional Networks for Large-Scale Image Recognition

VGG论文解析—Very Deep Convolutional Networks for Large-Scale Image Recognition -2015 研究背景 大规模图像识别的深度卷积神经网络 VGG&#xff08;牛津大学视觉几何组&#xff09; 认识数据集&#xff1a;ImageNet的大规模图像识别挑战赛 LSVRC-2014&#xff1a;Image…