当前位置: 首页 > news >正文

SpringSecurity框架【认证】

目录

一. 快速入门

二. 认证

2.1 登陆校验流程

2.2 原理初探

2.3 解决问题

2.3.1 思路分析

2.3.2 准备工作

2.3.3 实现

2.3.3.1 数据库校验用户

2.3.3.2 密码加密存储

2.3.3.3 登录接口

2.3.3.4 认证过滤器

2.3.3.5 退出登录


Spring Security是Spring家族中的一个安全管理框架,相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。

一般来说大型项目用Spring Security比较多,小项目用Shiro比较多,因为相比于Spring Security,Shiro上手比较简单。

一般Web应用需要进行认证授权

  • 认证:验证当前访问系统的是不是本系统用户,并且要确认具体是哪个用户
  • 授权:经过认证后判断当前用户是否有权限进行某个操作

而认证和授权正是Spring Security作为安全框架的核心功能!

一. 快速入门

我们先简单构建出一个SpringBoot项目。

这个时候我们访问我们写的一个简单的hello接口,验证是否构建成功。

接着引入SpringSecurity。

这个时候我们再看看访问接口的效果。

引入了SpringSecurity之后,访问接口会自动跳转到一个登录页面,默认的用户名是user,密码会输出到控制台,必须登录后才能对接口进行访问。

二. 认证

2.1 登陆校验流程

首先我们要先了解登录校验流程,首先前端携带用户名和密码访问登录接口,服务器拿到这个用户名和密码之后去和数据库中的进行比较,如果正确使用用户名/用户ID,生成一个jwt,接着把jwt响应给前端,之后登录后访问其他的请求都会在请求头中携带token,服务器每次获取请求头中的token进行解析、获取UserID,根据用户名id获取用户相关信息,查看器权限,如果有权限则响应给前端。

2.2 原理初探

SpringSecurity的原理其实就是一个过滤器链,内部提供了各种功能的过滤器,这里我们先看看上方快速入门中涉及的过滤器。

  • UsernamePasswordAuthenticationFilter负责处理在登录页面填写的用户名密码后的登录请求
  • ExceptionTranslationFilter处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException
  • FilterSecurityInterceptor负责权限校验的过滤器

我们也可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器以及顺序。

接下来我们来看看认证流程图的解析。

这里我们只需要能看懂其过程即可,简单来说就是:

用户提交了用户名和密码,UsernamePasswordAuthenticationFilter将其封装未Authentication对象,并且调用authenticate方法进行认证,接着在调用DaoAuthenticationProvider的authenticate方法进行认证,再调用loadUserByUserName方法查询用户,这里的查询是在内存中进行查找,然后将对应的用户信息封装未UserDetails对象,通过PasswordEncoder对比UserDetails中的密码和Authentication的密码是否正确,如果正确就把UserDetails中的权限信息设置到Authentication对象中,接着返回Authentication对象,最后使用SecurityContextHolder.getContext().setAuthentication方法存储该对象,其他过滤器会通过SecurityContextHoder来获取当前用户信息。(这一段不用记忆能听懂即可)

那么我们知道了其过程,才能对其进行修改,首先这里的从内存中查找,我们肯定是要该为从数据库中查找(这里需要我们自定义一个UserDetailsService的实现类),并且也不会使用默认的用户名密码,登录界面也一定是自己编写的,不需要用他提供的默认登录页面。

基于我们分析的情况,可以得到这样的一张图。

这个时候就返回了一个jwt给前端,而这时前端进行的其他请求都会携带token,那么我们第一步就需要先校验是否携带token,并且解析token,获取对应的userid,并且将其封装为Anthentication对象存入SecurityContextHolder(为了其他过滤器可以拿到)。

那么这里还有一个问题,从jwt认证过滤器中取到了userid后如何获取完整的用户信息?

这里我们使用redis,当服务器认证通过使用用户id生成jwt给前端的时候,以用户id作为key,用户的信息作为value存入redis,之后就可以通过userid从redis中获取到完整的用户信息了。

2.3 解决问题

2.3.1 思路分析

从上述的原理初探中,我们也大概分析出了我们要是自己实现前后端分离的认证流程,需要做的事情。

登录:

        a.自定义登录接口

                调用ProviderManager的方法进行认证,如果认证通过生成jwt

                把用户信息存入redis中

        b.自定义UserDetailsService

                在这个实现类中去查询数据库

校验:

        a.自定义jwt认证过滤器

                获取token

                解析token获取其中userid

                从redis中获取完整用户信息

                存入SecurityContextHolder

