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ù)的核心接口。里面定義了一個根據(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)的用戶名密碼然后封裝成UserDetai
l對象交給DaoAuthenticationProcider
校驗(yàn)但是我們在實(shí)際運(yùn)用場景中是從數(shù)據(jù)庫中查找用戶信息
所以此時我們需要寫一個
UserDetailsService
的實(shí)現(xiàn)類用來在數(shù)據(jù)庫中查詢用戶信息并且封裝到UserDetai
l對象
三、準(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)類使用封裝成UserDetai
l對象交給DaoAuthenticationProcider
校驗(yàn)但是我們在實(shí)際運(yùn)用場景中是從數(shù)據(jù)庫中查找用戶信息
所以此時我們需要寫一個
UserDetailsService
的實(shí)現(xiàn)類用來在數(shù)據(jù)庫中查詢用戶信息并且封裝到UserDetai
l對象中并且需要寫一個
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ù)庫中查詢用戶信息并且封裝到UserDetai
l對象中。其中UserDetai
l是個接口,我們需要編寫相應(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)文章希望大家以后多多支持腳本之家!
- 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上傳圖片配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03Netty分布式pipeline管道Handler的刪除邏輯操作
這篇文章主要為大家介紹了Netty分布式pipeline管道Handler的刪除邏輯操作,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03Java構(gòu)建菜單樹的實(shí)現(xiàn)示例
本文主要介紹了Java構(gòu)建菜單樹的實(shí)現(xiàn)示例,像一級菜單,二級菜單,三級菜單甚至更多層級的菜單,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05Java實(shí)現(xiàn)將方法作為參數(shù)傳遞的方法小結(jié)
在Java編程中,將方法作為參數(shù)傳遞是一種強(qiáng)大的技術(shù),可以提高代碼的靈活性和可重用性,本文將探討幾種在Java中實(shí)現(xiàn)這一目標(biāo)的方法,需要的朋友可以參考下2025-03-03java讀取文件內(nèi)容的三種方法代碼片斷分享(java文件操作)
本文介紹java讀取文件內(nèi)容的三種方法,代碼可以直接放到程序中使用,大家參考使用吧2014-01-01SpringBoot實(shí)現(xiàn)本地存儲文件上傳及提供HTTP訪問服務(wù)的方法
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)本地存儲文件上傳及提供HTTP訪問服務(wù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08