欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring Security實(shí)現(xiàn)登錄認(rèn)證實(shí)戰(zhàn)教程

 更新時間:2024年06月19日 11:43:28   作者:Maiko Star  
這篇文章主要介紹了Spring Security實(shí)現(xiàn)登錄認(rèn)證實(shí)戰(zhàn)教程,本文通過示例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧

一、回顧認(rèn)證流程詳解

概念速查:

Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。

AuthenticationManager接口:定義了認(rèn)證Authentication的方法

UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的方法。

UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中

二、思路分析

2.1登錄

登錄流程如下:

2.1.1自定義登錄接口

調(diào)用ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過生成jwt

把用戶信息存入redis中

SpringSecurity在默認(rèn)的認(rèn)證過程中如果賬號密碼校驗(yàn)成功會返回Authentication對象之后UsernamePasswordAuthenticationFilter會將用戶信息Authentication存入SecurityContextHolder

但是我們在實(shí)際運(yùn)用場景中認(rèn)證通過后還需要向前端返回一個JSON格式的數(shù)據(jù)里面包括了JWT

所以此時我們需要寫一個自定義登錄接口

2.1.2.自定義UserDetailsService接口

在這個實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫

2.2校驗(yàn)

校驗(yàn)流程如下:

定義Jwt認(rèn)證過濾器

獲取token

解析token獲取其中的userid

從redis中獲取用戶信息

存入SecurityContextHolder

SpringSecurity 默認(rèn)是在內(nèi)存中查找對應(yīng)的用戶名密碼然后封裝成UserDetail對象交給DaoAuthenticationProcider校驗(yàn)

但是我們在實(shí)際運(yùn)用場景中是從數(shù)據(jù)庫中查找用戶信息

所以此時我們需要寫一個UserDetailsService的實(shí)現(xiàn)類用來在數(shù)據(jù)庫中查詢用戶信息并且封裝到UserDetail對象

三、準(zhǔn)備工作

3.1添加依賴(pom.xml)

        <!-- Spring Boot 安全功能的starter包,用于web應(yīng)用的安全控制 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Spring Boot Web功能的starter包,提供web應(yīng)用的基本功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Lombok,提供簡單的代碼生成工具,減少樣板代碼,設(shè)置為可選依賴 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Spring Boot的測試starter包,用于單元測試和集成測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Security的測試包,用于安全測試 -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Redis的starter包,用于集成Redis作為緩存或持久化方案 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- FastJSON,一個Java語言編寫的高性能功能完備的JSON庫 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <!-- JWT(JSON Web Token)的庫,用于生成和解析JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!-- JAXB API,用于XML和Java對象之間的綁定 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <!-- MyBatis Plus的Spring Boot starter,用于簡化MyBatis的使用 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
        <!-- MySQL連接器,用于連接和操作MySQL數(shù)據(jù)庫 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>
        <!-- Spring Boot的測試starter包,重復(fù)項(xiàng),可能用于不同目的 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

3.2添加Redis相關(guān)配置(com.sangeng.utils | com.sangeng.config)

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);
    }
}

自定義redis的序列化方式

@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;
    }
}

3.3響應(yīng)類(com.sangeng.domain)

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
    /**
     * 狀態(tài)碼
     */
    private Integer code;
    /**
     * 提示信息,如果有錯誤時,前端可以獲取該字段進(jìn)行提示
     */
    private String msg;
    /**
     * 查詢到的結(jié)果數(shù)據(jù),
     */
    private T data;
    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

3.4工具(com.sangeng.utils)

/**
 * JWT工具類
 */
