SpringSecurity框架【認證】

目錄

一. 快速入門

二. 認證

2.1 登陸校驗流程

2.2 原理初探

2.3 解決問題

2.3.1 思路分析

2.3.2 準備工作

2.3.3 實現

2.3.3.1 數據庫校驗用戶

2.3.3.2 密碼加密存儲

2.3.3.3 登錄接口

2.3.3.4 認證過濾器

2.3.3.5 退出登錄


Spring Security是Spring家族中的一個安全管理框架,相比與另外一個安全框架Shiro,它提供了更豐富的功能,社區資源也比Shiro豐富。

一般來說大型項目用Spring Security比較多,小項目用Shiro比較多,因為相比于Spring Security,Shiro上手比較簡單。

一般Web應用需要進行認證授權

  • 認證:驗證當前訪問系統的是不是本系統用戶,并且要確認具體是哪個用戶
  • 授權:經過認證后判斷當前用戶是否有權限進行某個操作

而認證和授權正是Spring Security作為安全框架的核心功能!

一. 快速入門

我們先簡單構建出一個SpringBoot項目。

這個時候我們訪問我們寫的一個簡單的hello接口,驗證是否構建成功。

接著引入SpringSecurity。

這個時候我們再看看訪問接口的效果。

引入了SpringSecurity之后,訪問接口會自動跳轉到一個登錄頁面,默認的用戶名是user,密碼會輸出到控制臺,必須登錄后才能對接口進行訪問。

二. 認證

2.1 登陸校驗流程

首先我們要先了解登錄校驗流程,首先前端攜帶用戶名和密碼訪問登錄接口,服務器拿到這個用戶名和密碼之后去和數據庫中的進行比較,如果正確使用用戶名/用戶ID,生成一個jwt,接著把jwt響應給前端,之后登錄后訪問其他的請求都會在請求頭中攜帶token,服務器每次獲取請求頭中的token進行解析、獲取UserID,根據用戶名id獲取用戶相關信息,查看器權限,如果有權限則響應給前端。

2.2 原理初探

SpringSecurity的原理其實就是一個過濾器鏈,內部提供了各種功能的過濾器,這里我們先看看上方快速入門中涉及的過濾器。

  • UsernamePasswordAuthenticationFilter負責處理在登錄頁面填寫的用戶名密碼后的登錄請求
  • ExceptionTranslationFilter處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException
  • FilterSecurityInterceptor負責權限校驗的過濾器

我們也可以通過Debug查看當前系統中SpringSecurity過濾器鏈中有哪些過濾器以及順序。

接下來我們來看看認證流程圖的解析。

這里我們只需要能看懂其過程即可,簡單來說就是:

用戶提交了用戶名和密碼,UsernamePasswordAuthenticationFilter將其封裝未Authentication對象,并且調用authenticate方法進行認證,接著在調用DaoAuthenticationProvider的authenticate方法進行認證,再調用loadUserByUserName方法查詢用戶,這里的查詢是在內存中進行查找,然后將對應的用戶信息封裝未UserDetails對象,通過PasswordEncoder對比UserDetails中的密碼和Authentication的密碼是否正確,如果正確就把UserDetails中的權限信息設置到Authentication對象中,接著返回Authentication對象,最后使用SecurityContextHolder.getContext().setAuthentication方法存儲該對象,其他過濾器會通過SecurityContextHoder來獲取當前用戶信息。(這一段不用記憶能聽懂即可)

那么我們知道了其過程,才能對其進行修改,首先這里的從內存中查找,我們肯定是要該為從數據庫中查找(這里需要我們自定義一個UserDetailsService的實現類),并且也不會使用默認的用戶名密碼,登錄界面也一定是自己編寫的,不需要用他提供的默認登錄頁面。

基于我們分析的情況,可以得到這樣的一張圖。

這個時候就返回了一個jwt給前端,而這時前端進行的其他請求都會攜帶token,那么我們第一步就需要先校驗是否攜帶token,并且解析token,獲取對應的userid,并且將其封裝為Anthentication對象存入SecurityContextHolder(為了其他過濾器可以拿到)。

那么這里還有一個問題,從jwt認證過濾器中取到了userid后如何獲取完整的用戶信息?

這里我們使用redis,當服務器認證通過使用用戶id生成jwt給前端的時候,以用戶id作為key,用戶的信息作為value存入redis,之后就可以通過userid從redis中獲取到完整的用戶信息了。

