SpringBoot整合JWT+Spring Security+Redis实现登录拦截(一)登录认证

一、JWT简介

        JWT 全称 JSON Web Token,JWT 主要用于用户登录鉴权,当用户登录之后,返回给前端一个Token,之后用户利用Token进行信息交互。

       除了JWT认证之外,比较传统的还有Session认证,如何选择可以查看之前的博文:基于token与Session身份认证对比-CSDN博客简单来说认证就是让服务器知道你是谁?就是让服务器知道你能干什么,不能干什么?那么基于身份认证我们一般有俩种方式:Session-Cookie和JWT。https://blog.csdn.net/shaogaiyue9745602/article/details/135130114?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22135130114%22%2C%22source%22%3A%22shaogaiyue9745602%22%7D

二、Spring Security简介

        Spring Security 是基于 Spring 的身份认证(Authentication)和用户授权(Authorization)框架,提供了一套 Web 应用安全性的完整解决方案。其中核心技术使用了 Servlet 过滤器、IOC 和 AOP 等。实际操作时经常需要实现XXXFilter来自定义的登录以及访问控制。

  • 什么是身份认证

        身份认证指的是用户去访问系统资源时,系统要求验证用户的身份信息,用户身份合法才访问对应资源。常见的身份认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

  • 什么是用户授权

        当身份认证通过后,去访问系统的资源,系统会判断用户是否拥有访问该资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。比如 会员管理模块有增删改查功能,有的用户只能进行查询,而有的用户可以进行修改、删除。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

三、登录拦截流程

     其中认证的过程还应包含 鉴权 以及 token过期 验证。 

四、代码实现

     代码实现前需要有springboot项目有基础的用户user表,并可以实现数据的查询功能。最好有自己的统一返回配置和全局异常处理配置,若没有可参考前面的博客参考进行配置或自行配置。

  1.在pom中添加相关依赖

        <!--安全框架引入, 进行权限控制-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.50</version>
        </dependency>

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>

   引入依赖重新启动后,访问项目发现会出现登录页面如下:

        用户名默认为:user 

        默认密码在项目启动时会打印在控制台如下:

        也可通过在配置文件 application.properties 自定义用户名和密码

spring.security.user.name=admin
spring.security.user.password=admin

2.创建SecurityUser实现UserDetails

package com.hng.config.jwtSecurity;

import com.alibaba.fastjson.annotation.JSONField;
import com.hng.entity.SysUser;
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.Collection;
import java.util.List;

/**
 * @Author: 郝南过
 * @Description: 
 * @Date: 2023/11/29 10:26
 * @Version: 1.0
 */
@Data
@NoArgsConstructor
public class SecurityUser implements UserDetails {

    private SysUser user;

    private List<String> permissions;

    @JSONField(serialize = false) // 防止存入redis时序列化出错,不进行序列化,也不存入redis中
    private List<SimpleGrantedAuthority> authorities;

    public SecurityUser(SysUser user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

3.创建UserDetailsServiceImpl实现UserDetailsService

      该类主要功能是将Security拦截的用户名密码改为查询数据库中的用户名密码,以及权限的相关认证(权限认证相关本篇暂时不做添加

package com.hng.config.jwtSecurity;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hng.entity.SysUser;
import com.hng.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
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 javax.annotation.Resource;
import java.util.Objects;

/**
 * @Author: 郝南过
 * @Description: 将Security拦截的用户名密码改为数据库中已有的用户名密码,Security自己校验密码,默认使用PasswordEncoder,格式为{id}password(id代表加密方式),
 * 一般不采用此方式,SpringSecurity提供了BcryptPasswordEncoder,只需将此注入到spring容器中。SpringSecurity就会使用它进行替换校验
 * @Date: 2023/12/11 11:29
 * @Version: 1.0
 */
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper();
        queryWrapper.eq(SysUser::getUserName,username);
        SysUser user = sysUserService.getOne(queryWrapper);
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        //TODO 查询授权信息
        return new SecurityUser(user);
    }
}

4.创建JwtTokenUtil工具类

