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主三更!!!

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

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

相关文章

机器学习(V)--无监督学习(三)EM算法

EM算法 极大似然估计 极大似然估计&#xff1a;(maximum likelihood estimate, MLE) 是一种常用的模型参数估计方法。它假设观测样本出现的概率最大&#xff0c;也即样本联合概率&#xff08;也称似然函数&#xff09;取得最大值。 为求解方便&#xff0c;对样本联合概率取对…

华为HCIP Datacom H12-821 卷36

1.单选题 在PIM- SM中&#xff0c;以下关于RP 的描述&#xff0c;错误的是哪一选项? A、在PIM-SM中&#xff0c;组播数据流量不一定必须经过RP的转发。 B、对于一个组播组来说&#xff0c;可以同时有多个RP地址&#xff0c;提升网络可靠性。 C、组播网络中&#xff0c;可以…

【BUG】已解决:JsonMappingException

已解决&#xff1a;JsonMappingException 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 概述&#xff1a; 没有getter方法的实体的序列化&#xff0c;并解决Jackson引发的JsonMappingException异常。 默认情况下&#xff0c;Jackson 2只会处理公有字段或具有公有get…

Renesas R7FA8D1BH (Cortex®-M85) 控制DS18B20

目录 概述 1 软硬件 1.1 软硬件环境信息 1.2 开发板信息 1.3 调试器信息 2 FSP和KEIL配置 2.1 硬件接口电路 2.2 FSB配置DS18B20的IO 2.3 生成Keil工程文件 3 DS18B20驱动代码 3.1 DS18B20介绍 3.2 DS18B20驱动实现 3.2.1 IO状态定义 3.2.2 读IO状态函数 3.2.3…

OpenCV:python图像旋转,cv2.getRotationMatrix2D 和 cv2.warpAffine 函数

前言 仅供个人学习用&#xff0c;如果对各位朋友有参考价值&#xff0c;给个赞或者收藏吧 ^_^ 一. cv2.getRotationMatrix2D(center, angle, scale) 1.1 参数说明 parameters center&#xff1a;旋转中心坐标&#xff0c;是一个元组参数(col, row) angle&#xff1a;旋转角度…

Go-知识测试-模糊测试

Go-知识测试-模糊测试 1. 定义2. 例子3. 数据结构4. tesing.F.Add5. 模糊测试的执行6. testing.InternalFuzzTarget7. testing.runFuzzing8. testing.fRunner9. FuzzXyz10. RunFuzzWorker11. CoordinateFuzzing12. 总结 建议先看&#xff1a;https://blog.csdn.net/a1879272183…

一文入门【NestJs】Providers

Nest学习系列 ✈️一文入门【NestJS】 ✈️一文入门【NestJs】Controllers 控制器 &#x1f6a9; 前言 在NestJS的世界里&#xff0c;理解“Providers”是构建健壮、可维护的后端服务的关键。NestJS&#xff0c;作为Node.js的一个现代框架&#xff0c;采用了Angular的一些核…

Redis的安装配置及IDEA中使用

目录 一、安装redis&#xff0c;配置redis.conf 1.安装gcc 2.将redis的压缩包放到指定位置解压 [如下面放在 /opt 目录下] 3.编译安装 4.配置redis.conf文件 5.开机自启 二、解决虚拟机本地可以连接redis但是主机不能连接redis 1.虚拟机网络适配器网络连接设置为桥接模式…

VSCode上通过C++实现单例模式

单例模式实际上就是为了确保一个类最多只有一个实例&#xff0c;并且在程序的任何地方都可以访问这个实例&#xff0c;也就是提供一个全局访问点&#xff0c;单例对象不需要手动释放&#xff0c;交给系统来释放就可以了&#xff0c;单例模式的设计初衷就是为了在整个应用程序的…

vue 下拉菜单树形结构——vue-treeselect的组件使用

参考&#xff1a; https://www.cnblogs.com/syjtiramisu/p/17672866.htmlhttps://www.cnblogs.com/syjtiramisu/p/17672866.html vue-treeselect的使用 - 简书下载依赖 使用https://www.jianshu.com/p/459550e1477d 实际项目使用&#xff1a;

