likes
comments
collection
share

SpringSecurity+jwt+redis基于数据库登录认证

作者站长头像
站长
· 阅读数 13

一、前期准备

1. 创建项目

SpringSecurity+jwt+redis基于数据库登录认证

勾选需要用到的框架

SpringSecurity+jwt+redis基于数据库登录认证

2. 引入相关依赖

提示:有三个依赖需要手动添加,其余的在创建项目时就生成了

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.1</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>2.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
		<!--以上是创建项目时勾选直接生成的,下面是手动添加的-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.1-jre</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>

3. 创建数据库并生成数据

数据库名为javasec,可自行更改

role

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `rid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `nameZh` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'hGwQWakALy', 'admin', '管理员');
INSERT INTO `role` VALUES ('2', 'afdasfsadf', 'user', '普通用户');

SET FOREIGN_KEY_CHECKS = 1;

user

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `uid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `time` datetime NULL DEFAULT NULL,
  `locked` tinyint NULL DEFAULT NULL,
  `enabled` tinyint NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'xbvlhKeYXv', 'root', '$2a$10$SuYo3aVhuZDBDGaEJSlNGedkFcRqPB6WPlXpntpt8bklp067VtVs.', '2021-08-31 14:41:01', 0, 1);
INSERT INTO `user` VALUES ('2', 'asdfsd', 'user', '$2a$10$SuYo3aVhuZDBDGaEJSlNGedkFcRqPB6WPlXpntpt8bklp067VtVs.', '2023-08-22 21:00:22', 0, 1);

SET FOREIGN_KEY_CHECKS = 1;

user_role

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `uid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id',
  `rid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', 'xbvlhKeYXv', 'hGwQWakALy');
INSERT INTO `user_role` VALUES ('2', 'asdfsd', 'afdasfsadf');

SET FOREIGN_KEY_CHECKS = 1;

4. 整体项目结构

可以按照我的来,也可自行决定

SpringSecurity+jwt+redis基于数据库登录认证

二、具体实现

1. 编写配置文件

提示:修改数据库的密码,以及redis的端口号,我使用的7000,redis的默认端口号为6379

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/javasec?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&allowMultiQueries=true
    password: ****
    username: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  ## redis配置
  redis:
    database: 0 # 数据库索引 默认为0
    host: 127.0.0.1    # redis服务器地址
    port: 7000    # 端口号
    password:    # 密码(默认为空)
    timeout: 5000 # 连接超时时间(毫秒)
    jedis:
      pool: # 连接池配置
        max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
        max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-idle: 8 # 连接池中的最大空闲连接
        min-idle: 0 # 连接池中的最小空闲连接
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.javasec.bean

2. 创建实体类

package com.javasec.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @Author YZK
 * @Date 2023/7/5
 */
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    /**
     * 数据库主键
     */
    private String id;
    /**
     * 角色uid
     */
    private String rid;
    /**
     * 角色名称
     */
    private String name;
    /**
     * 角色名称中文
     */
    private String nameZh;
}

创建的User需要实现UserDetails接口

package com.javasec.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