      该工具类用于token的创建验证等

package com.hng.config.jwtSecurity;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description JwtToken生成的工具类
 * @Version 1.0
 **/
@Slf4j
@Component
public class JwtTokenUtil {
 
    private static final String CLAIM_KEY_USERNAME = "username";
    private static final String CLAIM_KEY_CREATED = "created";
    private static final String CLAIM_KEY_USER_ID = "userId";
 
    // 令牌自定义标识
    @Value("${jwt.token.header}")
    private String header;
 
    // 令牌秘钥
    @Value("${jwt.token.secret}")
    private String secret;
 
    // 令牌有效期(默认30分钟),也可将token的过期时间交给redis管理
    @Value("${jwt.token.expireTime}")
    private Long expiration;
 
 
    /**
     * 根据负责生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
 
    /**
     * 从token中获取JWT中的负载
     */
    public Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            log.info("JWT格式验证失败:{}",token);
        }
        return claims;
    }
 
    /**
     * 生成token的过期时间,返回Date
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token中获取登录用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username =  claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }
 
    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }
 
    /**
     * 判断token是否已经失效
     */
    public boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }
 
    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }
 
    /**
     * 根据用户信息生成token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
 
    /**
     * 根据用户信息生成token
     */
    public String generateToken(SecurityUser securityUser) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, securityUser.getUsername());
        claims.put(CLAIM_KEY_USER_ID, securityUser.getUser().getId());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
 
    /**
     * 判断token是否可以被刷新
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }
 
    /**
     * 刷新token
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
 
    /**
     * 获取请求token
     *
     * @param request
     * @return token
     */
    public String getToken(HttpServletRequest request)
    {
        return request.getHeader(header);
    }
}

5.创建RedisUtil工具类

       springboot如何集成redis可查看之前的博文

Springboot 集成Redis-CSDN博客文章浏览阅读621次,点赞9次,收藏9次。注意commons-pool2包与spring的版本一致性,若出错尝试升级或降级commons-pool2版本。https://blog.csdn.net/shaogaiyue9745602/article/details/134669420?spm=1001.2014.3001.5501

      记得在配置文件application.properties中添加redis配置信息

spring.redis.host=127.0.0.1
spring.redis.host.port=6379
#spring.redis.host.name=
#spring.redis.host.password=
  RedisUtil.java
package com.hng.config.redis;

