Spring Security實(shí)現(xiàn)登錄認(rèn)證實(shí)戰(zhàn)教程
一、回顧認(rèn)證流程詳解

概念速查:
Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
AuthenticationManager接口:定義了認(rèn)證Authentication的方法
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(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)成功會(huì)返回Authentication對象之后UsernamePasswordAuthenticationFilter會(huì)將用戶信息Authentication存入SecurityContextHolder中但是我們在實(shí)際運(yùn)用場景中認(rèn)證通過后還需要向前端返回一個(gè)JSON格式的數(shù)據(jù)里面包括了JWT
所以此時(shí)我們需要寫一個(gè)自定義登錄接口
2.1.2.自定義UserDetailsService接口
在這個(gè)實(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ù)庫中查找用戶信息
所以此時(shí)我們需要寫一個(gè)
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,一個(gè)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;
/**
* 提示信息,如果有錯(cuò)誤時(shí),前端可以獲取該字段進(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 一個(gè)小時(shí)
//設(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è)置過期時(shí)間
return builder.compact();
}
/**
* 生成jtw
*
* @param subject token中要存放的數(shù)據(jù)(json格式)
* @param ttlMillis token超時(shí)時(shí)間
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設(shè)置過期時(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ā)時(shí)間
.signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個(gè)參數(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è)置過期時(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 時(shí)間
* @param timeUnit 時(shí)間顆粒度
*/
public <T> void setCacheObject(final String key, final T value, final
Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 設(shè)置有效時(shí)間
*
* @param key Redis鍵
* @param timeout 超時(shí)時(shí)間
* @return true=設(shè)置成功;false=設(shè)置失敗
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 設(shè)置有效時(shí)間
*
* @param key Redis鍵
* @param timeout 超時(shí)時(shí)間
* @param unit 時(shí)間單位
* @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);
}
/**
* 刪除單個(gè)對象
*
* @param key
*/
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}
/**
* 刪除集合對象
*
* @param collection 多個(gè)對象
* @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);
}
/**
* 獲取多個(gè)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 {
// 啟動(dòng)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) {
// 啟動(dòng)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)建時(shí)間
*/
private LocalDateTime createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新時(shí)間
*/
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ǔ)的可跳過)
從之前的分析我們可以知道,我們可以自定義一個(gè)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)建時(shí)間', `update_by` bigint DEFAULT NULL COMMENT '更新人', `update_time` datetime DEFAULT NULL COMMENT '更新時(shí)間', `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ū)動(dòng)的依賴(前面已經(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: 104.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ù)庫中查找用戶信息
所以此時(shí)我們需要寫一個(gè)
UserDetailsService的實(shí)現(xiàn)類用來在數(shù)據(jù)庫中查詢用戶信息并且封裝到UserDetail對象中并且需要寫一個(gè)
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("用戶名或密碼錯(cuò)誤");
}
//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ù),并且如果你想讓用戶的密碼是明文存儲(chǔ),需要在密碼前加{noop}。例如:

這樣登陸的時(shí)候就可以用libai作為用戶名,123456作為密碼來登陸了。
4.2.3密碼加密存儲(chǔ)模式更改
實(shí)際項(xiàng)目中我們不會(huì)把密碼明文存儲(chǔ)在數(shù)據(jù)庫中。
默認(rèn)使用的PasswordEncoder要求數(shù)據(jù)庫中的密碼格式為:{id}password 。它會(huì)根據(jù)id去判斷密碼的加密方式。
但是我們一般不會(huì)采用這種方式。所以就需要替換PasswordEncoder。
我們一般使用SpringSecurity為我們提供的BCryptPasswordEncoder。
我們只需要使用把BCryptPasswordEncoder對象注入Spring容器中,SpringSecurity就會(huì)使用該P(yáng)asswordEncoder來進(jìn)行密碼驗(yàn)。
我們可以定義一個(gè)SpringSecurity的配置類,SpringSecurity要求這個(gè)配置類要繼承
WebSecurityConfigurerAdapter。
創(chuàng)建SpringSecurity配置類
@Configuration //配置類
@EnableWebSecurity // 開啟Spring Security的功能 代替了 implements WebSecurityConfigurerAdapter
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}4.2.4登陸接口
接下我們需要自定義登陸接口,然后讓SpringSecurity對這個(gè)接口放行,讓用戶訪問這個(gè)接口的時(shí)候不用登錄也能訪問。
在接口中我們通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
認(rèn)證成功的話要生成一個(gè)jwt,放入響應(yīng)中返回。并且為了讓用戶下回請求時(shí)能通過jwt識(shí)別出具體的是
哪個(gè)用戶,我們需要把用戶信息存入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ā)生錯(cuò)誤,則拋出異常。
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF保護(hù)
.csrf(csrf -> csrf.disable())
// 設(shè)置會(huì)話創(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是個(gè)接口,我們需要編寫相應(yīng)的實(shí)體類去實(shí)現(xiàn)這個(gè)接口。詳細(xì)內(nèi)容如4.2.1、4.2.2
②更改密碼加密存儲(chǔ)模式,這時(shí)我們就可以使用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)會(huì)攔截所有路徑,所以接下來配置登錄路徑放行的配置,在配置類中創(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()對象,其有兩個(gè)參數(shù)分別為用戶名和密碼。接下來注入AuthenticationManager對象
@Autowired
private AuthenticationManager authenticationManager;然后通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,參數(shù)為UsernamePasswordAuthenticationToken()對象,該方法通過一系列調(diào)用,將會(huì)調(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)證過濾器
我們需要自定義一個(gè)過濾器,這個(gè)過濾器會(huì)去獲取請求頭中的token,對token進(jìn)行解析取出其中的userid。(主要作用于除登錄外的請求)
使用userid去redis中獲取對應(yīng)的LoginUser對象。
然后封裝Authentication對象存入SecurityContextHolder
@Component
//OncePerRequestFilter特點(diǎn)是在處理單個(gè)HTTP請求時(shí)確保過濾器的 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放行 此時(shí)的SecurityContextHolder沒有用戶信息 會(huì)被后面的過濾器攔截
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ā)生錯(cuò)誤,則拋出異常。
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF保護(hù)
.csrf(csrf -> csrf.disable())
// 設(shè)置會(huì)話創(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退出登錄
我們只需要定義一個(gè)登陸接口,然后獲取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)這個(gè)功能我們需要知道SpringSecurity的異常處理機(jī)制。
在SpringSecurity中,如果我們在認(rèn)證或者授權(quán)的過程中出現(xiàn)了異常會(huì)被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會(huì)去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。
如果是認(rèn)證過程中出現(xiàn)的異常會(huì)被封裝成AuthenticationException然后調(diào)用AuthenticationEntryPoint對象的方法去進(jìn)行異常處理。
如果是授權(quán)過程中出現(xiàn)的異常會(huì)被封裝成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ā)生錯(cuò)誤,則拋出異常。
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF保護(hù)
.csrf(csrf -> csrf.disable())
// 設(shè)置會(huì)話創(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)文章希望大家以后多多支持腳本之家!
- Spring Security 自定義短信登錄認(rèn)證的實(shí)現(xiàn)
- Springboot+Spring Security實(shí)現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的示例代碼
- Java SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證
- SpringBoot security安全認(rèn)證登錄的實(shí)現(xiàn)方法
- SpringSecurity實(shí)現(xiàn)前后端分離登錄token認(rèn)證詳解
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過程
- springsecurity實(shí)現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項(xiàng)目)
- SpringSecurity 自定義認(rèn)證登錄的項(xiàng)目實(shí)踐
- spring security登錄認(rèn)證授權(quán)的項(xiàng)目實(shí)踐
相關(guān)文章
FeignMultipartSupportConfig上傳圖片配置方式
這篇文章主要介紹了FeignMultipartSupportConfig上傳圖片配置方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
Netty分布式pipeline管道Handler的刪除邏輯操作
這篇文章主要為大家介紹了Netty分布式pipeline管道Handler的刪除邏輯操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
Java構(gòu)建菜單樹的實(shí)現(xiàn)示例
本文主要介紹了Java構(gòu)建菜單樹的實(shí)現(xiàn)示例,像一級菜單,二級菜單,三級菜單甚至更多層級的菜單,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
Java實(shí)現(xiàn)將方法作為參數(shù)傳遞的方法小結(jié)
在Java編程中,將方法作為參數(shù)傳遞是一種強(qiáng)大的技術(shù),可以提高代碼的靈活性和可重用性,本文將探討幾種在Java中實(shí)現(xiàn)這一目標(biāo)的方法,需要的朋友可以參考下2025-03-03
java讀取文件內(nèi)容的三種方法代碼片斷分享(java文件操作)
本文介紹java讀取文件內(nèi)容的三種方法,代碼可以直接放到程序中使用,大家參考使用吧2014-01-01
SpringBoot實(shí)現(xiàn)本地存儲(chǔ)文件上傳及提供HTTP訪問服務(wù)的方法
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)本地存儲(chǔ)文件上傳及提供HTTP訪問服務(wù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08