2.3 解決問題

2.3.1 思路分析

從上述的原理初探中,我們也大概分析出了我們要是自己實現前后端分離的認證流程,需要做的事情。

登錄:

? ? ? ? a.自定義登錄接口

? ? ? ? ? ? ? ? 調用ProviderManager的方法進行認證,如果認證通過生成jwt

? ? ? ? ? ? ? ? 把用戶信息存入redis中

? ? ? ? b.自定義UserDetailsService

? ? ? ? ? ? ? ? 在這個實現類中去查詢數據庫

校驗:

? ? ? ? a.自定義jwt認證過濾器

? ? ? ? ? ? ? ? 獲取token

? ? ? ? ? ? ? ? 解析token獲取其中userid

? ? ? ? ? ? ? ? 從redis中獲取完整用戶信息

? ? ? ? ? ? ? ? 存入SecurityContextHolder

2.3.2 準備工作

首先需要添加對應的依賴

        <!--   SpringSecurity啟動器     --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!--   redis依賴     --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--   fastjson依賴     --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><!--   jwt依賴     --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>

接著我們需要用到Redis需要加入Redis相關的配置

首先是FastJson的序列化器

package org.example.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;import java.nio.charset.Charset;/*** Redis使用fastjson序列化* @param <T>*/
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;}@Overridepublic byte[] serialize(T t) throws SerializationException {if (t == null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic 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);}}

創建RedisConfig在其中創建序列化器,解決亂碼等問題

package org.example.config;
import org.example.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;@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來序列化和反序列化redus的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);template.afterPropertiesSet();return template;}
}

?還需要統一響應類

package org.example.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T>{/*** 狀態碼*/private Integer code;/*** 提示信息,如果有錯誤時,前端可以獲取該字段進行提示*/private String msg;/*** 查詢到的結果數據*/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;}
}

再需要jwt的工具類用于生成jwt,以及對jwt進行解析

package org.example.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;public class JwtUtil {//有效期為public static final Long JWT_TTL = 60*60*1000L; //一個小時//設置密鑰明文public static final String JWT_KEY = "hzj";public static String getUUID(){String token = UUID.randomUUID().toString().replaceAll("-","");return token;}/*** 生成jwt* @param subject token中要存放的數據(json格式)* @return*/public static String createJWT(String subject){JwtBuilder builder = getJwtBuilder(subject,null,getUUID()); //設置過期時間return builder.compact();}/*** 生成jwt* @param subject token中要存放的數據(json格式)* @param ttlMillis token超時時間* @return*/public static String createJWT(String subject,Long ttlMillis){JwtBuilder builder = getJwtBuilder(subject,ttlMillis,getUUID()); //設置過期時間return builder.compact();}private static JwtBuilder getJwtBuilder(String subject,Long ttlMillis,String uuid){SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalkey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid) //唯一的Id.setSubject(subject) //主題 可以是Json數據.setIssuer("hzj") //簽發者.setIssuedAt(now) //簽發時間.signWith(signatureAlgorithm,secretKey) //使用HS256對稱加密算法簽名,第二個參數為密鑰.setExpiration(expDate);}/*** 創建token* @param id* @param subject* @param ttlMillis* @return*/public static String createJWT(String id,String subject,Long ttlMillis){JwtBuilder builder = getJwtBuilder(subject,ttlMillis,id);//設置過期時間return builder.compact();}public static void main(String[] args) throws Exception{String token =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTg0MjU5MzIsInVzZX" +
"JJZCI6MTExLCJ1c2VybmFtZSI6Ik1hcmtaUVAifQ.PTlOdRG7ROVJqPrA0q2ac7rKFzNNFR3lTMyP_8fIw9Q";Claims claims = parseJWT(token);System.out.println(claims);}/*** 生成加密后的密鑰secretkey* @return*/public static SecretKey generalkey(){byte[] encodeedkey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodeedkey,0,encodeedkey.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();}
}

再定義一個Redis的工具類RedisCache,這樣可以使我們調用redistemplate更加簡單