import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
@Order(-1)
public final class RedisUtil {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return 0
     */

    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {

        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    // ============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {

        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {

        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    // ================================Map=================================

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return 值
     */

    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }


    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */

    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */

    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * 0
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {

        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {

        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     *
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {

        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     *
     * @param key 键
     * @return 0
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头, 第二个元素,依次类推;index<0时,-,表尾,-倒数第二个元素,依次类推
     * @return 0
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return 0
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return 0
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

6. 创建JwtAuthenticationTokenFilter实现OncePerRequestFilter

        该类为登录授权过滤器

package com.hng.config.jwtSecurity;

import com.hng.config.redis.RedisUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Qualifier;
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 org.springframework.web.servlet.HandlerExceptionResolver;

import javax.annotation.Resource;
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;

/**
 * @Author: 郝南过
 * @Description: JWT 登录授权过滤器
 * @Date: 2023/11/28 16:19
 * @Version: 1.0
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisUtil redisUtil;

    // JWT 工具类
    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    /**
     * 从请求中获取 JWT 令牌,并根据令牌获取用户信息,最后将用户信息封装到 Authentication 中,
     * 方便后续校验(只会执行一次)
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //token为空的话, 就不管它, 让SpringSecurity中的其他过滤器处理请求,请求放行
            filterChain.doFilter(request, response);
            return;
        }
        //token不为空时, 解析token
        String userId = null;
        try {
            Claims claims = jwtTokenUtil.getClaimsFromToken(token);
            //解析出userId
            userId = claims.get("userId", String.class);
        } catch (Exception e) {
            e.printStackTrace();
            // 交给全局异常处理类处理
            throw new RuntimeException("token非法");
//            resolver.resolveException(request, response, null,new RuntimeException("token非法"));
        }
        //使用userId从Redis缓存中获取用户信息
        String redisKey = "login:" + userId;
        SecurityUser securityUser = (SecurityUser)redisUtil.get(redisKey);
        if (Objects.isNull(securityUser)) {
            throw new RuntimeException("用户未登录");
            // 交给全局异常处理类处理
//            resolver.resolveException(request, response, null,new RuntimeException("用户未登录"));
        }
        //将用户安全信息存入SecurityContextHolder, 在之后SpringSecurity的过滤器就不会拦截
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

7.创建SecurityConfig继承WebSecurityConfigurerAdapter

        该类主要为Security配置类

package com.hng.config.jwtSecurity;

import lombok.extern.slf4j.Slf4j;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

/**
 * @Author: 郝南过
 * @Description: TODO
 * @Date: 2023/11/28 16:24
 * @Version: 1.0
 */
@Configuration //注册为SpringBoot的配置类
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入Jwt认证拦截器.
    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 将BCryptPasswordEncoder加密器注入SpringSecurity中,
     * SpringSecurity的DaoAuthenticaionProvider会调用该加密器中的match()方法进行密码比对, 密码比对过程不需要我们干涉
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bcryptPasswordBean(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 注入身份验证管理器, 直接继承即可.
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //禁用跨站请求伪造
                .csrf().disable()
                //禁用Session,使用token作为信息传递介质
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //把token校验过滤器添加到过滤器链中, 添加在UsernamePasswordAuthenticationFilter之前是因为只要用户携带token,
                //就不需要再去验证是否有用户名密码了 (而且我们不使用表单登入, UsernamePasswordAuthenticationFilter是无法解析Json的, 相当于它没用了)
                //UsernamePasswordAuthenticationFilter是SpringSecurity默认配置的表单登录拦截器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 认证请求的配置
                .authorizeRequests()
                // 将登入和注册的接口放开
                .antMatchers("/sys/login").anonymous()
                .antMatchers("/sys/register").anonymous()
                //除了上面的那些, 剩下的任何接口请求都需要经过认证
                .anyRequest().authenticated()
                .and()
                //允许跨域请求
                .cors();
    }

}

8.在配置文件application.properties中添加相关的JWT配置信息

# 令牌自定义标识
jwt.token.header=token
# 令牌秘钥
jwt.token.secret=ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=
# 令牌有效期(默认30分钟)
jwt.token.expireTime=1800

9.创建LoginController 

package com.hng.controller;

import com.hng.config.response.ResponseResult;
import com.hng.entity.SysUser;
import com.hng.service.SysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;


/**
* <p>
    * 系统用户 前端控制器
    * </p>
*/
@Slf4j
@Api(value = "SysUser", tags = "SysUser")
@RestController
@RequiredArgsConstructor
@RequestMapping("/sys-user")
public class SysUserController {

private final SysUserService sysUserService;

    @PostMapping("/getList")
    @ApiOperation("sysUser列表查询")
    public ResponseResult queryAllSysUser(@Validated @RequestBody SysUser sysUser){
        List<SysUser> list = sysUserService.queryAll(sysUser);
        return ResponseResult.success(list);
    }

    @PostMapping("/add")
    @ApiOperation("新增SysUser")
    public ResponseResult addSysUser(@Validated @RequestBody SysUser sysUser){
       sysUserService.addSysUser(sysUser);
       return ResponseResult.success();
    }

    /**
    * 根据ID查询数据
    * @param id ID
    */
    @GetMapping("getById/{id}")
    @ApiOperation("sysUser根据Id查询")
    @ResponseBody
    public ResponseResult getById(@PathVariable Integer id) {
       return ResponseResult.success(sysUserService.getSysUserById(id));
    }

    /**
    * 更新数据
    * @param sysUser 实体对象
    */
     @PutMapping("update")
     @ApiOperation("sysUser更新")
     @ResponseBody
     public ResponseResult update(@Validated @RequestBody SysUser sysUser) {
         sysUserService.updateSysUser(sysUser);
         return ResponseResult.success();
     }

      /**
      * 删除数据
      * @param id ID
      */
      @DeleteMapping("delete/{id}")
      @ApiOperation("sysUser根据Id删除")
      @ResponseBody
      public ResponseResult delete(@PathVariable Integer id) {
           sysUserService.deleteSysUser(id);
           return ResponseResult.success();
      }

}

五、测试

        使用postman进行测试

      1.login登录获取token

2.getList获取数据

        将login中获取的token复制到headers中,请求测试

到此登录拦截已经可以正常使用了

六 、配置登录异常处理器

  1.创建JwtAuthenticationEntryPoint实现AuthenticationEntryPoint

        该类主要用来处理认证失败异常

package com.hng.config.security;

import com.alibaba.fastjson.JSONObject;
import com.hng.config.response.ResponseResult;
import lombok.extern.slf4j.Slf4j;
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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: 郝南过
 * @Description: 认证失败处理器
 * @Date: 2023/12/19 17:17
 * @Version: 1.0
 */
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().write(JSONObject.toJSONString(ResponseResult.fail(HttpStatus.UNAUTHORIZED.value(),"认证失败")));
        response.getWriter().flush();
    }
}

2.创建JwtAccessDeniedHandler实现AccessDeniedHandler

        该类主要用于处理鉴权失败异常

package com.hng.config.security;

import com.alibaba.fastjson.JSONObject;
import com.hng.config.response.ResponseResult;
import lombok.extern.slf4j.Slf4j;
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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: 郝南过
 * @Description: 鉴权失败处理
 * @Date: 2023/12/19 17:26
 * @Version: 1.0
 */
@Component
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().write(JSONObject.toJSONString(ResponseResult.fail(HttpStatus.FORBIDDEN.value(),"鉴权失败")));
        response.getWriter().flush();
    }
}

