SpringBoot整合SpringSecrity+JWT

在这里插入图片描述
— 需要的技术 —

技术说明官网
springbootMVC框架https://spring.io/projects/spring-boot
mybatis-plusORM框架https://baomidou.com
SpringSecurity认证和授权框架https://spring.io/projects/spring-security
Redis分布式缓存https://redis.io/
JWTJWT登录支持https://github.com/jwtk/jjwt

— 身份认证流程 —
在这里插入图片描述
— 依赖 —

        <!--    spring-boot-starter-parent     -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.6.4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--mybatis-plus-boot-starter 包含了mybatis的所有依赖-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.4.2</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>3.4.1</version>
            </dependency>
            <dependency>
                <groupId>org.freemarker</groupId>
                <artifactId>freemarker</artifactId>
                <version>2.3.28</version>
            </dependency>
                        <!--使用连接池的方式管理连接-->
            <!--推荐大家使用阿里巴巴的druid的连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.2.8</version>
            </dependency>
                    <!--引入mysql数据库相关依赖-->
            <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
             </dependency>
             <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity-engine-core</artifactId>
                <version>2.2</version>
            </dependency>
            <!-- spring-boot-starter-security       -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <version>2.6.4</version>
            </dependency>
           <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>2.6.4</version>
            </dependency>
           <!--生成身份令牌-->
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.0.10.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.0</version>
            </dependency>
         <!--消除模板代码,生成getter、setter、构造器、toString()、equals()-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.16.10</version>
            </dependency>
              <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.47</version>
            </dependency>

— yml配置 —

server:
  port: 8081

spring:
  application:
    name: manage-client
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: root
  main:
    allow-circular-references: true
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        max-active: 100
        max-idle: 300
        max-wait: 10000
      cluster:
        refresh:
          adaptive: true
    timeout: 5000

#mybatis-plus配置
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:com/huangyabei/mapper/xml/*.xml

— 工具类 —
JwtUtil

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Slf4j
@Component
//@ConfigurationProperties(prefix = "jwt")
public class JwtUtil {
    /**
     * 携带JWT令牌的HTTP的Header的名称,在实际生产中可读性越差越安全
     */
//    @Getter
//    @Value("${jwt.header}")
    private String header = "Authorization";

    /**
     * 为JWT基础信息加密和解密的密钥
     * 在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改。
     */
//    @Value("${jwt.secret}")
    private String secret = "guYloAPAmKwvKq4a5f5dqnifiQatxMEPNOvtwPsJPQWLNKJDCXZ";

    /**
     * JWT令牌的有效时间,单位秒
     * - 默认1周
     */
//    @Value("${jwt.expiration}")
    private Long expiration = 604800L;

    /**
     * SecretKey 根据 SECRET 的编码方式解码后得到:
     * Base64 编码:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
     * Base64URL 编码:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString));
     * 未编码:SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
     */
    private static SecretKey getSecretKey(String secret) {
        byte[] encodeKey = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(encodeKey);
    }

    /**
     * 用claims生成token
     *
     * @param claims 数据声明,用来创建payload的私有声明
     * @return token 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        SecretKey key = getSecretKey(secret);
        //SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //两种方式等价

        // 添加payload声明
        JwtBuilder jwtBuilder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(UUID.randomUUID().toString())
                // iat: jwt的签发时间
                .setIssuedAt(new Date())

                // 你也可以改用你喜欢的算法,支持的算法详见:https://github.com/jwtk/jjwt#features
                // SignatureAlgorithm.HS256:指定签名的时候使用的签名算法,也就是header那部分
                .signWith(SignatureAlgorithm.HS256, key)
                .setExpiration(new Date(System.currentTimeMillis() + this.expiration * 1000));

        String token = jwtBuilder.compact();
        return token;
    }

    /**
     * 生成Token令牌
     *
     * @param userDetails 用户
     * @param id          用户编号
     * @return 令牌Token
     */
    public  String generateToken(UserDetails userDetails, String id) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", id);
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        return generateToken(claims);
    }

    /**
     * 从token中获取数据声明claim
     *
     * @param token 令牌token
     * @return 数据声明claim
     */
    public Claims getClaimsFromToken(String token) {
        try {
            SecretKey key = getSecretKey(secret);
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            return claims;
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
            log.error("token解析错误", e);
            throw new IllegalArgumentException("Token invalided.");
        }
    }

    public String getUserId(String token) {
        return (String) getClaimsFromToken(token).get("userId");
    }

    /**
     * 从token中获取登录用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getSubjectFromToken(String token) {
        String subject;
        try {
            Claims claims = getClaimsFromToken(token);
            subject = claims.getSubject();
        } catch (Exception e) {
            subject = null;
        }
        return subject;
    }


    /**
     * 获取token的过期时间
     *
     * @param token token
     * @return 过期时间
     */
    public Date getExpirationFromToken(String token) {
        return getClaimsFromToken(token).getExpiration();
    }

    /**
     * 判断token是否过期
     *
     * @param token 令牌
     * @return 是否过期:已过期返回true,未过期返回false
     */
    public Boolean isTokenExpired(String token) {
        Date expiration = getExpirationFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 验证令牌:判断token是否非法
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 如果token未过期且合法,返回true,否则返回false
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        //如果已经过期返回false
        if (isTokenExpired(token)) {
            return false;
        }
        String usernameFromToken = getSubjectFromToken(token);
        String username = userDetails.getUsername();
        return username.equals(usernameFromToken);
    }

}