/**
 * @Author YZK
 * @Date 2023/7/1
 */
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {
    /**
     * 数据库主键
     */
    private String id;
    /**
     * 用户uid
     */
    private String uid;
    /**
     * 用户登录名
     */
    private String username;
    /**
     * 用户登录密码
     */
    private String password;
    /**
     * 用户创建时间
     */
    private Date time;
    /**
     * 用户是否被锁
     */
    private boolean locked;
    /**
     * 用户是否开启
     */
    private boolean enabled;
    /**
     * 账户登录token
     */
    private String token;
    /**
     * 用户角色列表
     */
    List<Role> roles;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

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

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

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

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

3. 编写dao层代码以及mapper

UserDao中主要是通过username来查询数据库中是否存在这个用户

RoleDao主要是用来查询登录用户的==角色列表(一个用户可能有多个角色)==

package com.javasec.dao;

import com.javasec.bean.User;
import org.apache.ibatis.annotations.Param;

/**
 * @Author YZK
 * @Date 2023/7/5
 */
public interface UserDao {
    User loadUserByUsername(@Param("username") String username);
}
package com.javasec.dao;

import com.javasec.bean.Role;
import org.apache.ibatis.annotations.Param;
import java.util.List;

/**
 * @Author YZK
 * @Date 2023/7/5
 */
public interface RoleDao {
    List<Role> getUserRoleByUid(@Param("uid") String uid);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javasec.dao.UserDao">

    <resultMap type="com.javasec.bean.User" id="UserMap">
        <result property="id" column="id" jdbcType="VARCHAR"/>
        <result property="uid" column="uid" jdbcType="VARCHAR"/>
        <result property="username" column="username" jdbcType="VARCHAR"/>
        <result property="password" column="password" jdbcType="VARCHAR"/>
        <result property="time" column="time" jdbcType="TIMESTAMP"/>
        <result property="locked" column="locked" jdbcType="INTEGER"/>
        <result property="enabled" column="enabled" jdbcType="INTEGER"/>
    </resultMap>

    <select id="loadUserByUsername" resultMap="UserMap">
        select *
        from user
        where username = #{username}
    </select>
    
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javasec.dao.RoleDao">
    
    <resultMap type="com.javasec.bean.Role" id="RoleMap">
        <result property="id" column="id" jdbcType="VARCHAR"/>
        <result property="rid" column="rid" jdbcType="VARCHAR"/>
        <result property="name" column="name" jdbcType="VARCHAR"/>
        <result property="nameZh" column="nameZh" jdbcType="VARCHAR"/>
    </resultMap>
    
    <select id="getUserRoleByUid" resultMap="RoleMap">
        select *
        from role r,
             user_role ur
        where r.rid = ur.rid
          and ur.uid = #{uid}
    </select>
    
</mapper>

4. 编写springSecurity相关处理器

编写UserServices

进行身份验证之前,Spring Security会调用loadUserByUsername()方法来获取用户信息。该方法通常用于在数据库中查询用户信息,然后将其封装在UserDetails接口的实现类中,并返回给Spring Security。

package com.javasec.sec;


import com.javasec.bean.User;
import com.javasec.dao.RoleDao;
import com.javasec.dao.UserDao;
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 YZK
 * @Date 2023/7/5
 */
@Service
public class UserServices implements UserDetailsService {

    @Resource
    UserDao userDao;
    @Resource
    RoleDao roleDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.loadUserByUsername(username);
        if (Objects.isNull(user)) {
            throw new UsernameNotFoundException("用户不存在");
        }
        user.setRoles(roleDao.getUserRoleByUid(user.getUid()));
        return user;
    }
}

登录处理器

这个处理器中有两个方法,onAuthenticationSuccess()方法主要用于登录成功时为header设置token,并将整个已经登录的对象(authentication)存入到redis中,onAuthenticationFailure()方法主要用于登录失败时返回提示信息。

package com.javasec.sec.handler;

import com.alibaba.fastjson.JSONObject;
import com.javasec.bean.User;
import com.javasec.utils.JwtUtil;
import com.javasec.utils.RedisUtils;
import com.javasec.utils.result.SystemResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author YZK
 * @Date 2023/7/15
 */
@Component
public class CustomAuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler {

    @Resource
    RedisUtils redisUtils;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        User user = (User) authentication.getPrincipal();
        String jwt = JwtUtil.generateToken(user);
        if (redisUtils.exists("login:" + user.getUid())) {
            redisUtils.remove("login:" + user.getUid());
        }
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html; charset=UTF-8");
        redisUtils.set("login:" + user.getUid(), jwt);
        response.setHeader("token", jwt);
        response.getWriter().write(JSONObject.toJSONString(SystemResult.success("登录成功")));
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        response.getWriter().write(JSONObject.toJSONString(SystemResult.fail(401, exception.getMessage())));
    }
}

Jwt过滤器

该过滤器可以拦截每一次请求,并验证header中是否存在token,验证成功则会通过UsernamePasswordAuthenticationToken传给一个authentication provider验证成功则会返回一个带有授权信息的身份验证对象。如果身份验证失败,则应返回一个未通过的身份验证对象。

package com.javasec.sec.filter;

import com.alibaba.fastjson.JSON;
import com.javasec.bean.User;
import com.javasec.utils.JwtUtil;
import com.javasec.utils.RedisUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

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;