3.全局异常处理器GlobalExceptionHandler中配置以上俩个异常

    /**
     * 全局捕获security的权限不足异常
     */
    @ExceptionHandler(value = AccessDeniedException.class)
    public void accessDeniedException(AccessDeniedException e) {
        log.error("权限不足异常!原因是:[{}]", e.getMessage());
        throw e;
    }

    /**
     * 全局捕获security的认证失败异常
     */
    @ExceptionHandler(value = AuthenticationException.class)
    public void authenticationException(AuthenticationException e) {
        log.error("用户认证失败异常!原因是:[{}]", e.getMessage());
        throw e;
    }

4.在SecurityConfig中配置以上俩个异常

    @Resource
    private JwtAuthenticationEntryPoint authenticationEntryPoint;

    @Resource
    private JwtAccessDeniedHandler accessDeniedHandler;
            //配置异常处理器
            .exceptionHandling()
            //认证失败处理器
            .authenticationEntryPoint(authenticationEntryPoint)
            //鉴权失败处理器
            .accessDeniedHandler(accessDeniedHandler)

5.测试

        认证失败异常可使用错误的用户名密码登录进行测试

        鉴权失败异常,暂时不能测试,需要补全权限代码后才可以进行测试

【demo示例代码】

https://download.csdn.net/download/shaogaiyue9745602/88661442icon-default.png?t=N7T8https://download.csdn.net/download/shaogaiyue9745602/88661442

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MAGVIT: Masked Generative Video Transformer

Paper name MAGVIT: Masked Generative Video Transformer Paper Reading Note Paper URL: https://arxiv.org/abs/2212.05199 Project URL: https://magvit.cs.cmu.edu/ Code URL: https://github.com/google-research/magvit TL;DR 2023 年 CMU、google 等发表 CVPR20…