Redis
RedisConfig

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        //使用fastjson序列化
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}

RedisUtil

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

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

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        try {
            return redisTemplate.keys(pattern);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(final String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(final String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(final String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public <T> T get(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return key == null ? null : operation.get(key);
    }
    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public <T> boolean set(final String key,final T value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入, 不存在放入,存在返回
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public <T> boolean setnx(final String key,final T value) {
        try {
            redisTemplate.opsForValue().setIfAbsent(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 <T> boolean set(final String key,final T value,final 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 value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public <T> boolean setnx(final String key,final T value,final long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().setIfAbsent(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(final String key,final long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(final String key,final long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public <T> T hget(final  String key,final  String item) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, item);
    }
    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public <T> Map<String, T> hmget(final String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public <T> boolean hmset(final String key,final Map<String, T> 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 <T> boolean hmset(final String key,final Map<String, T> map,final 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 <T> boolean hset(final String key,final String item,final T value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public <T> boolean hset(final String key,final String item,final T value,final 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(final String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(final String key,final String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(final String key,final String item,final double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(final String key,final String item,final double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public <T> Set<T> sGet(final 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 <T> boolean sHasKey(final String key,final T value) {
        try {
            if (null == value){
                return false;
            }
            SetOperations setOperations = redisTemplate.opsForSet();
            return setOperations.isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public <T> BoundSetOperations<String, T> sSet(final String key, final Set<T> dataSet) {
        try {
            BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
            Iterator<T> it = dataSet.iterator();
            while (it.hasNext())
            {
                setOperation.add(it.next());
            }
            return setOperation;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public <T> BoundSetOperations<String, T> sSetAndTime(final String key,final long time,final Set<T> dataSet) {
        try {
            BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
            Iterator<T> it = dataSet.iterator();
            while (it.hasNext())
            {
                setOperation.add(it.next());
            }
            if (time > 0)
                expire(key, time);
            return setOperation;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(final String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(final 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 到 -1代表所有值
     * @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
     */
    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 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    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
     */
    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
     */
    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;
        }
    }
}

WebUtil

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class WebUtil {

    /**
     * 将字符串渲染到客户端
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static void renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    public static void setDownLoadHeader(String filename, ServletContext context, HttpServletResponse response) throws UnsupportedEncodingException {
        String mimeType = context.getMimeType(filename);//获取文件的mime类型
        response.setHeader("content-type",mimeType);
        String fname= URLEncoder.encode(filename,"UTF-8");
        response.setHeader("Content-disposition","attachment; filename="+fname);
    }

}

-----------------------------------------核心代码部分--------------------------------------------
我们可以自定义一个UserDetailsService,让SpringSecrity使用我们的UserDetailsService,我们自己的UserDetailsService可以从数据库中查询用户名/密码/用户权限
— Entity —
普通数据库表实体类(可以根据自己业务需要设置user实体)

/**
 * <p>
 * 用户表
 * </p>
 *
 * @author car-hailing-saas
 * @since 2022-04-12
 */
@Data
@TableName("t_tenant_user")
public class TenantUserEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 用户id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 姓名
     */
    private String realName;

    /**
     * 密码
     */
    private String passWord;

    /**
     * 租户ID
     */
    private Long tenantId;

    /**
     * 性别 0男 1女
     */
    private Integer sex;

    /**
     * 手机号
     */
    private String mobile;

    /**
     * 状态:(0:禁用,1:正常)
     */
    private Integer state;

    /**
     * 是否为超级管理员(0:否,1:是)
     */
    private Integer ifSuper;

    /**
     * 头像
     */
    private String headImage;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 联系电话
     */
    private String telephone;

    /**
     * 公司所在城市编号
     */
    private String cityNo;

    /**
     * 公司详细地址
     */
    private String detailedAddress;

    /**
     * 组织机构ID
     */
    private Long organizationId;

    /**
     * 职位ID
     */
    private Long positionId;

    /**
     * 工号
     */
    private String jobNumber;

    /**
     * 是否首次修改密码 0 首次  1 非首次
     */
    private Integer firstUpdatePwd;
    /**
     *删除状态(0:正常,1:删除)
     */
    private Integer delState;

    /**
     * 创建人ID
     */
    private Long createBy;

    /**
     * 创建时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")
    private Date createTime;

    /**
     * 更新者ID
     */
    private Long updateBy;

    /**
     * 更新的时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")
    private Date updateTime;

}

自定义实现 SpringSecrity的 UserDetails 接口的实体

import com.alibaba.fastjson.annotation.JSONField;
import com.car.hailing.saas.model.entity.TenantUserEntity;
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 org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
public class LoginTenantUserEntity implements UserDetails {

    /**
     *用户对象(这里是自己的用户实体,对应数据库中user的实体,根据需要进行修改)
     */
    private TenantUserEntity tenantUserEntity;

    /**
     * 权限集合(用来存从数据库查到的用户权限))
     */
    private List<String> permissions;

    //忽略序列化   SpringSecrity权限校验需要的格式的权限集合
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    public LoginTenantUserEntity(TenantUserEntity tenantUserEntity, List<String> permissions) {
        this.tenantUserEntity = tenantUserEntity;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (!CollectionUtils.isEmpty(authorities)){
            return authorities;
        }
        //把 permissions 中的String类型权限信息,封装成SimpleGrantedAuthority对象
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        //将用户实体中的用户密码指向 UserDetails的getPassword方法
        return tenantUserEntity.getPassWord();
    }

    @Override
    public String getUsername() {
        //将用户实体中的用户名指向 UserDetails的getPassword方法
        return tenantUserEntity.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;
    }
}

— UserDetailsServiceImpl —

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.car.hailing.saas.dao.TenantUserMapper;
import com.car.hailing.saas.manage.entity.LoginTenantUserEntity;
import com.car.hailing.saas.model.entity.TenantUserEntity;
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.List;
import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    //根据自己业务需要定义查询接口
    @Resource
    TenantUserMapper tenantUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息 根据自己业务需要定义查询接口
        QueryWrapper<TenantUserEntity> qw = new QueryWrapper<>();
        qw.eq("user_name",username);
        TenantUserEntity tenantUserEntity = tenantUserMapper.selectOne(qw);
        if (Objects.isNull(tenantUserEntity)){
           throw new UsernameNotFoundException("用户名未找到");
        }
        // TODO 查询对应的权限信息 根据自己业务需要定义查询接口
        QueryWrapper<TenantUserEntity> qwMenys = new QueryWrapper<>();
        qwMenys.eq("user_id",tenantUserEntity.getId());
        List<String> list = tenantUserMapper.selectMenusList(qwMenys);
        return new LoginTenantUserEntity(tenantUserEntity,list);
    }
}

JwtAuthenticationTokenFilter 过滤器

import com.alibaba.fastjson.JSON;
import com.car.hailing.saas.manage.entity.LoginTenantUserEntity;
import com.car.hailing.saas.utils.JwtUtil;
import com.car.hailing.saas.utils.redis.RedisUtil;
import io.jsonwebtoken.Claims;
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 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;

/**
 * 认知过滤器
 */

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    JwtUtil jwtUtil;
    @Resource
    RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //1.获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)){
            //这里的放行是请求中没有token而放行,比如登录接口这种白名单接口,会继续执行后面的过滤器
            filterChain.doFilter(request,response);
            return;
        }
        //2.解析token
        Claims claims = jwtUtil.getClaimsFromToken(token);
        Long userId = Long.valueOf(String.valueOf(claims.get("userId")));
        //3.从redis中获取用户信息
        Object userJson = redisUtil.hget("USER_TOKEN", userId + token);
        //校验redis中是否存在user信息
        if (Objects.isNull(userJson)){
            throw new RuntimeException("用户未登录");
        }
        LoginTenantUserEntity loginTenantUserEntity = JSON.parseObject(String.valueOf(userJson), LoginTenantUserEntity.class);
        //存入 SecurityContextHolder
        // TODO 获取权限信息封装到 Authentication 中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginTenantUserEntity,null,loginTenantUserEntity.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //对请求放行
        //这里的放行是已经对用户身份信息校验完毕了进行放行
        filterChain.doFilter(request,response);
    }
}

AuthenticationEntryPointHandler 授权认证异常处理器

import com.alibaba.fastjson.JSON;
import com.car.hailing.saas.base.CommonResult;
import com.car.hailing.saas.manage.utils.WebUtil;
import org.apache.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * security 授权认证异常处理器
 */

@Component
public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
    /**
     *身份认证失败处理
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //处理异常
        WebUtil.renderString(response, JSON.toJSONString(new CommonResult<>(HttpStatus.SC_UNAUTHORIZED,"用户认证失败!")));

    }

    /**
     *权限认证失败处理
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //处理异常
        WebUtil.renderString(response, JSON.toJSONString(new CommonResult<>(HttpStatus.SC_FORBIDDEN,"用户权限不足!")));
    }
}

**SecrityConfig 配置类 **

import com.car.hailing.saas.manage.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)  //启用Spring Security Global方法安全性类
public class SecrityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 密码加密/校验
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    AccessDeniedHandler accessDeniedHandler;
    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // anonymous()允许匿名访问 // permitAll() 任何人都可以访问  // denyAll() 任何人都不允许访问
                // 基于注解配置接口权限 hasAuthority() 单个权限 hasAnyAuthority()多个权限
                .antMatchers("/index/login").anonymous()
                //除上面外的所有请求全部需要鉴权认证,指定任何身份验证的用户允许使用URL
                .anyRequest().authenticated();

        //将自定义的认证过滤器添加到secruty过滤器链最前面的位置
        http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);

        //添加异常处理器
        http.exceptionHandling()
                //认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

        //允许跨越
        http.cors();

    }

    /**
     * 身份验证管理器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

CorsConfig Spring boot 跨域配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Spring boot 跨域配置类
 */

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置允许跨越的路径
        registry.addMapping("/**")
                //设置允许跨越请求的域名
                .allowedOriginPatterns("*")
                //是否允许cookie
                .allowCredentials(true)
                //设置允许的请求方式
                .allowedMethods("GET","POST","DELETE","PUT")
                //设置允许的header属性
                .allowedHeaders("*")
                //跨越允许时间 配置客户可以通过客户端缓存的飞行前请求的响应时间。
                //默认情况下,这将设置为1800秒
                .maxAge(900);
    }
}

---------模拟登录登出接口实现----------

Controller

import com.car.hailing.saas.base.CommonResult;
import com.car.hailing.saas.manage.entity.LoginParam;
import com.car.hailing.saas.manage.service.LoginService;
import com.google.common.collect.ImmutableMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/index")
public class LoginController {

    @Autowired
    public LoginService loginService;

    @RequestMapping(value = "/login",method = RequestMethod.POST)
    private CommonResult login(@RequestBody LoginParam loginParam){
        String token = loginService.login(loginParam);
        return new CommonResult(ImmutableMap.of("token",token));
    }

    @RequestMapping(value = "/logout",method = RequestMethod.POST)
    private CommonResult logout(HttpServletRequest request){
        loginService.logout(request);
        return new CommonResult();
    }
}

Service

import com.baomidou.mybatisplus.extension.service.IService;
import com.car.hailing.saas.manage.entity.LoginParam;
import com.car.hailing.saas.model.entity.TenantUserEntity;

import javax.servlet.http.HttpServletRequest;

public interface LoginService extends IService<TenantUserEntity> {

    /**
     * 退出登录
     */
    void logout(HttpServletRequest request);

    /**
     * 登录
     * @param loginParam
     * @return
     */
    String login(LoginParam loginParam);
}

LoginServiceImpl

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.car.hailing.saas.dao.TenantUserMapper;
import com.car.hailing.saas.manage.entity.LoginParam;
import com.car.hailing.saas.manage.entity.LoginTenantUserEntity;
import com.car.hailing.saas.manage.service.LoginService;
import com.car.hailing.saas.model.entity.TenantUserEntity;
import com.car.hailing.saas.utils.JwtUtil;
import com.car.hailing.saas.utils.redis.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;

@Service
public class LoginServiceImpl extends ServiceImpl<TenantUserMapper, TenantUserEntity> implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Resource
    JwtUtil jwtUtil;
    @Resource
    RedisUtil redisUtil;

    @Override
    public void logout(HttpServletRequest request) {
        String token = request.getHeader("token");
        String userId = jwtUtil.getUserId(token);
        redisUtil.hdel("USER_TOKEN",userId+token);
    }

    @Override
    public String login(LoginParam loginParam) {

        //AuthenticationManager authenticate 进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginParam.getLoginName(),loginParam.getPassWord());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果认证没通过,给出对应的提示
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败!");
        }
        LoginTenantUserEntity loginTenantUserEntity = (LoginTenantUserEntity) authenticate.getPrincipal();
        String userId = loginTenantUserEntity.getTenantUserEntity().getId().toString();
        String token = jwtUtil.generateToken(loginTenantUserEntity, userId);
        redisUtil.hset("USER_TOKEN", userId+token, loginTenantUserEntity, 604800L);
        return token;
    }
}

TenantUserMapper

/**
 * <p>
 * 租户用户表 Mapper 接口
 * </p>
 *
 * @author car-hailing-saas
 * @since 2022-04-12
 */
@Mapper
public interface TenantUserMapper extends BaseMapper<TenantUserEntity> {

}

认证失败的效果
在这里插入图片描述
— 自定义权限认证 (如果有特殊的权限验证要求可以自定义权限验证方法)—

***例如: ***
1.在实现了UserDetails接口的实体中定义一个需要验证的权限集合
LoginTenantUserEntity

    /**
     * 权限集合
     */
    private List<String> permissions;

2.在实现了UserDetailsService的实现类中查询出需要校验的权限并添加到实体中(注意修改构造方法)
UserDetailsServiceImpl

        // TODO 查询对应的权限信息
        List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
        return new LoginTenantUserEntity(tenantUserEntity,list);

3.编写自定义权限校验类

import com.car.hailing.saas.manage.entity.LoginTenantUserEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 自定义权限校验
 * 添加到spring容器中最好自定义一个Bean名称,这样在注解中用到就会比较清晰
 */
@Component("customize")
public class CustomizeExpressionRoot {

    public boolean customizeHasAuthority(String authority){
        //获取当前用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginTenantUserEntity loginTenantUserEntity = (LoginTenantUserEntity) authentication.getPrincipal();
        List<String> permissions = loginTenantUserEntity.getPermissions();
        //判断用户群权限集合中是否存在authority
        return permissions.contains(authority);
    }
}

Coenroller (注解模式验证添加权限验证)

@RestController
@RequestMapping("/test")
public class SecrityController {

    @RequestMapping(value = "/index",method = RequestMethod.GET)
    //Secrity 的权限验证方法不需要 加@ 符号 
//    @PreAuthorize("hasAuthority('test')")
    //自定义的权限验证类 bean名字前需要加 @ 符号  test 是这个接口需要具备的访问权限    
    @PreAuthorize("@customize.customizeHasAuthority('test')")  
    public CommonResult test(){
        return new CommonResult("OK");
    }
}

注解的方法与配置用的方法是一样的,只是可以使用配置类方式或者是注解方式来配置

hasAuthority(String authority)  //单个权限校验
hasAnyAuthority(String... authorities) //多个权限校验
hasRole(String role) //单个权限校验  这个方法会在传入的权限字符串前默认拼一个 defaultRolePrefix = "ROLE_";
hasAnyRole(String... roles)  // 多个权限校验  这个方法会在传入的权限字符串前默认拼一个 defaultRolePrefix = "ROLE_";
isAnonymous() //允许匿名访问 
permitAll() // 任何人都可以访问 
denyAll() // 任何人都不允许访问

源码:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值