2.3.2 准备工作

首先需要添加对应的依赖

        <!--   SpringSecurity启动器     --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--   redis依赖     --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--   fastjson依赖     --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><!--   jwt依赖     --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>

接着我们需要用到Redis需要加入Redis相关的配置

首先是FastJson的序列化器

package org.example.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
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 java.nio.charset.Charset;/*** Redis使用fastjson序列化* @param <T>*/
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);}}

创建RedisConfig在其中创建序列化器,解决乱码等问题

package org.example.config;
import org.example.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;@Configuration
public 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来序列化和反序列化redus的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);template.afterPropertiesSet();return template;}
}

 还需要统一响应类

package org.example.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
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 Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}public ResponseResult(Integer code,String msg,T data){this.code = code;this.msg = msg;this.data = data;}
}

再需要jwt的工具类用于生成jwt,以及对jwt进行解析

package org.example.utils;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;public class JwtUtil {//有效期为public static final Long JWT_TTL = 60*60*1000L; //一个小时//设置密钥明文public static final String JWT_KEY = "hzj";public static String getUUID(){String token = UUID.randomUUID().toString().replaceAll("-","");return token;}/*** 生成jwt* @param subject token中要存放的数据(json格式)* @return*/public static String createJWT(String subject){JwtBuilder builder = getJwtBuilder(subject,null,getUUID()); //设置过期时间return builder.compact();}/*** 生成jwt* @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("hzj") //签发者.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();}public static void main(String[] args) throws Exception{String token =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTg0MjU5MzIsInVzZX" +
"JJZCI6MTExLCJ1c2VybmFtZSI6Ik1hcmtaUVAifQ.PTlOdRG7ROVJqPrA0q2ac7rKFzNNFR3lTMyP_8fIw9Q";Claims claims = parseJWT(token);System.out.println(claims);}/*** 生成加密后的密钥secretkey* @return*/public static SecretKey generalkey(){byte[] encodeedkey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodeedkey,0,encodeedkey.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();}
}

再定义一个Redis的工具类RedisCache,这样可以使我们调用redistemplate更加简单

package org.example.utils;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" })
@Component
public class RedisCache
{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/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);}
}

我们还有可能往响应中写入数据,那么就还需要一个工具类WebUtils

package org.example.utils;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;}
}

最后写对应的用户实体类

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

 根据我们上方的分析我们是需要自定义一个UserDetailsService,让SpringSecuriry使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。

我们先建立一个数据库表sys_user。

CREATE TABLE `sys_user` (`id` bigint 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 '用户类型(O管理员,1普通用户)',`create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_by` bigint DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`del_flag` int DEFAULT '0' COMMENT '删除标志(O代表未删除,1代表已删除)',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

接着引入myBatisPlus和mysql驱动。

        <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>

 接着配置数据库的相关信息。

接着定义mapper接口UserMapper,使用mybatisplus加入对应的注解。

package org.example.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.domain.User;public interface UserMapper extends BaseMapper<User> {
}

接着配置组件扫描

最后测试一下mp能否正常使用。

引入junit

这样就是可以正常使用了。

2.3.3 实现

2.3.3.1 数据库校验用户

接下来我们需要进行核心代码的实现。

 首先我们先进行自定义UserDetailsService。

package org.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.example.domain.LoginUser;
import org.example.domain.User;
import org.example.mapper.UserMapper;
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.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//查询用户信息 [InMemoryUserDetailsManager是在内存中查找]LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUserName,username);User user = userMapper.selectOne(wrapper);//如果查询不到数据就抛出异常,给出提示if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误!");}//TODO 查询权限信息//封装为UserDetails对象返回return new LoginUser(user);}
}

这里要将user封装为UserDetails进行返回。

package org.example.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {private User user;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@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;}
}

最后这里有一个点,就是我们需要进行登录从数据库拿数据的测试,需要往表中写入用户数据,并且如果你想让用户的密码是明文传输,需要在密码前加上{noop}。

    这里就实现了输入数据库中的用户名密码进行登录了。

2.3.3.2 密码加密存储

这里说一下为什么要在密码前面加上{noop},因为默认使用的PasswordEncoder要求数据库中的密码格式为{id}password,它会根据id去判断密码的加密方式,但是我们一般不会采取这种方式,所以就需要替换掉PasswordEncoder。

接下来我们进行测试看看。

可以看到我们这里传入的两次密码原文是一样的,但是却得到了不同的结果,这里其实和加盐算法有关,之后我还会写一个自定义加密的文章。

