SpringSecurity使用教程

一、基本使用

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,专门设计用于保护基于 Spring 的应用程序。它不仅提供了全面的安全服务,还与 Spring 框架及其生态系统(如 Spring Boot、Spring MVC 等)紧密集成,简化了安全配置和实现。而认证和授权也是SpringSecurity作为安全框架的核心功能。

1.导入依赖

        <!--实现认证与授权功能。快速设置HTTP基本认证或表单登录--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId><version>2.2.4.RELEASE</version></dependency>

2.登录界面

SpringSecurity提供了登录和退出功能,导入SpringSecurity依赖后,必须登录后才能访问 

登录链接:http://localhost:8080/login

 默认情况下登录账号:user,密码在项目启动时会在控制台中打印

退出链接:http://localhost:8080/logout ​​​​​​

3.配置类

SpringSecurity的配置类,通过继承WebSecurityConfigurerAdapter 注解,在内存中设置账号密码

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {//配置用户信息服务@Beanpublic UserDetailsService userDetailsService() {//这里配置用户信息,这里暂时使用这种方式将用户存储在内存中InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build()); //拥有p1权限manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build()); //拥有p2权限return manager;}@Beanpublic PasswordEncoder passwordEncoder() {
//        //密码为明文方式,因为(配置类)内存中设置的密码是明文的,后期从数据库获取密文密码就用下面一个return NoOpPasswordEncoder.getInstance();
//          return new BCryptPasswordEncoder(); //密文,BCryptPasswordEncoder 会对输入的密码应用 bcrypt 算法并加上随机生成的盐值,然后将得到的散列值保存到数据库中}
}

二、RBAC权限模型

1.简介

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用 也是相对易用、通用权限模型。

 2.建表语句

  /*Table structure for table `sys_user` */DROP TABLE IF EXISTS `sys_user`;CREATE TABLE `sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` varchar(64) DEFAULT NULL COMMENT '邮箱',`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` varchar(128) DEFAULT NULL COMMENT '头像',`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';/*Table structure for table `sys_user_role` */DROP TABLE IF EXISTS `sys_user_role`;CREATE TABLE `sys_user_role` (`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',PRIMARY KEY (`user_id`,`role_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;/*Table structure for table `sys_role` */DROP TABLE IF EXISTS `sys_role`;CREATE TABLE `sys_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',`create_by` bigint(200) DEFAULT NULL,`create_time` datetime DEFAULT NULL,`update_by` bigint(200) DEFAULT NULL,`update_time` datetime DEFAULT NULL,`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';/*Table structure for table `sys_role_menu` */DROP TABLE IF EXISTS `sys_role_menu`;CREATE TABLE `sys_role_menu` (`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',PRIMARY KEY (`role_id`,`menu_id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;DROP TABLE IF EXISTS `sys_menu`;CREATE TABLE `sys_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',`path` varchar(200) DEFAULT NULL COMMENT '路由地址',`component` varchar(255) DEFAULT NULL COMMENT '组件路径',`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',`create_by` bigint(20) DEFAULT NULL,`create_time` datetime DEFAULT NULL,`update_by` bigint(20) DEFAULT NULL,`update_time` datetime DEFAULT NULL,`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

 3.查询某个用户的权限