package org.example.utils;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.*;
import java.util.concurrent.TimeUnit;@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{@Autowiredpublic RedisTemplate redisTemplate;/*** 緩存基本的對象,Integer、String、實體類等** @param key 緩存的鍵值* @param value 緩存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 緩存基本的對象,Integer、String、實體類等** @param key 緩存的鍵值* @param value 緩存的值* @param timeout 時間* @param timeUnit 時間顆粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 設置有效時間** @param key Redis鍵* @param timeout 超時時間* @return true=設置成功;false=設置失敗*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 設置有效時間** @param key Redis鍵* @param timeout 超時時間* @param unit 時間單位* @return true=設置成功;false=設置失敗*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 獲得緩存的基本對象。** @param key 緩存鍵值* @return 緩存鍵值對應的數據*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 刪除單個對象** @param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 刪除集合對象** @param collection 多個對象* @return*/public long deleteObject(final Collection collection){return redisTemplate.delete(collection);}/*** 緩存List數據** @param key 緩存的鍵值* @param dataList 待緩存的List數據* @return 緩存的對象*/public <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 獲得緩存的list對象** @param key 緩存的鍵值* @return 緩存鍵值對應的數據*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 緩存Set** @param key 緩存鍵值* @param dataSet 緩存的數據* @return 緩存數據的對象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 獲得緩存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 緩存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 獲得緩存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入數據** @param key Redis鍵* @param hKey Hash鍵* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 獲取Hash中的數據** @param key Redis鍵* @param hKey Hash鍵* @return Hash中的對象*/public <T> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 刪除Hash中的數據** @param key* @param hkey*/public void delCacheMapValue(final String key, final String hkey){HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hkey);}/*** 獲取多個Hash中的數據** @param key Redis鍵* @param hKeys Hash鍵集合* @return Hash對象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 獲得緩存的基本對象列表** @param pattern 字符串前綴* @return 對象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}
}

我們還有可能往響應中寫入數據,那么就還需要一個工具類WebUtils

package org.example.utils;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;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;}
}

最后寫對應的用戶實體類

package org.example.domain;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/*** 用戶表(User)實體類*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {private static final long serialVersionUID = -40356785423868312L;/*** 主鍵*/private Long id;/*** 用戶名*/private String userName;/*** 昵稱*/private String nickName;/*** 密碼*/private String password;/*** 賬號狀態(0正常 1停用)*/private String status;/*** 郵箱*/private String email;/*** 手機號*/private String phonenumber;/*** 用戶性別(0男,1女,2未知)*/private String sex;/*** 頭像*/private String avatar;/*** 用戶類型(0管理員,1普通用戶)*/private String userType;/*** 創建人的用戶id*/private Long createBy;/*** 創建時間*/private Date createTime;/*** 更新人*/private Long updateBy;/*** 更新時間*/private Date updateTime;/*** 刪除標志(0代表未刪除,1代表已刪除)*/private Integer delFlag;
}

?根據我們上方的分析我們是需要自定義一個UserDetailsService,讓SpringSecuriry使用我們的UserDetailsService。我們自己的UserDetailsService可以從數據庫中查詢用戶名和密碼。

我們先建立一個數據庫表sys_user。