得到加密之后的密码之后就可以将加密后的密码存入数据库,之后可以由前端传过来的明文密码与数据库中的加密后的密码进行验证进行登录。

这个时候我们启动项目去登录,发现之前的密码已经登不上了,因为数据库此时存放的应该是注册阶段存入数据库的加密后的密码,而不是原文密码了(因为没注册我将加密后的密码自行写入数据库中)。

2.3.3.3 登录接口

我们需要实现一个登录接口,然后让SpringSecuruty对其进行放行,如果不放行就自相矛盾了,在接口中通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

认证成功的话需要生成一个jwt,放入响应中,并且为了让用户下次请求时能通过jwt识别出具体是哪个用户,需要把用户信息存入redis,可以把用户id作为key。

先写LoginController

接着写对应的Service。

在SecurityConfig中进行AuthenticationManager的注入,和登录接口的放行。

在service中的业务逻辑中,如果认证失败则返回一个自定义异常,但是如果认证成功我们需要如何获取到对应的信息呢。

这里我们可以debug看看得到的对象。

这里发现是Principal中可以得到对应需要的信息。

接着补全代码。

最后进行测试看看。

2.3.3.4 认证过滤器

我先将代码贴上。

@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; //token为空 不执行后续流程}//解析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("用户未登录!");}//将信息存入SecurityContextHolder(因为过滤器链后面的filter都是从中获取认证信息进行对应放行)//TODO 获取权限信息封装到Authentication中UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request,response); //此时的放行是携带认证的,不同于上方token为空的放行}
}

首先这里获取token我们是从请求头中获取对应的token,然后对其进行判空,如果为空我们直接进行放行,且不走后续流程,接下来进行解析token,得到里面的userid,再根据userid从redis中获取对应的用户信息,最后将其存储到SecurityContextHolder中,因为后续的过滤器都需要从中获取日认证信息,最后进行分析操作。

 还有一个需要注意的点就是,SecurityContextHolder.getContext().setAuthentication()需要传入authentication对象,我们构建对象的时候采用的是三个参数的,因为第三个参数是判断是否认证的关键。

接下来我们需要将这个过滤器进行配置。

 接着我们进行访问user/login接口会返回给我们一个带token的响应体,再访问hello接口此时是403的,因为没有携带token,所以就对应上方的代码,没有token放行并且return不执行后续流程(这里的放行是因为后续有其他专门抛异常的过滤器进行处理,而return是为了不让其走响应的流程)

此时若我们将user/login生成的token放入hello接口的请求头那么就可以正常访问到了。

那么我们这套过滤器的目的也就达到了(获取token、解析token、存入SecurityContextHolder)

2.3.3.5 退出登录

到这里我们也就比较容易的实现退出登录了,我们只需要删除redis中对应的数据,之后携带token进行访问的时候,在我们自定义的过滤器中会获取redis中对应的用户信息,此时获取不到就意味着未登录。

我们携带这个token去访/user/logout接口。

那么退出登录功能就实现了。 

本文学习于b站up主三更!!!

相关文章:

SpringSecurity框架【认证】

目录 一. 快速入门 二. 认证 2.1 登陆校验流程 2.2 原理初探 2.3 解决问题 2.3.1 思路分析 2.3.2 准备工作 2.3.3 实现 2.3.3.1 数据库校验用户 2.3.3.2 密码加密存储 2.3.3.3 登录接口 2.3.3.4 认证过滤器 2.3.3.5 退出登录 Spring Security是Spring家族中的一个…...

python安全脚本开发简单思路

文章目录 为什么选择python作为安全脚本开发语言如何编写人生第一个安全脚本开发后续学习 为什么选择python作为安全脚本开发语言 易读性和易维护性&#xff1a;Python以其简洁的语法和清晰的代码结构著称&#xff0c;这使得它非常易于阅读和维护。在安全领域&#xff0c;代码…...

WPF学习(4) -- 数据模板

一、DataTemplate 在WPF&#xff08;Windows Presentation Foundation&#xff09;中&#xff0c;DataTemplate 用于定义数据的可视化呈现方式。它允许你自定义如何展示数据对象&#xff0c;从而实现更灵活和丰富的用户界面。DataTemplate 通常用于控件&#xff08;如ListBox、…...

GuLi商城-商品服务-API-品牌管理-JSR303分组校验

注解:@Validated 实体类: package com.nanjing.gulimall.product.entity;import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.nanjing.common.valid.ListValue; import com.nanjing.common.valid.Updat…...