SELECT 
DISTINCT m.`perms`FROMsys_user_role urLEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`WHEREuser_id = 2AND r.`status` = 0AND m.`status` = 0

 三、实体类

 1.用户表

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;import java.util.Date;//用户表(User)实体类@Data@AllArgsConstructor@NoArgsConstructor@TableName(value = "sys_user")public class User implements Serializable {private static final long serialVersionUID = -40356785423868312L;//序列化版本号@TableIdprivate Long id;//主键private String userName;//用户名private String nickName;//昵称private String password;//密码private String status;// 账号状态(0正常 1停用)private String email;//邮箱private String phonenumber;//手机号private String sex;// 用户性别(0男,1女,2未知)private String avatar;//头像private String userType;// 用户类型(0管理员,1普通用户)private Long createBy;//创建人的用户idprivate Date createTime;//创建时间private Long updateBy;//更新人private Date updateTime;//更新时间private Integer delFlag;//删除标志(0代表未删除,1代表已删除)}

 2.菜单表(权限表)

package com.xuexi.pojo;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import com.fasterxml.jackson.annotation.JsonInclude;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;import java.util.Date;//菜单(权限)表(Menu)实体类@TableName(value="sys_menu")@Data@AllArgsConstructor@NoArgsConstructor@JsonInclude(JsonInclude.Include.NON_NULL)//避免json结果返回null值public class Menu implements Serializable {private static final long serialVersionUID = -54979041104113736L;@TableIdprivate Long id;//主键private String menuName;//菜单名private String path;//路由地址private String component;//组件路径private String visible;//菜单状态(0显示 1隐藏)private String status;//菜单状态(0正常 1停用)private String perms;//权限标识private String icon;//菜单图标private Long createBy;private Date createTime;private Long updateBy;private Date updateTime;private Integer delFlag;//是否删除(0未删除 1已删除)private String remark;//备注}

 3.LoginUser

通过实现UserDetails 来存储当前登录用户的信息

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {private User user;private List<String> permissions;//存储SpringSecurity所需要的权限信息的集合@JSONField(serialize = false) // json序列化时忽略private List<SimpleGrantedAuthority>  authorities;public LoginUser(User user,List<String> permissions) {this.user = user;this.permissions = permissions;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if(authorities!=null){return authorities;}authorities = new ArrayList<>();for(String permission : permissions){SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);authorities.add(authority);}return authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

四、SpringSecurity相关配置

1.SecurityConfig 配置类

import com.xuexi.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启基于注解的权限认证public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // token校验过滤器@Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint; // 认证失败处理类@Autowiredprivate AccessDeniedHandler accessDeniedHandler; // 权限不足处理类@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();//密文}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //关闭session.and().authorizeRequests() // 请求权限配置// 对于登录接口 允许匿名访问.antMatchers("/user/register").permitAll() //表示所有用户都可以访问(不管有没有登录都可以访问).antMatchers("/user/login").anonymous() // 匿名访问(没有登录可以访问)// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//将jwtAuthenticationTokenFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器之前http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);http.cors();//开启SpringSecurity的跨域访问}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}

1.configure配置

默认情况下,SpringSecurity提供了默认的登录界面,在前后端分离项目中,不要需要使用默认的登录界面

重写configure方法:

        1.关闭csrf攻击,关闭session。csrf攻击是基于session,本项目采用jwt令牌,不需要考虑csrf攻击

        2.设置允许访问的路径

        3.在用户登录之前,添加过滤器jwtAuthenticationTokenFilter

        4.配置异常处理器,分为 认证异常处理器 权限异常处理器

        5.允许SpringSecurity跨域,后续还需要设置运行SpringBoot跨域

import com.xuexi.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启基于注解的权限认证public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // token校验过滤器@Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint; // 认证失败处理类@Autowiredprivate AccessDeniedHandler accessDeniedHandler; // 权限不足处理类@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //关闭session.and().authorizeRequests() // 请求权限配置// 对于登录接口 允许匿名访问.antMatchers("/user/register").permitAll() //表示所有用户都可以访问(不管有没有登录都可以访问).antMatchers("/user/login").anonymous() // 匿名访问(没有登录可以访问)// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//将jwtAuthenticationTokenFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器之前http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);http.cors();//开启SpringSecurity的跨域访问}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}}

 2.AuthenticationManager 认证

 在Spring Security中,AuthenticationManager  负责处理认证请求。默认情况下,Spring Security会创建并管理 AuthenticationManager 的实例,但是这个实例并不会自动暴露为Spring容器中的一个bean。因此,需要手动将其配置为一个bean。

3.权限认证具体实现

该方法在用户登录中进行使用,用户判断用户登录的账号密码是否正确

@Autowired
private AuthenticationManager authenticationManager;UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);//认证

 执行authenticationManager.authenticate(authenticationToken)时,会进行账号密码认证

账号认证

通过实现UserDetailsService接口重写loadUserByUsername方法进行账号认证

这个方法的主要职责是根据提供的用户名从数据库中加载用户信息,根据用户id获取用户的权限信息,而不涉及密码验证

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuexi.mapper.MenuMapper;
import com.xuexi.mapper.UserMapper;
import com.xuexi.pojo.User;
import com.xuexi.pojo.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate MenuMapper menuMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询用户信息LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUserName,username);User user = userMapper.selectOne(wrapper);//如果查询不到数据就通过抛出异常来给出提示if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}//TODO 根据用户查询权限信息 添加到LoginUser中//封装成UserDetails对象返回List<String> permissionKeyList =menuMapper.selectPermsByUserId(user.getId());//权限查询return new LoginUser(user,permissionKeyList);}}

 权限查询

    <select id="selectPermsByUserId" resultType="java.lang.String">SELECTDISTINCT m.`perms`FROMsys_user_role urLEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`WHEREuser_id = #{id}AND r.`status` = 0AND m.`status` = 0</select>

 密码验证