@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Resource
    RedisUtils redisUtils;

    public JwtAuthenticationFilter() {
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 从请求头或请求参数中获取JWT token
        String token = request.getHeader("token");
        try {
            Claims claimByToken = JwtUtil.getClaimByToken(token);
            assert claimByToken != null;
            String tem = JSON.toJSONString(claimByToken.get("user"));
            User user = JSON.parseObject(tem, User.class);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    redisUtils.get("login" + user.getUid()), null, user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        } catch (Exception e) {
            // 验证失败,可以进行一些处理
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            log.error(e.getMessage());
        }
        filterChain.doFilter(request, response);
    }
}

5. 本文所用的工具类

响应类

package com.javasec.utils.result;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SystemResult<T> implements Serializable {
    /**
     * 成功失败标识
     */
    private boolean flag;

    /**
     * 响应数据
     */
    private T data;
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 响应消息
     */
    private String message;

    public static Integer SUCCESS_200 = 200;
    public static Integer FAIL_500 = 500;

    public static <T> SystemResult<T> success() {
        return SystemResult.success(null);
    }

    public static <T> SystemResult<T> success(T result) {
        SystemResult<T> systemResult = new SystemResult<>();
        systemResult.setFlag(true);
        systemResult.setData(result);
        systemResult.setMessage("成功");
        systemResult.setCode(SUCCESS_200);
        return systemResult;
    }

    public static <T> SystemResult<T> success(String msg) {
        SystemResult<T> systemResult = new SystemResult<>();
        systemResult.setFlag(true);
        systemResult.setMessage(msg);
        return systemResult;
    }

    public static <T> SystemResult<T> fail(T result) {
        SystemResult<T> systemResult = new SystemResult<>();
        systemResult.setFlag(false);
        systemResult.setCode(FAIL_500);
        systemResult.setData(result);
        return systemResult;
    }

    public static <T> SystemResult<T> fail(String msg) {
        SystemResult<T> systemResult = new SystemResult<>();
        systemResult.setFlag(false);
        systemResult.setCode(FAIL_500);
        systemResult.setMessage(msg);
        return systemResult;
    }

    public static <T> SystemResult<T> fail(T result, String msg) {
        SystemResult<T> systemResult = new SystemResult<>();
        systemResult.setFlag(false);
        systemResult.setCode(FAIL_500);
        systemResult.setMessage(msg);
        systemResult.setData(result);
        return systemResult;
    }
}

Jwt工具类

package com.javasec.utils;

import com.javasec.bean.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
public class JwtUtil {

    //    private long expire;
//    private static final String secret = "admin";
    private String header;

    // 生成jwt
    public static String generateToken(User user) {

        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + 1000 * 604800);
//        Map<String, Object> userMap = new HashMap<>();
//        userMap.put("user",user);
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(user.getUsername())//主题
                .setIssuedAt(nowDate) //jwt的签发时间
                .setExpiration(expireDate) // 7天過期
//                .setPayload(String.valueOf(user))//设置载荷  payload和claims不能同时指定
                .claim("user",user)
                .signWith(SignatureAlgorithm.HS512, "admin")//指定加密算法
                .compact();
    }

    // 解析jwt
    public static Claims getClaimByToken(String jwt) {
        try {
            return (Claims) Jwts.parser()
                    .setSigningKey("admin")
//                    .parseClaimsJwt(jwt)
                    .parse(jwt)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
    }

    // jwt是否过期
    public static boolean isTokenExpired(Claims claims) {
        return claims.getExpiration().before(new Date());
    }

}

Redis工具类

package com.javasec.utils;
 
import com.google.common.collect.HashMultimap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;


/**
 * @author Administrator
 */
@Component
public class RedisUtils {
 
    @Autowired
    private StringRedisTemplate redisTemplate;
 