PyTorch DataLoader 学习

1. DataLoader的核心概念 DataLoader是PyTorch中一个重要的类&#xff0c;用于将数据集&#xff08;dataset&#xff09;和数据加载器&#xff08;sampler&#xff09;结合起来&#xff0c;以实现批量数据加载和处理。它可以高效地处理数据加载、多线程加载、批处理和数据增强…...

TCP传输控制协议二

TCP 是 TCP/IP 模型中的传输层一个最核心的协议&#xff0c;不仅如此&#xff0c;在整个 4 层模型中&#xff0c;它都是核心的协议&#xff0c;要不然模型怎么会叫做 TCP/IP 模型呢。 它向下使用网络层的 IP 协议&#xff0c;向上为 FTP、SMTP、POP3、SSH、Telnet、HTTP 等应用…...

【学习笔记】无人机(UAV)在3GPP系统中的增强支持(五)-同时支持无人机和eMBB用户数据传输的用例

引言 本文是3GPP TR 22.829 V17.1.0技术报告&#xff0c;专注于无人机&#xff08;UAV&#xff09;在3GPP系统中的增强支持。文章提出了多个无人机应用场景&#xff0c;分析了相应的能力要求&#xff0c;并建议了新的服务级别要求和关键性能指标&#xff08;KPIs&#xff09;。…...

使用F1C200S从零制作掌机之debian文件系统完善NES

一、模拟器源码 源码&#xff1a;https://files.cnblogs.com/files/twzy/arm-NES-linux-master.zip 二、文件系统 文件系统&#xff1a;debian bullseye 使用builtroot2018构建的文件系统&#xff0c;使用InfoNES模拟器存在bug&#xff0c;搞不定&#xff0c;所以放弃&…...

Vue 3 与 TypeScript:最佳实践详解

大家好,我是CodeQi! 很多人问我为什么要用TypeScript? 因为 Vue3 喜欢它! 开个玩笑... 在我们开始探索 Vue 3 和 TypeScript 最佳实践之前,让我们先打个比方。 如果你曾经尝试过在没有 GPS 的情况下开车到一个陌生的地方,你可能会知道那种迷失方向的感觉。 而 Typ…...

PyMysql error : Packet Sequence Number Wrong - got 1 expected 0

文章目录 错误一错误原因解决方案 错误二原因解决方案 我自己知道的&#xff0c;这类问题有两类原因&#xff0c;两种解决方案。 错误一 错误原因 pymysql的主进程启动的connect无法给子进程中使用&#xff0c;所以读取大批量数据时最后容易出现了此类问题。 解决方案 换成…...

MVC 生成验证码

在mvc 出现之前 生成验证码思路 在一个html页面上&#xff0c;生成一个验证码&#xff0c;在把这个页面嵌入到需要验证码的页面中。 JS生成验证码 <script type"text/javascript">jQuery(function ($) {/**生成一个随机数**/function randomNum(min, max) {…...

OSPF.综合实验

1、首先将各个网段基于172.16.0.0 16 进行划分 1.1、划分为4个大区域 172.16.0.0 18 172.16.64.0 18 172.16.128.0 18 172.16.192.0 18 四个网段 划分R4 划分area2 划分area3 划分area1 2、进行IP配置 如图使用配置指令进行配置 ip address x.x.x.x /x 并且将缺省路由…...

云计算【第一阶段(29)】远程访问及控制

一、ssh远程管理 1.1、ssh (secureshell)协议 是一种安全通道协议对通信数据进行了加密处理&#xff0c;用于远程管理功能SSH 协议对通信双方的数据传输进行了加密处理&#xff0c;其中包括用户登录时输入的用户口令&#xff0c;建立在应用层和传输层基础上的安全协议。SSH客…...

2024前端面试真题【CSS篇】

盒子模型 盒子模型&#xff1a;box-sizing&#xff0c;描述了文档中的元素如何生成矩形盒子&#xff0c;并通过这些盒子的布局来组织和设计网页。包含content、padding、margin、border四个部分。 分类 W3C盒子模型&#xff08;content-box&#xff09;&#xff1a;标准盒子模…...

python中设置代码格式,函数编写指南,类的编程风格

4.6 设置代码格式 随着你编写的程序越来越长&#xff0c;确保代码格式一致变得尤为重要。花时间让代码尽可能易于阅读&#xff0c;这不仅有助于你理解程序的功能&#xff0c;也能帮助他人理解你的代码。 为了保证所有人的代码结构大致一致&#xff0c;Python程序员遵循一系列…...