密码验证是在调用 AuthenticationManager.authenticate 方法时由Spring Security自动处理的

2.过滤器

在用户登录之前,需要 过滤器 检验

     1.若请求头中不含token,直接放行,进行登录   

      2.若请求头中含token,先对token进行解析,获取用户id。根据在id,在redis中查询该用户所具有的权限,

       3.设置用户权限后放行

 创建 filter包,配置过滤器

import com.xuexi.config.RedisCache;
import com.xuexi.pojo.LoginUser;
import com.xuexi.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain) throws ServletException,IOException {//获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {//放行filterChain.doFilter(request, response);return;}//解析tokenString userid;try {Claims claims = JwtUtil.parseJWT(token);userid = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}//从redis中获取用户信息String redisKey = "login:" + userid;LoginUser loginUser = redisCache.getCacheObject(redisKey);if(Objects.isNull(loginUser)){throw new RuntimeException("用户未登录");}//存入SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}

JWT工具类

导入依赖

        <!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency>

工具类 

 import io.jsonwebtoken.Claims;import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import javax.crypto.SecretKey;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;import java.util.Date;import java.util.UUID;/*** JWT工具类*/public class JwtUtil {//有效期为public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时//设置秘钥明文public static final String JWT_KEY = "sangeng";public static String getUUID(){String token = UUID.randomUUID().toString().replaceAll("-", "");return token;}/*** 生成jtw* @param subject token中要存放的数据(json格式)* @return*/public static String createJWT(String subject) {JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间return builder.compact();}/*** 生成jtw* @param subject token中要存放的数据(json格式)* @param ttlMillis token超时时间* @return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis,String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid)              //唯一的ID.setSubject(subject)   // 主题  可以是JSON数据.setIssuer("sg")     // 签发者.setIssuedAt(now)      // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token* @param id* @param subject* @param ttlMillis* @return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}/*** 生成加密后的秘钥 secretKey* @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析** @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}public static void main(String[] args) throws Exception {String jwt = createJWT("1234");System.out.println(jwt);Claims claims = parseJWT(jwt);System.out.println(claims);System.out.println(claims.getSubject());}}

 3.异常处理器

导入fastjson依赖

        <!--fastjson依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency>

认证失败处理器

package com.xuexi.handle;import com.alibaba.fastjson.JSON;
import com.xuexi.result.ResponseResult;
import com.xuexi.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class AuthenticationEntryPointImpl  implements AuthenticationEntryPoint {/*** 认证失败处理器* @param httpServletRequest* @param httpServletResponse* @param e* @throws IOException* @throws ServletException*/@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {ResponseResult result = new  ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");//401String json = JSON.toJSONString(result);WebUtils.renderString(httpServletResponse,json);}
}

 权限异常处理器

import com.alibaba.fastjson.JSON;
import com.xuexi.result.ResponseResult;
import com.xuexi.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Componentpublic class AccessDeniedHandlerImpl implements AccessDeniedHandler {/*** 权限异常处理器* @param request* @param response* @param accessDeniedException* @throws IOException* @throws ServletException*/@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response,AccessDeniedException accessDeniedException) throws IOException, ServletException {ResponseResult result = new  ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足");String json = JSON.toJSONString(result);WebUtils.renderString(response,json);}}

 返回值工具类

package com.xuexi.result;import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
// Java 对象序列化为 JSON 字符串时,可以确保那些值为 null 的字段不会出现在最终的 JSON 输出中。
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
public class ResponseResult<T> {private Integer code;private String msg;private T data;public ResponseResult(Integer code, String msg) {this.code = code;this.msg = msg;}public ResponseResult(Integer code, T data) {this.code = code;this.data = data;}public ResponseResult(Integer code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}}

客户端响应工具类 

import javax.servlet.http.HttpServletResponse;import java.io.IOException;public class WebUtils{/*** 将字符串渲染到客户端* * @param response 渲染对象* @param string 待渲染的字符串* @return null*/public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);}catch (IOException e){e.printStackTrace();}return null;}}

 五、权限校验方法

注解实现

@PreAuthorize注解

hasAuthority方法:只能传入一个权限

hasAnyAuthority方法:可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源

hasRole方法:hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所 以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

hasAnyRole方法:有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以 这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

 自定义权限表达式

我们可以在自己定义expression包下,创建自己的权限校验方法,然后在@PreAuthorize注解中使用自己的方法。

import com.xuexi.pojo.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;import java.util.List;@Component("ex")public class SGExpressionRoot {public boolean hasAuthority(String authority){//获取当前用户的权限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();List<String> permissions = loginUser.getPermissions();//判断用户权限集合中是否存在authorityreturn permissions.contains(authority);}}

 在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的 hasAuthority方法

    @RequestMapping("/hello")@PreAuthorize("@ex.hasAuthority('system:dept:list')")public String hello(){return "hello";}

 六、登录退出功能的实现

import com.xuexi.pojo.User;
import com.xuexi.result.ResponseResult;
import com.xuexi.service.LoginServcie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class LoginController {@Autowiredprivate LoginServcie loginServcie;@PostMapping("/user/login")public ResponseResult login(@RequestBody User user){return loginServcie.login(user);}@RequestMapping("/user/logout")public ResponseResult logout(){return loginServcie.logout();}}

用户登录

        1.对账号密码验证通过后,将用户id传入创建JWT令牌中,返回给前端。

        2.登录时将用户信息存入redis中,当用户登录后,在过滤器中对JWT令牌进行解析,获取令牌中存入的用户id,根据用户id在redis中进行查询,判断用户是否登录。

        3。若redis中不存在该用户,抛出异常,此时在 认证失败处理器 提示:认证失败请重新登录"

用户退出:从SpringSecurity获取用户id,根据用户id删除reids中存储的用户信息

        

import com.xuexi.config.RedisCache;
import com.xuexi.pojo.User;
import com.xuexi.pojo.LoginUser;
import com.xuexi.result.ResponseResult;
import com.xuexi.service.LoginServcie;
import com.xuexi.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Objects;@Service
public class LoginServiceImpl implements LoginServcie {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(User user) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);//认证if (Objects.isNull(authenticate)) {throw new RuntimeException("用户名或密码错误");}//使用userid生成tokenLoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userId = loginUser.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);//authenticate存入redisredisCache.setCacheObject("login:" + userId, loginUser);//把token响应给前端HashMap<String, String> map = new HashMap<>();map.put("token", jwt);return new ResponseResult(200, "登陆成功", map);}@Overridepublic ResponseResult logout() {//获取SecurityContextHolder中的用户idAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();Long userid = loginUser.getUser().getId();redisCache.deleteObject("login:"+userid);return new ResponseResult(200,"退出成功");}
}

 七、其他

1.reids配置类

import com.xuexi.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configurationpublic class RedisConfig {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}}

2.redis序列化工具类

        <!--fastjson依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency>
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature;import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;import org.springframework.data.redis.serializer.RedisSerializer;import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;import java.nio.charset.Charset;/*** Redis使用FastJson序列化*/public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null) {return new byte[0];}return JSON.toJSONString(t,SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}protected JavaType getJavaType(Class<?> clazz){return TypeFactory.defaultInstance().constructType(clazz);}}

