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

发布时间:2023年12月25日

一、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.测试

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

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

文章来源:https://blog.csdn.net/shaogaiyue9745602/article/details/135125654
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。