uni-app iOS上架相关App store App store connect 云打包有次数限制

相册权限 uni-app云打包免费有次数 切换一个账号继续

华为手机联系人不见了怎么恢复?3个解决方案

华为手机联系人列表就像是我们精心编织的社交网络之网。然而&#xff0c;有时&#xff0c;这张网可能会因为各种原因而意外破损&#xff0c;联系人信息消失得无影无踪&#xff0c;让我们陷入“人脉孤岛”的困境。华为手机联系人不见了怎么恢复&#xff1f;别担心&#xff0c;我…

构建高质量数据集与智能数据工程平台:播客AI Odyssey深度对话实录

对话整数智能联创和前IDEA研究员&#xff1a;构建高质量数据集与智能数据工程平台 - AI Odyssey | 小宇宙 - 听播客&#xff0c;上小宇宙 人工智能技术的日益深远发展&#xff0c;对人工智能的性能提升与技术迭代提出了新的要求。在大模型训练中&#xff0c;已有的研究和实践表…

【操作系统】进程管理——用信号量机制解决问题,以生产者-消费者问题为例(个人笔记)

学习日期&#xff1a;2024.7.10 内容摘要&#xff1a;利用信号量机制解决几个经典问题模型 目录 引言 问题模型 生产者-消费者问题&#xff08;经典&#xff09; 多生产者-多消费者问题 吸烟者问题 读者写者问题&#xff08;难点&#xff09; 哲学家进餐问题&#xff0…

解决POST请求中文乱码问题

解决POST请求中文乱码问题 1、乱码原因2、解决方法3、具体步骤 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在Web开发中&#xff0c;处理POST请求时经常遇到中文乱码问题&#xff0c;这主要是由于服务器在接收到POST请求的数据后&#x…

BUG解决:postman可以请求成功,但Python requests请求报403

目录 问题背景 问题定位 问题解决 问题背景 使用Python的requests库对接物联数据的接口之前一直正常运行&#xff0c;昨天突然请求不通了&#xff0c;通过进一步验证发现凡是使用代码调用接口就不通&#xff0c;而使用postman就能调通&#xff0c;请求参数啥的都没变。 接口…

SSL 证书错误:如何修复以及错误发生的原因

SSL证书可以提升网站的可信度。然而&#xff0c;如果您的SSL证书出现错误&#xff0c;您可能会得到一个“不安全”的标签&#xff0c;这可能会导致访问者失去对您网站的信任并转向竞争对手。 本文将介绍SSL证书错误的原因及其对用户的潜在影响。随后&#xff0c;我们将提供详细…

MybatisPlus 核心功能

MybatisPlus 核心功能 文章目录 MybatisPlus 核心功能1. 条件构造器1.1 QueryWrapper1.2 LambdaQueryWrapper&#xff08;推荐&#xff09;1.3 UpdateWrapper1.4 LambdaUpdateWrapper 2. 自定义SQL3. Service接口 1. 条件构造器 当涉及到查询或修改语句时&#xff0c;MybatisP…

界面组件Kendo UI for React 2024 Q2亮点 - 生成式AI集成、设计系统增强

随着最新的2024年第二季度发布&#xff0c;Kendo UI for React为应用程序开发设定了标准&#xff0c;包括生成式AI集成、增强的设计系统功能和可访问的数据可视化。新的2024年第二季度版本为应用程序界面提供了人工智能(AI)提示&#xff0c;从设计到代码的生产力增强、可访问性…

Java毕业设计 基于SSM vue图书管理系统小程序 微信小程序

Java毕业设计 基于SSM vue图书管理系统小程序 微信小程序 SSM 图书管理系统小程序 功能介绍 用户 登录 注册 首页 图片轮播 图书信息推荐 图书详情 赞 踩 评论 收藏 系统公告 公告详情 用户信息修改 我的待还 图书归还 催还提醒 我的收藏管理 意见反馈 管理员 登录 个人中心…