3.redis工具类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.*;import java.util.concurrent.TimeUnit;@SuppressWarnings(value = { "unchecked", "rawtypes" })@Componentpublic class RedisCache {@Autowiredpublic RedisTemplate redisTemplate;public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等* @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间* @param key Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间* @param key Redis键* @param timeout 超时时间* @param unit 时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。* @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象* @param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 删除集合对象* @param collection 多个对象* @return*/public long deleteObject(final Collection collection){return redisTemplate.delete(collection);}/*** 缓存List数据* @param key 缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象* @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set* @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set* @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map* @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map* @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据* @param key Redis键* @param hKey Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据* @param key Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 删除Hash中的数据* @param key* @param hkey*/public void delCacheMapValue(final String key, final String hkey){HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hkey);}/*** 获取多个Hash中的数据* @param key Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表* @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}}

4.SpringBoot跨域配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class CorsConfig implements WebMvcConfigurer {/*** 先对SpringBoot配置,运行跨域请求* @param registry*/@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域请求的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods("GET", "POST", "DELETE", "PUT")// 设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}}

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

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

相关文章

docker 安装mysql 5.7 详细保姆级教程

1. 安装mysql(5.7) docker pull mysql:5.7 若是拉取不了&#xff0c;可以配置下 docker 源 2. 查看是否安装成功 docker images 下图就是成功了 3.创建mysql专用目录、数据挂载目录、配置文件目录 &#xff0c;演示目录在于/home/下 //命令逐条执行cd /home/ mkdir mysql …