CREATE TABLE `sys_user` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主鍵',`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用戶名',`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '呢稱',`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密碼',`status` char(1) DEFAULT '0' COMMENT '賬號狀態(0正常1停用)',`email` varchar(64) DEFAULT NULL COMMENT '郵箱',`phonenumber` varchar(32) DEFAULT NULL COMMENT '手機號',`sex` char(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)',`avatar` varchar(128) DEFAULT NULL COMMENT '頭像',`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用戶類型(O管理員,1普通用戶)',`create_by` bigint DEFAULT NULL COMMENT '創建人的用戶id',`create_time` datetime DEFAULT NULL COMMENT '創建時間',`update_by` bigint DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT NULL COMMENT '更新時間',`del_flag` int DEFAULT '0' COMMENT '刪除標志(O代表未刪除,1代表已刪除)',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶表';

接著引入myBatisPlus和mysql驅動。

        <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>

?接著配置數據庫的相關信息。

接著定義mapper接口UserMapper,使用mybatisplus加入對應的注解。

package org.example.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.domain.User;public interface UserMapper extends BaseMapper<User> {
}

接著配置組件掃描

最后測試一下mp能否正常使用。

引入junit

這樣就是可以正常使用了。

2.3.3 實現

2.3.3.1 數據庫校驗用戶

接下來我們需要進行核心代碼的實現。

?首先我們先進行自定義UserDetailsService。

package org.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.example.domain.LoginUser;
import org.example.domain.User;
import org.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//查詢用戶信息 [InMemoryUserDetailsManager是在內存中查找]LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUserName,username);User user = userMapper.selectOne(wrapper);//如果查詢不到數據就拋出異常,給出提示if(Objects.isNull(user)){throw new RuntimeException("用戶名或密碼錯誤!");}//TODO 查詢權限信息//封裝為UserDetails對象返回return new LoginUser(user);}
}

這里要將user封裝為UserDetails進行返回。

package org.example.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {private User user;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

最后這里有一個點,就是我們需要進行登錄從數據庫拿數據的測試,需要往表中寫入用戶數據,并且如果你想讓用戶的密碼是明文傳輸,需要在密碼前加上{noop}。

? ? 這里就實現了輸入數據庫中的用戶名密碼進行登錄了。

2.3.3.2 密碼加密存儲

這里說一下為什么要在密碼前面加上{noop},因為默認使用的PasswordEncoder要求數據庫中的密碼格式為{id}password,它會根據id去判斷密碼的加密方式,但是我們一般不會采取這種方式,所以就需要替換掉PasswordEncoder。

接下來我們進行測試看看。

可以看到我們這里傳入的兩次密碼原文是一樣的,但是卻得到了不同的結果,這里其實和加鹽算法有關,之后我還會寫一個自定義加密的文章。

得到加密之后的密碼之后就可以將加密后的密碼存入數據庫,之后可以由前端傳過來的明文密碼與數據庫中的加密后的密碼進行驗證進行登錄。

這個時候我們啟動項目去登錄,發現之前的密碼已經登不上了,因為數據庫此時存放的應該是注冊階段存入數據庫的加密后的密碼,而不是原文密碼了(因為沒注冊我將加密后的密碼自行寫入數據庫中)。

2.3.3.3 登錄接口

我們需要實現一個登錄接口,然后讓SpringSecuruty對其進行放行,如果不放行就自相矛盾了,在接口中通過AuthenticationManager的authenticate方法來進行用戶認證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

認證成功的話需要生成一個jwt,放入響應中,并且為了讓用戶下次請求時能通過jwt識別出具體是哪個用戶,需要把用戶信息存入redis,可以把用戶id作為key。

先寫LoginController

接著寫對應的Service。

在SecurityConfig中進行AuthenticationManager的注入,和登錄接口的放行。

在service中的業務邏輯中,如果認證失敗則返回一個自定義異常,但是如果認證成功我們需要如何獲取到對應的信息呢。

這里我們可以debug看看得到的對象。

這里發現是Principal中可以得到對應需要的信息。

接著補全代碼。

最后進行測試看看。

2.3.3.4 認證過濾器

我先將代碼貼上。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//獲取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {//放行filterChain.doFilter(request, response); //這里放行是因為還有后續的過濾器會給出對應的異常return; //token為空 不執行后續流程}//解析tokenString userid;try {Claims claims = JwtUtil.parseJWT(token);userid = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法!");}//從redis中獲取用戶信息String redisKey = "login:" + userid;LoginUser loginUser = redisCache.getCacheObject(redisKey);if (Objects.isNull(loginUser)){throw new RuntimeException("用戶未登錄!");}//將信息存入SecurityContextHolder(因為過濾器鏈后面的filter都是從中獲取認證信息進行對應放行)//TODO 獲取權限信息封裝到Authentication中UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request,response); //此時的放行是攜帶認證的,不同于上方token為空的放行}
}

首先這里獲取token我們是從請求頭中獲取對應的token,然后對其進行判空,如果為空我們直接進行放行,且不走后續流程,接下來進行解析token,得到里面的userid,再根據userid從redis中獲取對應的用戶信息,最后將其存儲到SecurityContextHolder中,因為后續的過濾器都需要從中獲取日認證信息,最后進行分析操作。

?還有一個需要注意的點就是,SecurityContextHolder.getContext().setAuthentication()需要傳入authentication對象,我們構建對象的時候采用的是三個參數的,因為第三個參數是判斷是否認證的關鍵。

接下來我們需要將這個過濾器進行配置。

?接著我們進行訪問user/login接口會返回給我們一個帶token的響應體,再訪問hello接口此時是403的,因為沒有攜帶token,所以就對應上方的代碼,沒有token放行并且return不執行后續流程(這里的放行是因為后續有其他專門拋異常的過濾器進行處理,而return是為了不讓其走響應的流程)

此時若我們將user/login生成的token放入hello接口的請求頭那么就可以正常訪問到了。

那么我們這套過濾器的目的也就達到了(獲取token、解析token、存入SecurityContextHolder)

2.3.3.5 退出登錄

到這里我們也就比較容易的實現退出登錄了,我們只需要刪除redis中對應的數據,之后攜帶token進行訪問的時候,在我們自定義的過濾器中會獲取redis中對應的用戶信息,此時獲取不到就意味著未登錄。

我們攜帶這個token去訪/user/logout接口。

那么退出登錄功能就實現了。?

本文學習于b站up主三更!!!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/45222.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/45222.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/45222.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Python爬蟲并輸出

1. Python爬蟲并輸出示例 下面是一個使用Python編寫的簡單網絡爬蟲示例&#xff0c;該爬蟲將抓取某個網頁&#xff08;例如&#xff0c;我們假設為https://example.com&#xff0c;但請注意實際使用時我們需要替換為一個真實且允許抓取的網站&#xff09;的標題&#xff08;Ti…

機器學習(V)--無監督學習(三)EM算法

EM算法 極大似然估計 極大似然估計&#xff1a;(maximum likelihood estimate, MLE) 是一種常用的模型參數估計方法。它假設觀測樣本出現的概率最大&#xff0c;也即樣本聯合概率&#xff08;也稱似然函數&#xff09;取得最大值。 為求解方便&#xff0c;對樣本聯合概率取對…

工作理念分享

上份工作的上級&#xff0c;分享他的工作理念&#xff0c;做個整理&#xff1a; 1 士氣上要奮發向上有追求&#xff0c;最低限度當然是要恰飯&#xff0c;保證生活&#xff0c;最好是做一些事情&#xff0c;把錢掙了的同時也能有更多的收獲。 2 公司為社會&#xff0c;用戶創造…

華為HCIP Datacom H12-821 卷36

1.單選題 在PIM- SM中&#xff0c;以下關于RP 的描述&#xff0c;錯誤的是哪一選項? A、在PIM-SM中&#xff0c;組播數據流量不一定必須經過RP的轉發。 B、對于一個組播組來說&#xff0c;可以同時有多個RP地址&#xff0c;提升網絡可靠性。 C、組播網絡中&#xff0c;可以…

【BUG】已解決:JsonMappingException

已解決&#xff1a;JsonMappingException 歡迎來到英杰社區https://bbs.csdn.net/topics/617804998 概述&#xff1a; 沒有getter方法的實體的序列化&#xff0c;并解決Jackson引發的JsonMappingException異常。 默認情況下&#xff0c;Jackson 2只會處理公有字段或具有公有get…

vue 級聯下拉框選擇的思維

在原來的js的思維下&#xff0c;級聯下拉框的選擇往往是&#xff0c;先綁定一級下拉框的菜單&#xff0c;然后在該下拉框下onchange, 在onchange事件中獲取當前選項&#xff0c;然后綁定二級下拉框的數據&#xff0c;以此類推…… 在vue框架下應該改變思維&#xff0c;首先設置…

經典再現,回顧常見排序算法之冒泡排序,附Java源碼及優化改進實現

回顧一下排序算法&#xff0c;老酒裝新瓶&#xff0c;給自己的技能點做個回放。 排序(Sorting) 是計算機程序設計中的一種重要操作&#xff0c;它的功能是將一個數據元素(或記錄)的任意序列&#xff0c;重新排列成一個有序的序列&#xff0c;也可以理解為高矮個站隊。 衡量排…

Renesas R7FA8D1BH (Cortex?-M85) 控制DS18B20

目錄 概述 1 軟硬件 1.1 軟硬件環境信息 1.2 開發板信息 1.3 調試器信息 2 FSP和KEIL配置 2.1 硬件接口電路 2.2 FSB配置DS18B20的IO 2.3 生成Keil工程文件 3 DS18B20驅動代碼 3.1 DS18B20介紹 3.2 DS18B20驅動實現 3.2.1 IO狀態定義 3.2.2 讀IO狀態函數 3.2.3…

OpenCV:python圖像旋轉,cv2.getRotationMatrix2D 和 cv2.warpAffine 函數

前言 僅供個人學習用&#xff0c;如果對各位朋友有參考價值&#xff0c;給個贊或者收藏吧 ^_^ 一. cv2.getRotationMatrix2D(center, angle, scale) 1.1 參數說明 parameters center&#xff1a;旋轉中心坐標&#xff0c;是一個元組參數(col, row) angle&#xff1a;旋轉角度…

Go-知識測試-模糊測試

Go-知識測試-模糊測試 1. 定義2. 例子3. 數據結構4. tesing.F.Add5. 模糊測試的執行6. testing.InternalFuzzTarget7. testing.runFuzzing8. testing.fRunner9. FuzzXyz10. RunFuzzWorker11. CoordinateFuzzing12. 總結 建議先看&#xff1a;https://blog.csdn.net/a1879272183…

一文入門【NestJs】Providers

Nest學習系列 ??一文入門【NestJS】 ??一文入門【NestJs】Controllers 控制器 &#x1f6a9; 前言 在NestJS的世界里&#xff0c;理解“Providers”是構建健壯、可維護的后端服務的關鍵。NestJS&#xff0c;作為Node.js的一個現代框架&#xff0c;采用了Angular的一些核…

Redis的安裝配置及IDEA中使用

目錄 一、安裝redis&#xff0c;配置redis.conf 1.安裝gcc 2.將redis的壓縮包放到指定位置解壓 [如下面放在 /opt 目錄下] 3.編譯安裝 4.配置redis.conf文件 5.開機自啟 二、解決虛擬機本地可以連接redis但是主機不能連接redis 1.虛擬機網絡適配器網絡連接設置為橋接模式…

VSCode上通過C++實現單例模式

單例模式實際上就是為了確保一個類最多只有一個實例&#xff0c;并且在程序的任何地方都可以訪問這個實例&#xff0c;也就是提供一個全局訪問點&#xff0c;單例對象不需要手動釋放&#xff0c;交給系統來釋放就可以了&#xff0c;單例模式的設計初衷就是為了在整個應用程序的…

vue 下拉菜單樹形結構——vue-treeselect的組件使用

參考&#xff1a; https://www.cnblogs.com/syjtiramisu/p/17672866.htmlhttps://www.cnblogs.com/syjtiramisu/p/17672866.html vue-treeselect的使用 - 簡書下載依賴 使用https://www.jianshu.com/p/459550e1477d 實際項目使用&#xff1a;

uni-app iOS上架相關App store App store connect 云打包有次數限制

相冊權限 uni-app云打包免費有次數 切換一個賬號繼續

使用SOAP與TrinityCore交互(待定)

原文&#xff1a;SOAP with TrinityCore | TrinityCore MMo Project Wiki 如何使用SOAP與TC交互 SOAP代表簡單對象訪問協議&#xff0c;是一種類似于REST的基于標準的web服務訪問協議的舊形式。只要必要的配置到位&#xff0c;您就可以利用SOAP向TrinityCore服務器發送命令。 …

Open3D 計算點云配準的精度和重疊度

目錄 一、概述 1.1計算配準精度 1.2計算點云重疊度 二、代碼實現 2.1關鍵函數 2.2完整代碼 三、實現效果 3.1原始點云 3.2計算結果 一、概述 在點云配準中,精度和重疊度是兩個重要的評價指標。精度通常用均方根誤差(RMSE)來衡量,而重疊度則表示兩個點云在…

centos環境啟動/重啟java服務腳本優化

centos環境啟動/重啟java服務腳本優化 引部分命令說明根據端口查詢服務進程殺死進程函數腳本接收參數 腳本注意重啟文檔位置異常 引 在離線環境部署的多個java應用組成的系統&#xff0c;測試階段需要較為頻繁的發布&#xff0c;因資源限制&#xff0c;沒有弄devops或CICD那套…

華為手機聯系人不見了怎么恢復?3個解決方案

華為手機聯系人列表就像是我們精心編織的社交網絡之網。然而&#xff0c;有時&#xff0c;這張網可能會因為各種原因而意外破損&#xff0c;聯系人信息消失得無影無蹤&#xff0c;讓我們陷入“人脈孤島”的困境。華為手機聯系人不見了怎么恢復&#xff1f;別擔心&#xff0c;我…

構建高質量數據集與智能數據工程平臺:播客AI Odyssey深度對話實錄

對話整數智能聯創和前IDEA研究員&#xff1a;構建高質量數據集與智能數據工程平臺 - AI Odyssey | 小宇宙 - 聽播客&#xff0c;上小宇宙 人工智能技術的日益深遠發展&#xff0c;對人工智能的性能提升與技術迭代提出了新的要求。在大模型訓練中&#xff0c;已有的研究和實踐表…