    public RedisUtils(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
 
    /**
     * 写入缓存
     *
     * @param key   redis键
     * @param value redis值
     * @return 是否成功
     */
    public boolean set(final String key, String value) {
        boolean result = false;
        try {
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
 
 
    /**
     * 写入缓存设置时效时间
     *
     * @param key   redis键
     * @param value redis值
     * @return 是否成功
     */
    public boolean set(final String key, String value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
 
    /**
     * 批量删除对应的键值对
     *
     * @param keys Redis键名数组
     */
    public void removeByKeys(final String... keys) {
        for (String key : keys) {
            remove(key);
        }
    }
 
    /**
     * 批量删除Redis key
     *
     * @param pattern 键名包含字符串(如:myKey*)
     */
    public void removePattern(final String pattern) {
        Set<String> keys = redisTemplate.keys(pattern);
        if (keys != null && keys.size() > 0) {
            redisTemplate.delete(keys);
        }
    }
 
    /**
     * 删除key,也删除对应的value
     *
     * @param key Redis键名
     */
    public void remove(final String key) {
        if (exists(key)) {
            redisTemplate.delete(key);
        }
    }
 
    /**
     * 判断缓存中是否有对应的value
     *
     * @param key Redis键名
     * @return 是否存在
     */
    public Boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }
 
    /**
     * 读取缓存
     *
     * @param key Redis键名
     * @return 是否存在
     */
    public String get(final String key) {
        String result = null;
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }
 
    /**
     * 哈希 添加
     *
     * @param key     Redis键
     * @param hashKey 哈希键
     * @param value   哈希值
     */
    public void hmSet(String key, String hashKey, String value) {
        HashOperations<String, String, String> hash = redisTemplate.opsForHash();
        hash.put(key, hashKey, value);
    }
 
    /**
     * 哈希获取数据
     *
     * @param key     Redis键
     * @param hashKey 哈希键
     * @return 哈希值
     */
    public String hmGet(String key, String hashKey) {
        HashOperations<String, String, String> hash = redisTemplate.opsForHash();
        return hash.get(key, hashKey);
    }
 
    /**
     * 判断hash是否存在键
     *
     * @param key     Redis键
     * @param hashKey 哈希键
     * @return 是否存在
     */
    public boolean hmHasKey(String key, String hashKey) {
        HashOperations<String, String, String> hash = redisTemplate.opsForHash();
        return hash.hasKey(key, hashKey);
    }
 
    /**
     * 删除hash中一条或多条数据
     *
     * @param key      Redis键
     * @param hashKeys 哈希键名数组
     * @return 删除数量
     */
    public long hmRemove(String key, String... hashKeys) {
        HashOperations<String, String, String> hash = redisTemplate.opsForHash();
        return hash.delete(key, hashKeys);
    }
 
    /**
     * 获取所有哈希键值对
     *
     * @param key Redis键名
     * @return 哈希Map
     */
    public Map<String, String> hashMapGet(String key) {
        HashOperations<String, String, String> hash = redisTemplate.opsForHash();
        return hash.entries(key);
    }
 
    /**
     * 保存Map到哈希
     *
     * @param key Redis键名
     * @param map 哈希Map
     */
    public void hashMapSet(String key, Map<String, String> map) {
        HashOperations<String, String, String> hash = redisTemplate.opsForHash();
        hash.putAll(key, map);
    }
 
    /**
     * 列表-追加值
     *
     * @param key   Redis键名
     * @param value 列表值
     */
    public void lPush(String key, String value) {
        ListOperations<String, String> list = redisTemplate.opsForList();
        list.rightPush(key, value);
    }
 
    /**
     * 列表-获取指定范围数据
     *
     * @param key   Redis键名
     * @param start 开始行号
     * @param end   结束行号
     * @return 列表
     */
    public List<String> lRange(String key, long start, long end) {
        ListOperations<String, String> list = redisTemplate.opsForList();
        return list.range(key, start, end);
    }
 
    /**
     * 集合添加
     *
     * @param key   Redis键名
     * @param value 值
     */
    public void add(String key, String value) {
        SetOperations<String, String> set = redisTemplate.opsForSet();
        set.add(key, value);
    }
 
    /**
     * 集合获取
     *
     * @param key Redis键名
     * @return 集合
     */
    public Set<String> setMembers(String key) {
        SetOperations<String, String> set = redisTemplate.opsForSet();
        return set.members(key);
    }
 
    /**
     * 有序集合添加
     *
     * @param key   Redis键名
     * @param value 值
     * @param score 排序号
     */
    public void zAdd(String key, String value, double score) {
        ZSetOperations<String, String> zSet = redisTemplate.opsForZSet();
        zSet.add(key, value, score);
    }
 
    /**
     * 有序集合-获取指定范围
     *
     * @param key        Redis键
     * @param startScore 开始序号
     * @param endScore   结束序号
     * @return 集合
     */
    public Set<String> rangeByScore(String key, double startScore, double endScore) {
        ZSetOperations<String, String> zset = redisTemplate.opsForZSet();
        return zset.rangeByScore(key, startScore, endScore);
    }
 
    /**
     * 模糊查询Redis键名
     *
     * @param pattern 键名包含字符串(如:myKey*)
     * @return 集合
     */
    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }
 
    /**
     * 获取多个hashMap
     *
     * @param keySet
     * @return List<Map < String, String>> hashMap列表
     */
    public List hashMapList(Collection<String> keySet) {
        return redisTemplate.executePipelined(new SessionCallback<String>() {
            @Override
            public <K, V> String execute(RedisOperations<K, V> operations) throws DataAccessException {
                HashOperations hashOperations = operations.opsForHash();
                for (String key : keySet) {
                    hashOperations.entries(key);
                }
                return null;
            }
        });
    }
 
    /**
     * 保存多个哈希表(HashMap)(Redis键名可重复)
     *
     * @param batchMap Map<Redis键名,Map<键,值>>
     */
    public void batchHashMapSet(HashMultimap<String, Map<String, String>> batchMap) {
        // 设置5秒超时时间
        redisTemplate.expire("max", 25, TimeUnit.SECONDS);
        redisTemplate.executePipelined(new RedisCallback<List<Map<String, String>>>() {
 
            @Override
            public List<Map<String, String>> doInRedis(RedisConnection connection) throws DataAccessException {
                Iterator<Map.Entry<String, Map<String, String>>> iterator = batchMap.entries().iterator();
                while (iterator.hasNext()) {
                    Map.Entry<String, Map<String, String>> hash = iterator.next();
                    // 哈希名,即表名
                    byte[] hashName = redisTemplate.getStringSerializer().serialize(hash.getKey());
                    Map<String, String> hashValues = hash.getValue();
                    Iterator<Map.Entry<String, String>> it = hashValues.entrySet().iterator();
                    // 将元素序列化后缓存,即表的多条哈希记录
                    Map<byte[], byte[]> hashes = new HashMap<byte[], byte[]>();
                    while (it.hasNext()) {
                        // hash中一条key-value记录
                        Map.Entry<String, String> entry = it.next();
                        byte[] key = redisTemplate.getStringSerializer().serialize(entry.getKey());
                        byte[] value = redisTemplate.getStringSerializer().serialize(entry.getValue());
                        hashes.put(key, value);
                    }
                    // 批量保存
                    connection.hMSet(hashName, hashes);
                }
                return null;
            }
        });
    }
 
    /**
     * 保存多个哈希表(HashMap)(Redis键名不可以重复)
     *
     * @param dataMap Map<Redis键名,Map<哈希键,哈希值>>
     */
    public void batchHashMapSet(Map<String, Map<String, String>> dataMap) {
        // 设置5秒超时时间
        redisTemplate.expire("max", 25, TimeUnit.SECONDS);
        redisTemplate.executePipelined(new RedisCallback<List<Map<String, String>>>() {
 
            @Override
            public List<Map<String, String>> doInRedis(RedisConnection connection) throws DataAccessException {
                Iterator<Map.Entry<String, Map<String, String>>> iterator = dataMap.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry<String, Map<String, String>> hash = iterator.next();
                    // 哈希名,即表名
                    byte[] hashName = redisTemplate.getStringSerializer().serialize(hash.getKey());
                    Map<String, String> hashValues = hash.getValue();
                    Iterator<Map.Entry<String, String>> it = hashValues.entrySet().iterator();
                    // 将元素序列化后缓存,即表的多条哈希记录
                    Map<byte[], byte[]> hashes = new HashMap<byte[], byte[]>();
                    while (it.hasNext()) {
                        // hash中一条key-value记录
                        Map.Entry<String, String> entry = it.next();
                        byte[] key = redisTemplate.getStringSerializer().serialize(entry.getKey());
                        byte[] value = redisTemplate.getStringSerializer().serialize(entry.getValue());
                        hashes.put(key, value);
                    }
                    // 批量保存
                    connection.hMSet(hashName, hashes);
                }
                return null;
            }
        });
    }
 
    /**
     * 保存多个哈希表(HashMap)列表(哈希map的Redis键名不能重复)
     *
     * @param list Map<Redis键名,Map<哈希键,哈希值>>
     * @see RedisUtils*.batchHashMapSet()*
     */
    public void batchHashMapListSet(List<Map<String, Map<String, String>>> list) {
        // 设置5秒超时时间
        redisTemplate.expire("max", 25, TimeUnit.SECONDS);
        redisTemplate.executePipelined(new RedisCallback<List<Map<String, String>>>() {
 
            @Override
            public List<Map<String, String>> doInRedis(RedisConnection connection) throws DataAccessException {
                for (Map<String, Map<String, String>> dataMap : list) {
                    Iterator<Map.Entry<String, Map<String, String>>> iterator = dataMap.entrySet().iterator();
                    while (iterator.hasNext()) {
                        Map.Entry<String, Map<String, String>> hash = iterator.next();
                        // 哈希名,即表名
                        byte[] hashName = redisTemplate.getStringSerializer().serialize(hash.getKey());
                        Map<String, String> hashValues = hash.getValue();
                        Iterator<Map.Entry<String, String>> it = hashValues.entrySet().iterator();
                        // 将元素序列化后缓存,即表的多条哈希记录
                        Map<byte[], byte[]> hashes = new HashMap<byte[], byte[]>();
                        while (it.hasNext()) {
                            // hash中一条key-value记录
                            Map.Entry<String, String> entry = it.next();
                            byte[] key = redisTemplate.getStringSerializer().serialize(entry.getKey());
                            byte[] value = redisTemplate.getStringSerializer().serialize(entry.getValue());
                            hashes.put(key, value);
                        }
                        // 批量保存
                        connection.hMSet(hashName, hashes);
                    }
                }
                return null;
            }
        });
    }
}

6. SpringScurity配置类

package com.javasec.config;

import com.javasec.sec.UserServices;
import com.javasec.sec.filter.JwtAuthenticationFilter;
import com.javasec.sec.handler.CustomAuthenticationHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;


/**
 * @Author YZK
 * @Date 2023/5/4
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Resource
    UserServices userServices;

    @Resource
    JwtAuthenticationFilter jwtAuthenticationFilter;

    @Resource
    CustomAuthenticationHandler customAuthenticationHandler;


    @Bean
    public PasswordEncoder passwordEncoder() {
        //开启加密
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServices);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            //所有接口都需要登录才能访问
                .anyRequest().authenticated()
                .and()
            //开启表单登录
                .formLogin()
            //登录成功的处理方法
                .successHandler(customAuthenticationHandler)
            //登录失败的处理方法
                .failureHandler(customAuthenticationHandler);
        //关闭csrf
        http.csrf().disable();
        //开启过滤器,并将其置于UsernamePasswordAuthenticationFilter过滤器之前
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

}

三、测试

1. 测试类

有两个账户,一个是root,一个是user,密码都是123456

先写一个测试类,这个类中的接口需要有相关权限的用户才能访问

package com.javasec.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author YZK
 * @Date 2023/8/21
 */
@RestController
public class TestController {
    //该接口需要admin权限才能访问
    @PreAuthorize("hasAuthority('admin')")
    @GetMapping("/test1")
    public String test() {
        return "测试";
    }