fixture装饰器

普通代码案例&#xff1a; python本身执行 import pytestdef init_new():print("init_new...")return Truedef test_case(init_new):if init_new is True:print("如果init_new返回True&#xff0c;就执行用例test_case")if __name__ __main__:#用python本…

【韩顺平Java JDBC学习笔记】

Java JDBC 文章目录 jdbc概述基本介绍jdbc原理示意图 jdbc快速入门JDBC程序编写步骤获取数据库连接5种方式ResultSet[结果集]SQL注入Statement PreparedStatement预处理好处基本使用 JDBC APIJDBCUtils工具类使用工具类 事务基本介绍应用实例模拟经典的转帐业务 - 未使用事务模…

KeepAlive与RouterView缓存

参考 vue动态组件&#xff1c;Component&#xff1e;与&#xff1c;KeepAlive&#xff1e; KeepAlive官网介绍 缓存之keep-alive的理解和应用 Vue3Vite KeepAlive页面缓存问题 vue多级菜单(路由)导致缓存(keep-alive)失效 vue3 router-view keeperalive对于同一路径但路径…

【经验分享】搭建本地训练环境知识点及方法

最近忙于备考没关注&#xff0c;有次点进某小黄鱼发现首页出现了我的笔记还被人收费了 虽然我也卖了一些资源&#xff0c;但我以交流、交换为主&#xff0c;笔记都是免费给别人看的 由于当时刚刚接触写的并不成熟&#xff0c;为了避免更多人花没必要的钱&#xff0c;所以决定公…

为什么要使用数据仓库?

现状和需求 大量的企业经营性数据&#xff08;订单&#xff0c;库存&#xff0c;原料&#xff0c;付款等&#xff09;在企业的业务运营系统以及其后台的(事务型)数据库中产生的。 企业的决策者需要及时地对这些数据进行归类分析&#xff0c;从中获得企业运营的各种业务特征&a…

CSS|07 标准文档流

标准文档流 一、什么是标准文档流 在制作的 HTML 网页和 PS 画图软件画图时有本质上面的区别: HTML 网页在制作的时候都得遵循一个“流的规则:从左至右、从上至下。 使用 Ps 软件画图时可以在任意地方画图。 <!DOCTYPE html> <html lang"en"> <hea…

JS设计模式之访问者模式

前言 访问者模式&#xff08;Visitor Pattern&#xff09;是一种 行为设计模式&#xff0c;它允许在不改变对象结构的情况下&#xff0c;定义新的操作。 这种模式通过将操作封装在访问者对象中&#xff0c;使得可以在不修改被访问对象的情况下&#xff0c;增加新的功能。 本…

快速上手:利用 FFmpeg 合并音频文件的实用教程

FFmpeg 是一个强大的多媒体处理工具&#xff0c;能够轻松地对音频、视频进行编辑和转换。本文将介绍如何使用 FFmpeg 来合并&#xff08;拼接&#xff09;多个音频文件为一个单一文件。无论您是想要创建播客、音乐混音还是其他任何形式的音频项目&#xff0c;这都是一个非常实用…

使用idea创建一个JAVA WEB项目

