• SpringSecurity 认证实战


    一. 项目数据准备

    1.1 添加依赖

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.33version>
        dependency>
        
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>0.9.1version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.5.3.1version>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/db_drugstore?characterEncoding=utf-8&serverTimezone=UTC
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.2 添加Redis相关配置

    package com.liming.config;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.serializer.SerializerFeature;
    import com.fasterxml.jackson.databind.JavaType;
    import com.fasterxml.jackson.databind.type.TypeFactory;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.SerializationException;
    import com.alibaba.fastjson.parser.ParserConfig;
    
    import java.nio.charset.Charset;
    
    /**
     * 描述:Redis使用FastJson序列化
     * 创建人: 黎明
     * 创建时间: 2023/10/26
     * 版本: 1.0.0
     */
    public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
    
        public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    
        private Class<T> clazz;
    
        static {
            ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        }
    
        public FastJsonRedisSerializer(Class<T> clazz) {
            super();
            this.clazz = clazz;
        }
    
        @Override
        public byte[] serialize(T t) throws SerializationException {
            if (t == null) {
                return new byte[0];
            }
            return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
        }
    
        @Override
        public T deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length <= 0) {
                return null;
            }
            String str = new String(bytes, DEFAULT_CHARSET);
    
            return JSON.parseObject(str, clazz);
        }
    
    
        protected JavaType getJavaType(Class<?> clazz) {
            return TypeFactory.defaultInstance().constructType(clazz);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    package com.liming.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * 描述:Redis配置
     * 创建人: 黎明
     * 创建时间: 2023/10/26
     * 版本: 1.0.0
     */
    @Configuration
    public class RedisConfig {
    
        @Bean
        @SuppressWarnings(value = {"unchecked", "rawtypes"})
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
            RedisTemplate<Object, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(connectionFactory);
    
            FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
    
            // 使用StringRedisSerializer来序列化和反序列化redis的key值
            template.setKeySerializer(new StringRedisSerializer());
            template.setValueSerializer(serializer);
    
            // Hash的key也采用StringRedisSerializer的序列化方式
            template.setHashKeySerializer(new StringRedisSerializer());
            template.setHashValueSerializer(serializer);
    
            template.afterPropertiesSet();
            return template;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    1.3 工具类

    package com.liming.utils;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.JwtBuilder;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    
    import javax.crypto.SecretKey;
    import javax.crypto.spec.SecretKeySpec;
    import java.util.Base64;
    import java.util.Date;
    import java.util.UUID;
    
    /**
     * 描述:JWT工具类
     * 创建人: 黎明
     * 创建时间: 2023/10/26
     * 版本: 1.0.0
     */
    public class JwtUtil {
    
        //有效期为
        public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000  一个小时
        //设置秘钥明文
        public static final String JWT_KEY = "itlils";
    
        public static String getUUID() {
            String token = UUID.randomUUID().toString().replaceAll("-", "");
            return token;
        }
    
        /**
         * 生成jtw
         *
         * @param subject token中要存放的数据(json格式)
         * @return
         */
        public static String createJWT(String subject) {
            JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
            return builder.compact();
        }
    
        /**
         * 生成jtw
         *
         * @param subject   token中要存放的数据(json格式)
         * @param ttlMillis token超时时间
         * @return
         */
        public static String createJWT(String subject, Long ttlMillis) {
            JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
            return builder.compact();
        }
    
        private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
            SecretKey secretKey = generalKey();
            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);
            if (ttlMillis == null) {
                ttlMillis = JwtUtil.JWT_TTL;
            }
            long expMillis = nowMillis + ttlMillis;
            Date expDate = new Date(expMillis);
            return Jwts.builder()
                    .setId(uuid)              //唯一的ID
                    .setSubject(subject)   // 主题  可以是JSON数据
                    .setIssuer("ydlclass")     // 签发者
                    .setIssuedAt(now)      // 签发时间
                    .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                    .setExpiration(expDate);
        }
    
        /**
         * 创建token
         *
         * @param id
         * @param subject
         * @param ttlMillis
         * @return
         */
        public static String createJWT(String id, String subject, Long ttlMillis) {
            JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
            return builder.compact();
        }
    
    
        /**
         * 生成加密后的秘钥 secretKey
         *
         * @return
         */
        public static SecretKey generalKey() {
            byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
            SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
            return key;
        }
    
        /**
         * 解析
         *
         * @param jwt
         * @return
         * @throws Exception
         */
        public static Claims parseJWT(String jwt) throws Exception {
            SecretKey secretKey = generalKey();
            return Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(jwt)
                    .getBody();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    package com.liming.utils;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.BoundSetOperations;
    import org.springframework.data.redis.core.HashOperations;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ValueOperations;
    import org.springframework.stereotype.Component;
    
    import java.util.*;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 描述:redis缓存工具类
     * 创建人: 黎明
     * 创建时间: 2023/10/26
     * 版本: 1.0.0
     */
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    @Component
    public class RedisCache {
        @Autowired
        public RedisTemplate redisTemplate;
    
        /**
         * 缓存基本的对象,Integer、String、实体类等
         *
         * @param key   缓存的键值
         * @param value 缓存的值
         */
        public <T> void setCacheObject(final String key, final T value) {
            redisTemplate.opsForValue().set(key, value);
        }
    
        /**
         * 缓存基本的对象,Integer、String、实体类等
         *
         * @param key      缓存的键值
         * @param value    缓存的值
         * @param timeout  时间
         * @param timeUnit 时间颗粒度
         */
        public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
            redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
        }
    
        /**
         * 设置有效时间
         *
         * @param key     Redis键
         * @param timeout 超时时间
         * @return true=设置成功;false=设置失败
         */
        public boolean expire(final String key, final long timeout) {
            return expire(key, timeout, TimeUnit.SECONDS);
        }
    
        /**
         * 设置有效时间
         *
         * @param key     Redis键
         * @param timeout 超时时间
         * @param unit    时间单位
         * @return true=设置成功;false=设置失败
         */
        public boolean expire(final String key, final long timeout, final TimeUnit unit) {
            return redisTemplate.expire(key, timeout, unit);
        }
    
        /**
         * 获得缓存的基本对象。
         *
         * @param key 缓存键值
         * @return 缓存键值对应的数据
         */
        public <T> T getCacheObject(final String key) {
            ValueOperations<String, T> operation = redisTemplate.opsForValue();
            return operation.get(key);
        }
    
        /**
         * 删除单个对象
         *
         * @param key
         */
        public boolean deleteObject(final String key) {
            return redisTemplate.delete(key);
        }
    
        /**
         * 删除集合对象
         *
         * @param collection 多个对象
         * @return
         */
        public long deleteObject(final Collection collection) {
            return redisTemplate.delete(collection);
        }
    
        /**
         * 缓存List数据
         *
         * @param key      缓存的键值
         * @param dataList 待缓存的List数据
         * @return 缓存的对象
         */
        public <T> long setCacheList(final String key, final List<T> dataList) {
            Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
            return count == null ? 0 : count;
        }
    
        /**
         * 获得缓存的list对象
         *
         * @param key 缓存的键值
         * @return 缓存键值对应的数据
         */
        public <T> List<T> getCacheList(final String key) {
            return redisTemplate.opsForList().range(key, 0, -1);
        }
    
        /**
         * 缓存Set
         *
         * @param key     缓存键值
         * @param dataSet 缓存的数据
         * @return 缓存数据的对象
         */
        public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
            BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
            Iterator<T> it = dataSet.iterator();
            while (it.hasNext()) {
                setOperation.add(it.next());
            }
            return setOperation;
        }
    
        /**
         * 获得缓存的set
         *
         * @param key
         * @return
         */
        public <T> Set<T> getCacheSet(final String key) {
            return redisTemplate.opsForSet().members(key);
        }
    
        /**
         * 缓存Map
         *
         * @param key
         * @param dataMap
         */
        public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
            if (dataMap != null) {
                redisTemplate.opsForHash().putAll(key, dataMap);
            }
        }
    
        /**
         * 获得缓存的Map
         *
         * @param key
         * @return
         */
        public <T> Map<String, T> getCacheMap(final String key) {
            return redisTemplate.opsForHash().entries(key);
        }
    
        /**
         * 往Hash中存入数据
         *
         * @param key   Redis键
         * @param hKey  Hash键
         * @param value 值
         */
        public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
            redisTemplate.opsForHash().put(key, hKey, value);
        }
    
        /**
         * 获取Hash中的数据
         *
         * @param key  Redis键
         * @param hKey Hash键
         * @return Hash中的对象
         */
        public <T> T getCacheMapValue(final String key, final String hKey) {
            HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
            return opsForHash.get(key, hKey);
        }
    
        /**
         * 删除Hash中的数据
         *
         * @param key
         * @param hkey
         */
        public void delCacheMapValue(final String key, final String hkey) {
            HashOperations hashOperations = redisTemplate.opsForHash();
            hashOperations.delete(key, hkey);
        }
    
        /**
         * 获取多个Hash中的数据
         *
         * @param key   Redis键
         * @param hKeys Hash键集合
         * @return Hash对象集合
         */
        public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
            return redisTemplate.opsForHash().multiGet(key, hKeys);
        }
    
        /**
         * 获得缓存的基本对象列表
         *
         * @param pattern 字符串前缀
         * @return 对象列表
         */
        public Collection<String> keys(final String pattern) {
            return redisTemplate.keys(pattern);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    /**
     * 描述:web工具类
     * 创建人: 黎明
     * 创建时间: 2023/10/26
     * 版本: 1.0.0
     */
    public class WebUtils {
        /**
         * 将字符串渲染到客户端
         *
         * @param response 渲染对象
         * @param string   待渲染的字符串
         * @return null
         */
        public static String renderString(HttpServletResponse response, String string) {
            try {
                response.setStatus(200);
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.getWriter().print(string);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    1.4 实体类

    /**
     * 描述:用户
     * 创建人: 黎明
     * 创建时间: 2023/10/26
     * 版本: 1.0.0
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @TableName(value = "sys_user")
    public class User implements Serializable {
        private static final long serialVersionUID = 1L;
    
        /**
         * 主键
         */
        @TableId(type = IdType.AUTO)
        private Long id;
        /**
         * 用户名
         */
        private String userName;
        /**
         * 昵称
         */
        private String nickName;
        /**
         * 密码
         */
        private String password;
        /**
         * 账号状态(0正常 1停用)
         */
        private String status;
        /**
         * 邮箱
         */
        private String email;
        /**
         * 手机号
         */
        private String phonenumber;
        /**
         * 用户性别(0男,1女,2未知)
         */
        private String sex;
        /**
         * 头像
         */
        private String avatar;
        /**
         * 用户类型(0管理员,1普通用户)
         */
        private String userType;
        /**
         * 创建人的用户id
         */
        private Long createBy;
        /**
         * 创建时间
         */
        private Date createTime;
        /**
         * 更新人
         */
        private Long updateBy;
        /**
         * 更新时间
         */
        private Date updateTime;
        /**
         * 删除标志(0代表未删除,1代表已删除)
         */
        private Integer delFlag;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    1.4 数据库表设计

    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user` (
      `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
      `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
      `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
      `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
      `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
      `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
      `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
      `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
      `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
      `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
      `create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      `update_by` bigint DEFAULT NULL COMMENT '更新人',
      `update_time` datetime DEFAULT NULL COMMENT '更新时间',
      `del_flag` int DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
    
    -- ----------------------------
    -- Records of sys_user
    -- ----------------------------
    INSERT INTO `sys_user` VALUES ('1', 'liming', '黎明', '$2a$10$CLZ2BaVAaNnWJAi49MfdG.Tl9m4hKhMJOTNpPI34ftMnjQOLGl.M6', '0', '123@qq.com', '13012345678', '0', 'a', '1', '1', '2023-10-26 18:52:41', '1', '2023-10-26 18:52:49', '0');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    二.数据库校验用户

    自定义一个UserDetailsService的实现类,让SpringSecurity使用我们的实现类。从数据库中查询用户名和密码。

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private UserMapper userMapper;
    
        /**
         * 按用户名加载用户
         *
         * @param username 用户名
         * @return {@link UserDetails}
         * @throws UsernameNotFoundException 用户名未发现异常
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 1.根据用户名获取数据库中用户信息
            LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
            lqw.eq(User::getUserName, username);
            User user = userMapper.selectOne(lqw);
            if (Objects.isNull(user)) {
                throw new RuntimeException("用户名错误!");
            }
            // 2.@TODO 获取用户权限信息
    
            // 3.返回UserDetails对象
            return new LoginUser(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class LoginUser implements UserDetails {
    
        private User user;
    
        /**
         * 获取权限
         *
         * @return {@link Collection}<{@link ?} {@link extends} {@link GrantedAuthority}>
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return null;
        }
    
    
        /**
         * 得到密码
         *
         * @return {@link String}
         */
        @Override
        public String getPassword() {
            return user.getPassword();
        }
    
    
        /**
         * 获得用户名
         *
         * @return {@link String}
         */
        @Override
        public String getUsername() {
            return user.getUserName();
        }
    
    
        /**
         * 账户未过期吗?
         *
         * @return boolean
         */
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
    
        /**
         * 账户未锁定吗?
         *
         * @return boolean
         */
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
    
        /**
         * 证书是否未过期?
         *
         * @return boolean
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
    
        /**
         * 启用了
         *
         * @return boolean
         */
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    三.密码加密存储

    实际项目中我们不会把密码明文存储在数据库中,​ 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder,我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。​ 我们可以定义一个SpringSecurity的配置类。

    @Configuration
    public class SecurityConfig{
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    使用BCryptPasswordEncoder的好处:

    四.自定义登陆接口

    4.1 自定义一个controller登陆接口

    @RestController
    public class LoginController {
        @Autowired
        private LoginService loginService;
    
        @PostMapping("/user/login")
        public Result<Map<String, String>> login(@RequestBody User user) {
    
            return loginService.login(user);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4.2 放行自定义登陆接口

    @Configuration
    public class SecurityConfig {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http
                    //关闭csrf
                    .csrf().disable()
                    //不通过Session获取SecurityContext
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    // 对于登录接口 允许匿名访问
                    .antMatchers("/user/login").anonymous()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated();
    
            return http.build();
        }
    
        // 注入AuthenticationConfiguration
        @Autowired
        private AuthenticationConfiguration authenticationConfiguration;
    
    
        /**
         * 通过authenticationConfiguration获取一个AuthenticationManager实例
         * 这个Bean可以在其他地方被引用,以实现用户认证的功能
         *
         * @return {@link AuthenticationManager}
         * @throws Exception 异常
         */
        @Bean
        public AuthenticationManager authenticationManager() throws Exception {
            AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
            return authenticationManager;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    4.3 使用AuthenticationManager auth方法进行验证

    @Service
    public class LoginServiceImpl implements LoginService {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private RedisCache redisCache;
    
        /**
         * 登录
         *
         * @param user 用户
         * @return {@link Result}
         */
        @Override
        public Result<Map<String, String>> login(User user) {
            // 3.使用AuthenticationManager authenticate方法进行验证
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
            Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
    
            //校验失败了
            if (Objects.isNull(authenticate)) {
                throw new AppException(AppExceptionCodeMsg.USERNAME_PASSWORD_ERR);
            }
    
            // 4.自己生成jwt给前端
            LoginUser loginUser = (LoginUser) (authenticate.getPrincipal());
            String userId = loginUser.getUser().getId().toString();
            String jwt = JwtUtil.createJWT(userId);
            Map<String, String> map = new HashMap<>();
            map.put("token", jwt);
    
            // 5.系统用户相关所有信息放入redis
            redisCache.setCacheObject("login:" + userId, loginUser);
    
            return Result.success("登陆成功", map);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    五.认证过滤器

    spring securirt自带16个过滤器链,我们前面自定义修改了UsernamePasswordAuthenticationFilter过滤器,但是后面的过滤器也需要使用Authentication 用户登录权限信息,所有我们就需要一个认证过滤器将用户的信息Authentication存入到SecurityContextHolder供其他过滤器使用

    实现步骤:

    • 获取token

    • 解析token

    • 获取userid

    • 封装Authentication

    • 存入SecurityContextHolder

    /**
     * 描述:JWT身份验证令牌过滤器
     * 创建人: 黎明
     * 创建时间: 2023/10/27
     * 版本: 1.0.0
     * OncePerRequestFilter 只走一次,请求前
     */
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        @Autowired
        private RedisCache redisCache;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // 1.获取token  header的token
            String token = request.getHeader("token");
            if (!StringUtils.hasText(token)) {
                //放行,让后面的过滤器执行报错
                filterChain.doFilter(request, response);
                return;
            }
            // 2.解析token
            String userId;
            try {
                Claims claims = JwtUtil.parseJWT(token);
                userId = claims.getSubject();
            } catch (Exception e) {
                throw new AppException(AppExceptionCodeMsg.TOKEN_ERR);
            }
    
            // 3.获取userId, redis获取用户信息
            LoginUser loginUser = redisCache.getCacheObject("login:" + userId);
            if (Objects.isNull(loginUser)) {
                throw new AppException(AppExceptionCodeMsg.LOGIN_ERR);
            }
    
            // 4.封装Authentication
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
                    = new UsernamePasswordAuthenticationToken(loginUser, null, null);
    
            // 5.存入SecurityContextHolder
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
    
            //放行,让后面的过滤器执行
            filterChain.doFilter(request, response);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    过滤器配置(SecurityConfig文件中):

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    //把token校验过滤器添加到过滤器链中(filterChain方法中)
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    测试登录:

    未登陆访问资源的话

    在这里插入图片描述

    携带token访问

    在这里插入图片描述

    六.退出登陆

    写一个退出接口,删除reids里的key即可

    LoginController:

    @PostMapping("/user/logout")
    public Result logout(){
         return loginService.logout();
    }
    
    • 1
    • 2
    • 3
    • 4

    LoginServiceImpl:

     @Override
     public Result logout() {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
         LoginUser loginUser = (LoginUser) authentication.getPrincipal();
         Long userId = loginUser.getUser().getId();
         redisCache.deleteObject("login:" + userId);
         return Result.success("退出成功!");
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    测试:

    登陆:

    在这里插入图片描述

    访问资源:

    在这里插入图片描述

    退出:

    在这里插入图片描述

    再访问资源:

    在这里插入图片描述

  • 相关阅读:
    PostGIS学习教程六:几何图形(geometry)
    sequence启动的两种方式
    【Leetcode刷题笔记05】242.有效的字母异位词 349. 两个数组的交集 202. 快乐数 1. 两数之和
    javaweb基础:tomcat的安装,以及目录结构
    C++中string的用法总结+底层剖析
    小球反弹(蓝桥杯)
    华为OD 最大岛屿体积(100分)【java】A卷+B卷
    19 【移动Web开发之flex布局】
    ZBrush软件这些实用小技巧,你知道几个?
    软件测试中如何测试算法?
  • 原文地址:https://blog.csdn.net/weixin_46370595/article/details/134060673