CentOS 8升级gcc版本

1、查看gcc版本 gcc -v发现gcc版本为8.x.x&#xff0c;而跑某个项目的finetune需要gcc-9&#xff0c;之前搜索过很多更新gcc版本的方式&#xff0c;例如https://blog.csdn.net/xunye_dream/article/details/108918316?spm1001.2014.3001.5506&#xff0c;但执行指令 sudo yu…...

Kafka基础入门篇(深度好文)

Kafka简介 Kafka 是一个高吞吐量的分布式的基于发布/订阅模式的消息队列&#xff08;Message Queue&#xff09;&#xff0c;主要应用与大数据实时处理领域。 1. 以时间复杂度为O(1)的方式提供消息持久化能力。 2. 高吞吐率。&#xff08;Kafka 的吞吐量是MySQL 吞吐量的30…...

C++之复合资料型态KU网址第二部V蒐NAY3989

结构 结构可存放不同资料型态的数值&#xff0c;例如 #include <iostream>struct Demo {int member1;char *member2;float member3; };int main() {Demo d;d.member1 19823;d.member2 "203";d.member3 3.011;std::cout << "member1: " &l…...

乡镇集装箱生活污水处理设备处理效率高

乡镇集装箱生活污水处理设备处理效率高 乡镇集装箱生活污水处理设备优势 结构紧凑&#xff1a;集装箱式设计减少了占地面积&#xff0c;便于在土地资源紧张的乡镇地区部署。 安装方便&#xff1a;设备出厂前已完成组装和调试&#xff0c;现场只需进行简单的连接和调试即可投入使…...

计算机网络高频面试题

从输入URL到展现页面的全过程&#xff1a; 用户在浏览器中输入URL。浏览器解析URL&#xff0c;确定协议、主机名和路径。浏览器查找本地DNS缓存&#xff0c;如果没有找到&#xff0c;向DNS服务器发起查询请求。DNS服务器解析主机名&#xff0c;返回IP地址。浏览器使用IP地址建立…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录

ASP.NET Core 是一个跨平台的开源框架&#xff0c;用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录&#xff0c;以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

FFmpeg 低延迟同屏方案

引言 在实时互动需求激增的当下&#xff0c;无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作&#xff0c;还是游戏直播的画面实时传输&#xff0c;低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架&#xff0c;凭借其灵活的编解码、数据…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例

使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件&#xff0c;常用于在两个集合之间进行数据转移&#xff0c;如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model&#xff1a;绑定右侧列表的值&…...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码

目录 一、&#x1f468;‍&#x1f393;网站题目 二、✍️网站描述 三、&#x1f4da;网站介绍 四、&#x1f310;网站效果 五、&#x1fa93; 代码实现 &#x1f9f1;HTML 六、&#x1f947; 如何让学习不再盲目 七、&#x1f381;更多干货 一、&#x1f468;‍&#x1f…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖

在Vuzix M400 AR智能眼镜的助力下&#xff0c;卢森堡罗伯特舒曼医院&#xff08;the Robert Schuman Hospitals, HRS&#xff09;凭借在无菌制剂生产流程中引入增强现实技术&#xff08;AR&#xff09;创新项目&#xff0c;荣获了2024年6月7日由卢森堡医院药剂师协会&#xff0…...

【JavaSE】多线程基础学习笔记

多线程基础 -线程相关概念 程序&#xff08;Program&#xff09; 是为完成特定任务、用某种语言编写的一组指令的集合简单的说:就是我们写的代码 进程 进程是指运行中的程序&#xff0c;比如我们使用QQ&#xff0c;就启动了一个进程&#xff0c;操作系统就会为该进程分配内存…...

Qt 事件处理中 return 的深入解析

Qt 事件处理中 return 的深入解析 在 Qt 事件处理中&#xff0c;return 语句的使用是另一个关键概念&#xff0c;它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别&#xff1a;不同层级的事件处理 方…...

算法打卡第18天

从中序与后序遍历序列构造二叉树 (力扣106题) 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder [9,3,15,20,7…...

基于stm32F10x 系列微控制器的智能电子琴(附完整项目源码、详细接线及讲解视频)

注&#xff1a;文章末尾网盘链接中自取成品使用演示视频、项目源码、项目文档 所用硬件&#xff1a;STM32F103C8T6、无源蜂鸣器、44矩阵键盘、flash存储模块、OLED显示屏、RGB三色灯、面包板、杜邦线、usb转ttl串口 stm32f103c8t6 面包板 …...