springSecurity自定義登錄接口和JWT認(rèn)證過(guò)濾器的流程
下面我會(huì)根據(jù)該流程圖去自定義接口:

我們需要做的任務(wù)有:
登陸:1、通過(guò)ProviderManager的方法進(jìn)行認(rèn)證,生成jwt;2、把用戶信息存入redis;3、自定義UserDetailsService實(shí)現(xiàn)到數(shù)據(jù)庫(kù)查詢數(shù)據(jù)的方法。
校驗(yàn):自定義一個(gè)jwt認(rèn)證過(guò)濾器,其實(shí)現(xiàn)功能:獲取token;解析token;從redis獲取信息;存入SecurityContextHolder。
登陸:
圖中的 5.1步驟是到內(nèi)存中查詢用戶信息,而我們需要的是到數(shù)據(jù)庫(kù)中查詢。而圖中查詢用戶信息是調(diào)用loadUserbyUsername方法實(shí)現(xiàn)的。
所以我們需要實(shí)現(xiàn)UserDetailsService接口并重寫該方法:(下面案例中我用的mybatis plus實(shí)現(xiàn)的查詢數(shù)據(jù)庫(kù))
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
//loadUserByUsername方法即為流程圖中查詢用戶信息的方法。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查詢用戶信息
LambdaQueryWrapper<User> queryWrapper= new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//如果沒(méi)有查詢到用戶
if(Objects.isNull(user)){
throw new RuntimeException("用戶名或密碼錯(cuò)誤");
}
//封裝為UserDetails類型返回
return new LoginUser(user);
}
}我們先寫好登陸功能的controller層代碼:
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
//登陸
return loginService.login(user);
}我們需要讓springSecurity對(duì)該登陸接口放行,不需要登陸就能訪問(wèn)。在登陸service層接口中需要通過(guò)AuthenticationManager的authenticate方法進(jìn)行用戶認(rèn)證,我們先在SecurityConfig中把AuthenticationManager注入容器。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean //(name = "") //獲取AuthenticationManager的bean,因?yàn)楝F(xiàn)在只有這一個(gè)AuthenticationManager,所以不寫也沒(méi)事。
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
//放開(kāi)接口
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//關(guān)閉csrf,csrf為跨域策略,不支持post
.csrf().disable()
//不通過(guò)session獲取SecurityContext 前后端分離時(shí)session不可用
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//對(duì)于登陸接口,允許匿名訪問(wèn),登陸之后不允許訪問(wèn),只允許匿名的用戶,可以防止重復(fù)登陸。
.antMatchers("/user/login").anonymous() //permitAll() 登錄能訪問(wèn),不登錄也能訪問(wèn),一般用于靜態(tài)資源js等
//除了上面外,所有請(qǐng)求需要鑒權(quán)認(rèn)證
.anyRequest().authenticated();//authenticated():任意用戶,認(rèn)證后都可訪問(wèn)。
}
}然后我們?nèi)バ薷牡顷懡涌诘膕ervice層實(shí)現(xiàn)類代理:
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
//登陸
@Override
public ResponseResult login(User user) {
//獲取AuthenticationManager的authenticate方法進(jìn)行認(rèn)證。
//通過(guò)SecurityConfig獲取AuthenticationManager
//創(chuàng)建Authentication,第一個(gè)參數(shù)為認(rèn)證主體,沒(méi)有的話傳用戶名,第二個(gè)參數(shù)傳密碼
UsernamePasswordAuthenticationToken authenticationToken
=new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
//需要Authentication參數(shù)(上面)
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//這樣讓ProviderManager調(diào)用UserDetailsService類中的loadUserByUsername方法完成認(rèn)證
//如果認(rèn)證不通過(guò),authenticate為null
//認(rèn)證沒(méi)通過(guò),給出提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登陸失敗");
}
//認(rèn)證通過(guò),使用userid生成jwt,jwt存入ResponseResult返回
//獲取userId
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
Map<String,String> map = new HashMap<>();
map.put("token",jwt);
//完整信息存入redis,userid作key
redisCache.setCacheObject("login:"+userId,loginUser);
return new ResponseResult(200,"登陸成功",map);
}
}springSecurity流程圖中是通過(guò)獲取AuthenticationManager的authenticate方法進(jìn)行認(rèn)證。通過(guò)SecurityConfig中注入的bean獲取AuthenticationManager。
authenticationManager的authenticate方法需要一個(gè)Authentication實(shí)現(xiàn)類參數(shù),所以我們創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken實(shí)現(xiàn)類
其中的JwtUtil.createJWT(userId);方法,是我自定義的根據(jù)userId生成JWT的工具類方法:
public class JwtUtil {
//有效期為
public static final Long JWT_TTL = 60*60*1000L;//一個(gè)小時(shí)
//設(shè)置密鑰明文 。隨便定義,方便記憶和使用即可,但需要長(zhǎng)度要為4的倍數(shù)。
public static final String JWT_KEY = "jyue";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
//生成JWT
//subject為token中存放的數(shù)據(jù)(json格式)
public static String createJWT(String subject){
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());//設(shè)置過(guò)期時(shí)間
return builder.compact();
}
public 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 expMills=nowMillis+ttlMillis;
Date expDate = new Date(expMills);
return Jwts.builder()
.setId(uuid) //唯一id
.setSubject(subject) //主題 可以是JSON數(shù)據(jù)
.setIssuer("jy") //簽發(fā)者,隨便寫
.setIssuedAt(now) //簽發(fā)時(shí)間
.signWith(signatureAlgorithm,secretKey) //使用HS256對(duì)稱加密算法簽名,第二個(gè)參數(shù)為密鑰。
.setExpiration(expDate);
}
public static SecretKey generalKey(){
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKeySpec key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
return key;
}
public static Claims parseJWT(String jwt)throws Exception{
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}redisCache也為自定義的redis工具類:
@SuppressWarnings(value = {"unchecked","rawtypes"})
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
//緩存對(duì)象
//key 緩存鍵值
//value 緩存值
public <T> void setCacheObject(final String key,final T value){
redisTemplate.opsForValue().set(key,value);
}
//獲取緩存的基本對(duì)象
// key 鍵值
// return 緩存鍵對(duì)應(yīng)的數(shù)據(jù)
public <T>T getCacheObject(final String key){
ValueOperations<String,T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
}JWT認(rèn)證:
@Component //繼承這個(gè)實(shí)現(xiàn)類,保證了請(qǐng)求只會(huì)經(jīng)過(guò)該過(guò)濾器一次
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//首先需要從請(qǐng)求頭中獲取token
String token = request.getHeader("token");
//判斷token是否為Null
if(!StringUtils.hasText(token)) {
//token沒(méi)有的話,直接放行,拋異常的活交給后續(xù)專門的過(guò)濾器。
filterChain.doFilter(request, response);
//響應(yīng)時(shí)還會(huì)經(jīng)過(guò)該過(guò)濾器一次,直接return,不能執(zhí)行下面的解析token的代碼。
return;
}
//如何不為空,解析token,獲得了UserId
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
//token格式異常,不是正經(jīng)token
throw new RuntimeException("token非法");
}
//根據(jù)UserId查redis獲取用戶數(shù)據(jù)
String key = "login:"+userId;
LoginUser loginUser = redisCache.getCacheObject(key);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用戶未登錄");
}
//然后封裝Authentication對(duì)象存入SecurityContextHolder
// 因?yàn)楹罄m(xù)的過(guò)濾器會(huì)從SecurityContextHolder中獲取信息判斷認(rèn)證情況,而決定是否放行。
// 這里用UsernamePasswordAuthenticationToken三個(gè)參數(shù)的構(gòu)造函數(shù),是因?yàn)槠淠茉O(shè)置已認(rèn)證的狀態(tài)(因?yàn)橐呀?jīng)從redis中獲取了信息,確認(rèn)是認(rèn)證的了)
//第一個(gè)參數(shù)為用戶信息,第三個(gè)參數(shù)為權(quán)限信息,目前還沒(méi)獲取,先填null
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
//默認(rèn)SecurityContextHolder是ThreadLocal線程私有的,這也是為什么上面要用UsernamePasswordAuthenticationToken三個(gè)參數(shù)的構(gòu)造方法
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}
}這樣登陸后用戶發(fā)送請(qǐng)求,后端會(huì)先從請(qǐng)求頭中獲取token,然后解析出userId,然后從redis中查詢?cè)撚脩粼敿?xì)信息。然后把用戶的詳細(xì)信息存入U(xiǎn)sernamePasswordAuthenticationToken三個(gè)參數(shù)的構(gòu)造函數(shù),是因?yàn)槠淠茉O(shè)置已認(rèn)證的狀態(tài)(因?yàn)橐呀?jīng)從redis中獲取了信息,確認(rèn)是認(rèn)證的了),然后把UsernamePasswordAuthenticationToken存入SecurityContextHolder。
因?yàn)楹罄m(xù)的過(guò)濾器會(huì)從SecurityContextHolder中獲取信息判斷認(rèn)證情況,而決定是否放行。
到此這篇關(guān)于springSecurity自定義登陸接口和JWT認(rèn)證過(guò)濾器的文章就介紹到這了,更多相關(guān)springSecurity自定義登陸接口內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity的安全過(guò)濾器鏈功能詳解
- SpringSecurity中內(nèi)置過(guò)濾器的使用小結(jié)
- SpringSecurity中的Filter Chain(過(guò)濾器鏈)
- idea如何debug看springsecurity的過(guò)濾器順序
- SpringSecurity request過(guò)濾問(wèn)題示例小結(jié)
- SpringSecurity定義多個(gè)過(guò)濾器鏈的操作代碼
- springSecurity之如何添加自定義過(guò)濾器
- SpringSecurity學(xué)習(xí)之自定義過(guò)濾器的實(shí)現(xiàn)代碼
- springSecurity過(guò)濾web請(qǐng)求的項(xiàng)目實(shí)踐
相關(guān)文章
詳解spring集成mina實(shí)現(xiàn)服務(wù)端主動(dòng)推送(包含心跳檢測(cè))
本篇文章主要介紹了詳解spring集成mina實(shí)現(xiàn)服務(wù)端主動(dòng)推送(包含心跳檢測(cè)),具有一定的參考價(jià)值,與興趣的可以了解一下2017-09-09
java中將list用逗號(hào)隔開(kāi)拼成字符串的4種方法例子
我們經(jīng)常在開(kāi)發(fā)過(guò)程之中會(huì)遇到需要把返回的Id list轉(zhuǎn)換為一個(gè)使用逗號(hào)(,)分隔的字符串,下面這篇文章主要給大家介紹了關(guān)于java中將list用逗號(hào)隔開(kāi)拼成字符串的4種方法例子,需要的朋友可以參考下2024-01-01
SpringBoot深入講解單元測(cè)試與熱部署應(yīng)用
這篇文章介紹了SpringBoot單元測(cè)試與熱部署,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
java?MongoDB實(shí)現(xiàn)列表分頁(yè)查詢的示例代碼
本文主要介紹了java?MongoDB實(shí)現(xiàn)列表分頁(yè)查詢的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
Java通過(guò)HttpClient進(jìn)行HTTP請(qǐng)求的代碼詳解
Apache?HttpClient是一個(gè)功能強(qiáng)大且廣泛使用的Java庫(kù),它提供了方便的方法來(lái)執(zhí)行HTTP請(qǐng)求并處理響應(yīng)。本文將介紹如何使用HttpClient庫(kù)進(jìn)行HTTP請(qǐng)求,包括GET請(qǐng)求、POST請(qǐng)求、添加參數(shù)和請(qǐng)求體、設(shè)置請(qǐng)求頭等操作,需要的朋友可以參考下2023-05-05