public class JwtUtil {
    //有效期為
    public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000 一個小時
    //設(shè)置秘鑰明文
    public static final String JWT_KEY = "sangeng";
    public static String getUUID() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    /**
     * 生成jtw
     *
     * @param subject token中要存放的數(shù)據(jù)(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 設(shè)置過期時間
        return builder.compact();
    }
    /**
     * 生成jtw
     *
     * @param subject   token中要存放的數(shù)據(jù)(json格式)
     * @param ttlMillis token超時時間
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設(shè)置過期時間
        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數(shù)據(jù)
                .setIssuer("sg") // 簽發(fā)者
                .setIssuedAt(now) // 簽發(fā)時間
                .signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個參數(shù)為秘鑰
                .setExpiration(expDate);
    }
    /**
     * 創(chuàng)建token
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 設(shè)置過期時間
        return builder.compact();
    }
    public static void main(String[] args) throws Exception {
        String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg ";
        Claims claims = parseJWT(token);
        System.out.println(claims);
    }
    /**
     * 生成加密后的秘鑰 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();
    }
}
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {
    @Autowired
    public RedisTemplate redisTemplate;
    /**
     * 緩存基本的對象,Integer、String、實(shí)體類等
     *
     * @param key   緩存的鍵值
     * @param value 緩存的值
     */
    public <T> void setCacheObject(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }
    /**
     * 緩存基本的對象,Integer、String、實(shí)體類等
     *
     * @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);
    }
    /**
     * 設(shè)置有效時間
     *
     * @param key     Redis鍵
     * @param timeout 超時時間
     * @return true=設(shè)置成功;false=設(shè)置失敗
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }
    /**
     * 設(shè)置有效時間
     *
     * @param key     Redis鍵
     * @param timeout 超時時間
     * @param unit    時間單位
     * @return true=設(shè)置成功;false=設(shè)置失敗
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }
    /**
     * 獲得緩存的基本對象。
     *
     * @param key 緩存鍵值
     * @return 緩存鍵值對應(yīng)的數(shù)據(jù)
     */
    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數(shù)據(jù)
     *
     * @param key      緩存的鍵值
     * @param dataList 待緩存的List數(shù)據(jù)
     * @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 緩存鍵值對應(yīng)的數(shù)據(jù)
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }
    /**
     * 緩存Set
     *
     * @param key     緩存鍵值
     * @param dataSet 緩存的數(shù)據(jù)
     * @return 緩存數(shù)據(jù)的對象
     */
    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中存入數(shù)據(jù)
     *
     * @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中的數(shù)據(jù)
     *
     * @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中的數(shù)據(jù)
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }
    /**
     * 獲取多個Hash中的數(shù)據(jù)
     *
     * @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);
    }
}
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;
    }
}
public class RedisUtils {
    // 啟動Redis服務(wù)器
    public static void startRedisServer() {
        try {
            Process process = Runtime.getRuntime().exec("C:\\develop1\\Redis-x64-3.2.100\\redis-server.exe C:\\develop1\\Redis-x64-3.2.100\\redis.windows.conf");
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // 登錄到Redis服務(wù)器
    public static void loginRedisCli(String host, int port, String password) {
        try {
            String command = "redis-cli.exe -h " + host + " -p " + port + " -a " + password;
            Process process = Runtime.getRuntime().exec(command);
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        // 啟動Redis服務(wù)器
        startRedisServer();
        // 登錄到Redis服務(wù)器
        loginRedisCli("localhost", 6379, "123456");
    }
}

3.5實(shí)體類

 
/**
 * <p>
 * 用戶表
 * </p>
 *
 * @author 哈納桑
 * @since 2024-05-07
 */
@TableName("sys_user")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主鍵
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 用戶名
     */
    private String userName;
    /**
     * 昵稱
     */
    private String nickName;
    /**
     * 密碼
     */
    private String password;
    /**
     * 用戶類型:0代表普通用戶,1代表管理員
     */
    private String type;
    /**
     * 賬號狀態(tài)(0正常 1停用)
     */
    private String status;
    /**
     * 郵箱
     */
    private String email;
    /**
     * 手機(jī)號
     */
    private String phonenumber;
    /**
     * 用戶性別(0男,1女,2未知)
     */
    private String sex;
    /**
     * 頭像
     */
    private String avatar;
    /**
     * 創(chuàng)建人的用戶id
     */
    private Long createBy;
    /**
     * 創(chuàng)建時間
     */
    private LocalDateTime createTime;
    /**
     * 更新人
     */
    private Long updateBy;
    /**
     * 更新時間
     */
    private LocalDateTime updateTime;
    /**
     * 刪除標(biāo)志(0代表未刪除,1代表已刪除)
     */
    private Integer delFlag;
}