    //该接口需要user权限才能访问
    @PreAuthorize("hasAuthority('user')")
    @GetMapping("/test2")
    public String demo() {
        return "这是demo接口";
    }
}

2. 使用root账户登录

SpringSecurity+jwt+redis基于数据库登录认证

访问test1接口,没有问题

SpringSecurity+jwt+redis基于数据库登录认证

访问test2接口,被禁止,权限错误的信息也可以自定义,本项目未自定义

SpringSecurity+jwt+redis基于数据库登录认证

3. 使用user账户登录

同样的登录成功的信息

SpringSecurity+jwt+redis基于数据库登录认证

访问test1接口,被禁止

SpringSecurity+jwt+redis基于数据库登录认证

访问test2接口,访问成功

SpringSecurity+jwt+redis基于数据库登录认证

redis中登录成功存储的jwt,有两个账号登录,所以有两条

SpringSecurity+jwt+redis基于数据库登录认证

总结

后端生成的jwt应该存储在redis中,每次处理请求时都应检验一次用户的权限信息,以及jwt是否过期,在前后端分离的项目中,前端登录后,后端生成的jwt会在请求头中设置,然后前端拿到后,会存储在localstorage中(只是举例,想存哪儿随意),在前端发起请求时,也会携带着token到后端进行检验。