目錄
一. 快速入門
二. 認證
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主三更!!!