文章目录 1. javaweb项目简介2. 创建2.1 idea新建项目2.2 选择&#xff0c;命名2.3 打开2.4 选择tomcat运行2.5 结果 3. 总结 1. javaweb项目简介 JavaWeb项目是一种基于Java技术的Web应用程序&#xff0c;主要用于开发动态网页和Web服务。这种项目能够构建在Java技术栈之上&a…

鸿蒙NEXT开发案例:九宫格随机

【引言】 在鸿蒙NEXT开发中&#xff0c;九宫格抽奖是一个常见且有趣的应用场景。通过九宫格抽奖&#xff0c;用户可以随机获得不同奖品&#xff0c;增加互动性和趣味性。本文将介绍如何使用鸿蒙开发框架实现九宫格抽奖功能&#xff0c;并通过代码解析展示实现细节。 【环境准…

金融信息分析基础(1)

1.金融数据 金融数据分为&#xff1a;交易数据&#xff08;低频数据&#xff0c;高频数据&#xff0c;超高频数据&#xff09;&#xff0c;报表数据&#xff08;财务报表&#xff0c;研报&#xff09;&#xff0c;金融社交媒体数据 低频数据&#xff1a; 以日、周、月、季、年…

.NET 技术系列 | 通过CreatePipe函数创建管道

01阅读须知 此文所提供的信息只为网络安全人员对自己所负责的网站、服务器等&#xff08;包括但不限于&#xff09;进行检测或维护参考&#xff0c;未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失&#xf…

docker安装、升级、以及sudo dockerd --debug查看启动失败的问题

1、docker安装包tar下载地址 Index of linux/static/stable/x86_64/ 2、下载tgz文件并解压 tar -zxvf docker-24.0.8.tgz 解压后docker文件夹下位docker相关文件 3、将老版本docker相关文件&#xff0c;备份 将 /usr/bin/docker下docker相关的文件&#xff0c;mv到备份目录…

uniapp——H5中使用富文本编辑器,如何使用。

一、插件市场 去插件市场找到这个插件https://ext.dcloud.net.cn/plugin?id14726 二、引入 找到自己项目引入 项目里面多了很多文件 三、使用 找到A页面&#xff0c;在里面引入组件 <view class"editBox"><sp-editor exportHtml"handleExpor…

arXiv-2024 | VLM-GroNav: 基于物理对齐映射视觉语言模型的户外环境机器人导航

作者&#xff1a; Mohamed Elnoor, Kasun Weerakoon, Gershom Seneviratne, Ruiqi Xian, Tianrui Guan, Mohamed Khalid M Jaffar, Vignesh Rajagopal, and Dinesh Manocha单位&#xff1a;马里兰大学学院公园分校原文链接&#xff1a;VLM-GroNav: Robot Navigation Using Phys…

音频客观测评方法PESQ

一、简介 语音质量感知评估&#xff08;Perceptual Evaluation of Speech Quality&#xff09;是一系列的标准&#xff0c;包括一种用于自动评估电话系统用户所体验到的语音质量的测试方法。该标准于2001年被确定为ITU-T P.862建议书[1]。PESQ被电话制造商、网络设备供应商和电…

Gitlab服务管理和仓库项目权限管理

Gitlab服务管理 gitlab-ctl start # 启动所有 gitlab 组件&#xff1b; gitlab-ctl stop # 停止所有 gitlab 组件&#xff1b; gitlab-ctl restart # 重启所有 gitlab 组件&#xff1b; gitlab-ctl status …

浏览器插件开发实战

浏览器插件开发实战 [1] 入门DEMO一、创建项目二、创建manifest.json三、加载插件四、配置 service-worker.js五、以书签管理器插件为例manifest.jsonpopup.htmlpopup.js查看效果 [2] Vue项目改造成插件一、复习Vue项目的结构二、删除、添加个别文件三、重写build [3] 高级开发…

SpringBoot集成JWT和Redis实现鉴权登录功能

目前市面上有许多鉴权框架&#xff0c;鉴权原理大同小异&#xff0c;本文简单介绍下利用JWT和Redis实现鉴权功能&#xff0c;算是抛砖引玉吧。 主要原理就是“令牌主动失效机制”&#xff0c;主要包括以下4个步骤&#xff1a; (1)利用拦截器LoginInterceptor实现所有接口登录拦…