[Python工程化之路] 搭建Python开发环境 包管理环境以及Linter

个人博客:Sekyoro的博客小屋 个人网站:Proanimer的个人网站 在工程化上,Python相比于Java,C#这类语言还是差了不少,不过整个生态还是不错的. 项目结构 一般有两种,一种称为flat另一种为src. ├── sample │ ├── AUTHORS.rst │ ├── docs | | ├── conf.py │ │ └…

深入Apache Commons Config:管理和使用配置文件

第1章&#xff1a;引言 咱们都知道&#xff0c;在软件开发中&#xff0c;管理配置文件是一件既重要又让人头疼的事。想象一下&#xff0c;咱们的应用程序有一堆设置需要调整&#xff0c;比如数据库的连接信息、应用的端口号&#xff0c;或者是一些功能的开关。如果这些信息硬编…

java实现广度优先搜索算法

广度优先搜索算法&#xff08;BFS&#xff09;是一种用于图遍历的算法。它从图的某个节点开始&#xff0c;依次访问其所有邻接节点&#xff0c;再依次访问邻接节点的邻接节点&#xff0c;以此类推&#xff0c;直到遍历完所有节点。 BFS使用队列数据结构来实现遍历过程。具体步…

关于 Appium 各种版本的安装,都在这里

大家在初次接触 Appium 时会看到网上各种帖子讲解如何安装 Appium&#xff0c;各种 Appium 版本的安装教程满天飞&#xff0c;而很多帖子中提供的安装教程是已经过时了的&#xff0c;容易误导初学者。 这篇文章带着你一起全面了解 Appium 各种版本如何选择如何安装。 一句话概述…

Superset 二次开发之自定义Viz Plugins(Hello World v2)

环境&#xff1a; Node.js 16npm 7 or 8安装webpack 全局安装 npm install webpack -g 安装eslint superset-frontend> npm install eslint 1.Yeoman 生成器 全局安装Yo> npm i -g yo 2.进入/superset-frontend/packages/generator-superset目录 npm i && npm…

传感器原理与应用--传感器基本特性与应变式传感器

文章目录 上一篇传感器的基本特性应变式传感器应变式传感器的应用下一篇 上一篇 传感器的基本特性 一般来说能把特定被测量信息按一定规律转换成某种可用信号的器件或装置&#xff0c;称为传感器 静态特性 灵敏度 定义&#xff1a;输出量增量 Δ y \Delta y Δy与引起输出量…

xstream 远程代码执行 CVE-2021-29505 已亲自复现

xstream 远程代码执行 CVE-2021-29505 已亲自复现 漏洞名称漏洞描述影响版本 漏洞复现环境搭建漏洞利用 修复建议总结 漏洞名称 漏洞描述 XStream 是用于将 Java 对象序列化为 XML 并再次序列化的软件。 1.4.17 之前的 XStream 版本中存在一个漏洞&#xff0c;可能允许远程攻…

集成钉钉机器人消息推送

一、简介 背景 客户需要通过钉钉接收消息通知 名词解释 群聊机器人&#xff1a;钉钉群里可以创建一个机器人&#xff0c;平台通过机器人把告警/通知推送到群里私聊机器人&#xff1a;钉钉后台开启机器人配置&#xff0c;平台绑定此机器人后&#xff0c;可以通过私聊的方式将…

C/S医院检验LIS系统源码

一、检验科LIS系统概述&#xff1a; LIS系统即实验室信息管理系统。LIS系统能实现临床检验信息化&#xff0c;检验科信息管理自动化。其主要功能是将检验科的实验仪器传出的检验数据经数据分析后&#xff0c;自动生成打印报告&#xff0c;通过网络存储在数据库中&#xff…

20231226在Firefly的AIO-3399J开发板上在Android11下调通后摄像头ov13850