3.6項(xiàng)目結(jié)構(gòu)

四、實(shí)戰(zhàn)

4.1數(shù)據(jù)庫校驗(yàn)用戶(有基礎(chǔ)的可跳過)

從之前的分析我們可以知道,我們可以自定義一個UserDetailsService,讓SpringSecurity使用我們的UserDetailsService。我們自己的UserDetailsService可以從數(shù)據(jù)庫中查詢用戶名和密碼。

4.1.1準(zhǔn)備工作

數(shù)據(jù)庫表, 建表語句如下

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 '密碼',
  `type` char(1) DEFAULT '0' COMMENT '用戶類型:0代表普通用戶,1代表管理員',
  `status` char(1) DEFAULT '0' COMMENT '賬號狀態(tài)(0正常 1停用)',
  `email` varchar(64) DEFAULT NULL COMMENT '郵箱',
  `phonenumber` varchar(32) DEFAULT NULL COMMENT '手機(jī)號',
  `sex` char(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)',
  `avatar` varchar(128) DEFAULT NULL COMMENT '頭像',
  `create_by` bigint DEFAULT NULL COMMENT '創(chuàng)建人的用戶id',
  `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時間',
  `update_by` bigint DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新時間',
  `del_flag` int DEFAULT '0' COMMENT '刪除標(biāo)志(0代表未刪除,1代表已刪除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14787164048663 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶表'

4.1.2引入MybatisPuls和mysql驅(qū)動的依賴(前面已經(jīng)引入過了)

        <!-- MyBatis Plus的Spring Boot starter,用于簡化MyBatis的使用 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
        <!-- MySQL連接器,用于連接和操作MySQL數(shù)據(jù)庫 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

4.1.3配置數(shù)據(jù)庫信息

spring:
  application:
    name: SecurityTest
  datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/sg_blog?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: qq1664546939
  data:
      redis:
        host: localhost
        port: 6379
        password: 123456
        database: 10

4.1.4定義Mapper接口(com.sangeng.mapper)

public interface UserMapper extends BaseMapper<User> {}

4.1.5配置Mapper掃描(com.sangeng)

@SpringBootApplication
@MapperScan("com.example.securitytest.mapper")//掃描mapper
public class SecurityTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityTestApplication.class, args);
    }
}

4.1.6測試MP是否能正常使用

package com.example.securitytest;
import com.example.securitytest.domain.User;
import com.example.securitytest.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class SecurityTestApplicationTests {
    @Autowired
    UserMapper userMapper;
    @Test
    void contextLoads() {
        List<User> users = userMapper.selectList(null);
        System.out.println(users);
    }
}

4.2核心代碼(必看)

分析:

SpringSecurity 默認(rèn)是在內(nèi)存中查找對應(yīng)的用戶名密碼然后UserDetailsService的默認(rèn)實(shí)現(xiàn)類使用封裝成UserDetail對象交給DaoAuthenticationProcider校驗(yàn)

但是我們在實(shí)際運(yùn)用場景中是從數(shù)據(jù)庫中查找用戶信息

所以此時我們需要寫一個UserDetailsService的實(shí)現(xiàn)類用來在數(shù)據(jù)庫中查詢用戶信息并且封裝到UserDetail對象中

并且需要寫一個UserDetai的實(shí)現(xiàn)類因?yàn)橛脩粜畔⒉粌H僅只有用戶名和密碼還有其他信息

4.2.1創(chuàng)建UserDetailsService實(shí)現(xiàn)類

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根據(jù)用戶名查詢用戶信息
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);
        //如果沒有該用戶就拋出異常
        if (Objects.isNull(user)) {
            throw new RuntimeException("用戶名或密碼錯誤");
        }
        //TODO: 查詢權(quán)限信息封裝到LoginUser中
        // 將用戶信息封裝到UserDetails實(shí)現(xiàn)類中
        return new LoginUser(user);
    }
}

4.2.2創(chuàng)建UserDetail實(shí)現(xiàn)類

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private User user;//封裝用戶信息
    //獲取權(quán)限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
    //獲取密碼
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    //獲取用戶名
    @Override
    public String getUsername() {
        return user.getUserName();
    }
    //賬戶是否未過期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    //賬戶是否未鎖定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    //密碼是否未過期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    //賬戶是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

注意:如果要測試,需要往用戶表中寫入用戶數(shù)據(jù),并且如果你想讓用戶的密碼是明文存儲,需要在密碼前加{noop}。例如:

這樣登陸的時候就可以用libai作為用戶名,123456作為密碼來登陸了。

4.2.3密碼加密存儲模式更改

實(shí)際項(xiàng)目中我們不會把密碼明文存儲在數(shù)據(jù)庫中。

默認(rèn)使用的PasswordEncoder要求數(shù)據(jù)庫中的密碼格式為:{id}password 。它會根據(jù)id去判斷密碼的加密方式。

但是我們一般不會采用這種方式。所以就需要替換PasswordEncoder。

我們一般使用SpringSecurity為我們提供的BCryptPasswordEncoder。

我們只需要使用把BCryptPasswordEncoder對象注入Spring容器中,SpringSecurity就會使用該P(yáng)asswordEncoder來進(jìn)行密碼驗(yàn)。

我們可以定義一個SpringSecurity的配置類,SpringSecurity要求這個配置類要繼承

WebSecurityConfigurerAdapter。

創(chuàng)建SpringSecurity配置類

@Configuration //配置類
@EnableWebSecurity // 開啟Spring Security的功能 代替了 implements WebSecurityConfigurerAdapter
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4.2.4登陸接口

接下我們需要自定義登陸接口,然后讓SpringSecurity對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。

在接口中我們通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

認(rèn)證成功的話要生成一個jwt,放入響應(yīng)中返回。并且為了讓用戶下回請求時能通過jwt識別出具體的是

哪個用戶,我們需要把用戶信息存入redis,可以把用戶id作為key

(com.sangeng.controller) :

@RestController
public class LoginController {
    @Autowired
    private LoginServcie loginServcie;
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        return loginServcie.login(user);
    }
}

(com.sangeng.service):

public interface LoginServcie {
    ResponseResult login(User user);
}

(com.sangeng.service.impl):

@Service
public class LoginServiceImpl implements LoginServcie {
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    RedisCache redisCache;
    @Override
    public ResponseResult login(User user) {
        //1.封裝Authentication對象
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        //2.通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證
        Authentication authenticated = authenticationManager.authenticate(authenticationToken);
        //3.在Authentication中獲取用戶信息
        LoginUser loginUser = (LoginUser) authenticated.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        //4.認(rèn)證通過生成token
        String jwt = JwtUtil.createJWT(userId);
        //5.用戶信息存入redis
        redisCache.setCacheObject("login:" + userId, loginUser);
        //6.把token返回給前端
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("token", jwt);
        return new ResponseResult(200, "登錄成功", hashMap);
    }
}

(com.sangeng.config):

@Configuration //配置類
@EnableWebSecurity // 開啟Spring Security的功能 代替了 implements WebSecurityConfigurerAdapter
public class SecurityConfig {
    @Autowired
    AuthenticationConfiguration authenticationConfiguration;//獲取AuthenticationManager
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    /**
     * 配置Spring Security的過濾鏈。
     *
     * @param http 用于構(gòu)建安全配置的HttpSecurity對象。
     * @return 返回配置好的SecurityFilterChain對象。
     * @throws Exception 如果配置過程中發(fā)生錯誤,則拋出異常。
     */
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 禁用CSRF保護(hù)
                .csrf(csrf -> csrf.disable())
                // 設(shè)置會話創(chuàng)建策略為無狀態(tài)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置授權(quán)規(guī)則   指定user/login路徑.允許匿名訪問(未登錄可訪問已登陸不能訪問). 其他路徑需要身份認(rèn)證
                .authorizeHttpRequests(auth -> auth.requestMatchers("/user/login").anonymous().anyRequest().authenticated())
                //開啟跨域訪問
                .cors(AbstractHttpConfigurer::disable);
        // 構(gòu)建并返回安全過濾鏈
        return http.build();
    }
}

測試

4.2.5周氏總結(jié):(如何使用Spring Security實(shí)現(xiàn)用戶登錄認(rèn)證)

①編寫實(shí)現(xiàn)類去實(shí)現(xiàn)UserDetailsService接口,然后重寫里面的loadUserByUsername()方法,用來用來在數(shù)據(jù)庫中查詢用戶信息并且封裝到UserDetail對象中。其中UserDetail是個接口,我們需要編寫相應(yīng)的實(shí)體類去實(shí)現(xiàn)這個接口。詳細(xì)內(nèi)容如4.2.1、4.2.2

②更改密碼加密存儲模式,這時我們就可以使用Spring Security默認(rèn)提供的登錄接口localhost:8080/login來嘗試登陸。詳細(xì)內(nèi)容如:4.2.3

③如何編寫登陸接口?

首先編寫對應(yīng)的controller,然后調(diào)用對應(yīng)的service。由于Spring Security默認(rèn)會攔截所有路徑,所以接下來配置登錄路徑放行的配置,在配置類中創(chuàng)建參數(shù)為HttpSecurity http,返回值為SecurityFilterChain的方法or重寫參數(shù)為HttpSecurity http的configure()方法(推薦前者),并完成登錄路徑放行的配置。再者,創(chuàng)建AuthenticationManager對象。

創(chuàng)建AuthenticationManager對象方法1:

@Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

創(chuàng)建AuthenticationManager對象方法2:

@Bean
    protected AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

在完成配置后,創(chuàng)建UsernamePasswordAuthenticationToken()對象,其有兩個參數(shù)分別為用戶名和密碼。接下來注入AuthenticationManager對象

    @Autowired
    private AuthenticationManager authenticationManager;

然后通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,參數(shù)為UsernamePasswordAuthenticationToken()對象,該方法通過一系列調(diào)用,將會調(diào)用實(shí)現(xiàn)UserDetailsService接口的實(shí)現(xiàn)類中的loadUserByUsername()方法。authenticationManager.authenticate返回值為Authentication,其中包括getPrincipal()方法,該方法返回的對象正是實(shí)現(xiàn)UserDetailsService接口的實(shí)現(xiàn)類中的loadUserByUsername()方法的返回值,我們可以對其進(jìn)行強(qiáng)轉(zhuǎn)來獲取從數(shù)據(jù)庫中查詢的信息。

4.2.6認(rèn)證過濾器

我們需要自定義一個過濾器,這個過濾器會去獲取請求頭中的token,對token進(jìn)行解析取出其中的userid。(主要作用于除登錄外的請求)

使用userid去redis中獲取對應(yīng)的LoginUser對象。

然后封裝Authentication對象存入SecurityContextHolder

@Component
//OncePerRequestFilter特點(diǎn)是在處理單個HTTP請求時確保過濾器的 doFilterInternal 方法只被調(diào)用一次
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //1.在請求頭中獲取token
        String token = request.getHeader("token");
        //此處需要判斷token是否為空
        if (!StringUtils.hasText(token)){
            //沒有token放行 此時的SecurityContextHolder沒有用戶信息 會被后面的過濾器攔截
            filterChain.doFilter(request,response);
            return;
        }
        //2.解析token獲取用戶id
        String subject;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            subject = claims.getSubject();
        } catch (Exception e) {
            //解析失敗
            throw new RuntimeException("token非法");
        }
        //3.在redis中獲取用戶信息 注意:redis中的key是login:+userId
        String redisKey = "login:" + subject;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        //此處需要判斷l(xiāng)oginUser是否為空
        if (Objects.isNull(loginUser)){
            throw new RuntimeException("用戶未登錄");
        }
        //4.將獲取到的用戶信息存入SecurityContextHolder 參數(shù)(用戶信息,,權(quán)限信息)
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //5.放行
        filterChain.doFilter(request,response);
    }
}

把token校驗(yàn)過濾器添加到過濾器鏈中

關(guān)鍵代碼:

.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

@Configuration //配置類
@EnableWebSecurity // 開啟Spring Security的功能 代替了 implements WebSecurityConfigurerAdapter
public class SecurityConfig {
    @Autowired
    AuthenticationConfiguration authenticationConfiguration;//獲取AuthenticationManager
    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    /**
     * 配置Spring Security的過濾鏈。
     *
     * @param http 用于構(gòu)建安全配置的HttpSecurity對象。
     * @return 返回配置好的SecurityFilterChain對象。
     * @throws Exception 如果配置過程中發(fā)生錯誤,則拋出異常。
     */
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 禁用CSRF保護(hù)
                .csrf(csrf -> csrf.disable())
                // 設(shè)置會話創(chuàng)建策略為無狀態(tài)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置授權(quán)規(guī)則                 指定user/login路徑.允許匿名訪問(未登錄可訪問已登陸不能訪問). 其他路徑需要身份認(rèn)證
                .authorizeHttpRequests(auth -> auth.requestMatchers("/user/login").anonymous().anyRequest().authenticated())
                //開啟跨域訪問
                .cors(AbstractHttpConfigurer::disable)
                // 添加JWT認(rèn)證過濾器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 構(gòu)建并返回安全過濾鏈
        return http.build();
    }
}

4.2.7退出登錄

我們只需要定義一個登陸接口,然后獲取SecurityContextHolder中的認(rèn)證信息,刪除redis中對應(yīng)的數(shù)據(jù)即可。

(com.sangeng.service.impl.LoginServiceImpl)

  @Override
    public ResponseResult logout() {
        //獲取SecurityContextHolder中的用戶id
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        //刪除redis中的用戶信息
        redisCache.deleteObject("login:" + userId);
        return new ResponseResult(200, "退出成功");
    }

(com.sangeng.controller.LoginController)

    @PostMapping("/user/logout")
    public ResponseResult logout(){
        System.out.println("開始登出");
        return loginServcie.logout();
    }

4.2.8自定義失敗處理器

我們還希望在認(rèn)證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對響應(yīng)進(jìn)行統(tǒng)一的處理。要實(shí)現(xiàn)這個功能我們需要知道SpringSecurity的異常處理機(jī)制。

在SpringSecurity中,如果我們在認(rèn)證或者授權(quán)的過程中出現(xiàn)了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。

如果是認(rèn)證過程中出現(xiàn)的異常會被封裝成AuthenticationException然后調(diào)用AuthenticationEntryPoint對象的方法去進(jìn)行異常處理。

如果是授權(quán)過程中出現(xiàn)的異常會被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對象的方法去進(jìn)行異常處理。

所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置SpringSecurity即可。

(com.sangeng.handler)

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException,
            ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),
                "權(quán)限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response, json);
    }
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse
            response, AuthenticationException authException) throws IOException,
            ServletException {
        ResponseResult result = new
                ResponseResult(HttpStatus.UNAUTHORIZED.value(), "認(rèn)證失敗請重新登錄");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response, json);
    }
}

修改配置類

@Configuration //配置類
@EnableWebSecurity // 開啟Spring Security的功能 代替了 implements WebSecurityConfigurerAdapter
public class SecurityConfig {
    @Autowired
    AuthenticationConfiguration authenticationConfiguration;//獲取AuthenticationManager
    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    AccessDeniedHandlerImpl accessDeniedHandler;
    @Autowired
    AuthenticationEntryPointImpl authenticationEntryPoint;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    /**
     * 配置Spring Security的過濾鏈。
     *
     * @param http 用于構(gòu)建安全配置的HttpSecurity對象。
     * @return 返回配置好的SecurityFilterChain對象。
     * @throws Exception 如果配置過程中發(fā)生錯誤,則拋出異常。
     */
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 禁用CSRF保護(hù)
                .csrf(csrf -> csrf.disable())
                // 設(shè)置會話創(chuàng)建策略為無狀態(tài)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置授權(quán)規(guī)則                 指定user/login路徑.允許匿名訪問(未登錄可訪問已登陸不能訪問). 其他路徑需要身份認(rèn)證
                .authorizeHttpRequests(auth -> auth.requestMatchers("/user/login").anonymous().anyRequest().authenticated())
                //開啟跨域訪問
                .cors(AbstractHttpConfigurer::disable)
                // 添加JWT認(rèn)證過濾器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 配置異常處理
                .exceptionHandling(exception -> exception.accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint));
        // 構(gòu)建并返回安全過濾鏈
        return http.build();
    }

測試

正常登錄

訪問接口

退出登錄

再次訪問接口

到此這篇關(guān)于Spring Security如何實(shí)現(xiàn)登錄認(rèn)證的文章就介紹到這了,更多相關(guān)Spring Security登錄認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • FeignMultipartSupportConfig上傳圖片配置方式

    FeignMultipartSupportConfig上傳圖片配置方式

    這篇文章主要介紹了FeignMultipartSupportConfig上傳圖片配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • maven解決包沖突方法詳解

    maven解決包沖突方法詳解

    這篇文章主要介紹了maven解決包沖突方法詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-10-10
  • Netty分布式pipeline管道Handler的刪除邏輯操作

    Netty分布式pipeline管道Handler的刪除邏輯操作

    這篇文章主要為大家介紹了Netty分布式pipeline管道Handler的刪除邏輯操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-03-03
  • Java卡片布局管理器解釋及實(shí)例

    Java卡片布局管理器解釋及實(shí)例

    這篇文章主要介紹了Java卡片布局管理器解釋及實(shí)例,需要的朋友可以參考下。
    2017-09-09
  • Java構(gòu)建菜單樹的實(shí)現(xiàn)示例

    Java構(gòu)建菜單樹的實(shí)現(xiàn)示例

    本文主要介紹了Java構(gòu)建菜單樹的實(shí)現(xiàn)示例,像一級菜單,二級菜單,三級菜單甚至更多層級的菜單,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-05-05
  • Java實(shí)現(xiàn)將方法作為參數(shù)傳遞的方法小結(jié)

    Java實(shí)現(xiàn)將方法作為參數(shù)傳遞的方法小結(jié)

    在Java編程中,將方法作為參數(shù)傳遞是一種強(qiáng)大的技術(shù),可以提高代碼的靈活性和可重用性,本文將探討幾種在Java中實(shí)現(xiàn)這一目標(biāo)的方法,需要的朋友可以參考下
    2025-03-03
  • 五種Java多線程同步的方法

    五種Java多線程同步的方法

    這篇文章主要為大家詳細(xì)介紹了五種Java多線程同步的方法,需要的朋友可以參考下
    2015-09-09
  • 深入淺出Java中重試機(jī)制的多種方式

    深入淺出Java中重試機(jī)制的多種方式

    重試機(jī)制在分布式系統(tǒng)中,或者調(diào)用外部接口中,都是十分重要的。重試機(jī)制可以保護(hù)系統(tǒng)減少因網(wǎng)絡(luò)波動、依賴服務(wù)短暫性不可用帶來的影響,讓系統(tǒng)能更穩(wěn)定的運(yùn)行的一種保護(hù)機(jī)制。本文就來和大家聊聊Java中重試機(jī)制的多種方式
    2023-03-03
  • java讀取文件內(nèi)容的三種方法代碼片斷分享(java文件操作)

    java讀取文件內(nèi)容的三種方法代碼片斷分享(java文件操作)

    本文介紹java讀取文件內(nèi)容的三種方法,代碼可以直接放到程序中使用,大家參考使用吧
    2014-01-01
  • SpringBoot實(shí)現(xiàn)本地存儲文件上傳及提供HTTP訪問服務(wù)的方法

    SpringBoot實(shí)現(xiàn)本地存儲文件上傳及提供HTTP訪問服務(wù)的方法

    這篇文章主要介紹了SpringBoot實(shí)現(xiàn)本地存儲文件上傳及提供HTTP訪問服務(wù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-08-08

最新評論