20231226在Firefly的AIO-3399J开发板上在Android11下调通后摄像头ov13850 2023/12/26 8:22 开发板&#xff1a;Firefly的AIO-3399J【RK3399】 SDK&#xff1a;rk3399-android-11-r20211216.tar.xz【Android11】 Android11.0.tar.bz2.aa【ToyBrick】 Android11.0.tar.bz2.ab And…

一文搞懂类加载过程

废话不多说&#xff0c;先上一张图 1、“加载”过程做了什么&#xff1f;什么是双亲委派&#xff1f;为什么要使用双亲委派机制&#xff1f;有什么利弊&#xff1f; **加载&#xff1a;**就是将编译后的.class字节码文件【jvm只认.class文件&#xff0c;.class文件也并非只有…

C++ std::string使用效率优化

字符串操作是任何一个C开发程序无法绕过的点&#xff0c;很多时候针对字符串的操作需要进行优化&#xff0c;从而达到更优的使用效率和内存利用率。一般会采用标准的std::string替代C字符串&#xff0c;一方面是std::string为一个成熟的类对象&#xff0c;其成员操作基本能满足…

阿赵UE学习笔记——4、新建关卡

阿赵UE学习笔记目录 大家好&#xff0c;我是阿赵。   之前介绍了虚幻引擎的常用窗口功能&#xff0c;这次开始创建游戏内的世界了。首先先从创建关卡开始。 一、创建新关卡 在使用UE引擎制作游戏&#xff0c;首先要有一个场景作为基础&#xff0c;这个场景在UE里面成为关卡。…

若依(Spring boot)框架中如何在不同的控制器之间共享与使用数据

在若依框架或Spring boot框架中&#xff0c;控制器共享和使用数据是为了确保数据一致性、传递信息、提高效率和降低系统复杂性。这可以通过全局变量、依赖注入或数据库/缓存等方式实现。共享和使用数据对框架的正常运行非常关键&#xff0c;有助于促进控制器之间的协同工作&…

消息走漏提前做空腾讯爆赚30倍?逐帧分析还原真相

数量技术宅团队在CSDN学院推出了量化投资系列课程 欢迎有兴趣系统学习量化投资的同学&#xff0c;点击下方链接报名&#xff1a; 量化投资速成营&#xff08;入门课程&#xff09; Python股票量化投资 Python期货量化投资 Python数字货币量化投资 C语言CTP期货交易系统开…

C语言结构体内存对齐

文章目录 一、结构体内存对齐问题二、查看结构体成员起始位置三、设置内存对齐方式 一、结构体内存对齐问题 如下的info_s结构体类型&#xff0c;包含一个int型成员age, 一个char型成员gender, 一个int型成员id。 单从数据成员的大小进行分析&#xff0c;整个结构体的大小应为…

【JavaWeb学习笔记】18 - 文件上传下载

项目代码 https://github.com/yinhai1114/JavaWeb_LearningCode/tree/main/fileupdown 目录 文件上传 一、基本介绍 二、文件上传的基本原理 ​编辑 三、文件上传应用实例 四、文件上传的注意细节 1.解决中文乱码问题 2.分割文件夹 3.防止重名 4.百度WebUploader 5.空…

Windows无法安装edge 无法连接Internet

如果出现以上问题&#xff0c;或者Edge浏览器无法更新&#xff0c;提示防火墙错误之类的都可以解决问题。 下载以下证书文件并导入即可解决问题。 MicrosoftRootCertificateAuthority2011.cer

注意力机制(数学公式)

人类视觉注意力机制极大地提高了视觉信息处理的效率与准确性 计算机注意力机制是为了让卷积神经网络注意到他更加需要注意的地方 &#xff0c;而不是什么都关注 。 分为三种注意力机制&#xff0c;空间注意力机制&#xff0c;通道注意力机制&#xff0c;以及两者的结合。 …
最新文章