springSecurity自定義登錄接口和JWT認(rèn)證過濾器的流程
下面我會根據(jù)該流程圖去自定義接口:
我們需要做的任務(wù)有:
登陸:1、通過ProviderManager的方法進(jìn)行認(rèn)證,生成jwt;2、把用戶信息存入redis;3、自定義UserDetailsService實現(xiàn)到數(shù)據(jù)庫查詢數(shù)據(jù)的方法。
校驗:自定義一個jwt認(rèn)證過濾器,其實現(xiàn)功能:獲取token;解析token;從redis獲取信息;存入SecurityContextHolder。
登陸:
圖中的 5.1步驟是到內(nèi)存中查詢用戶信息,而我們需要的是到數(shù)據(jù)庫中查詢。而圖中查詢用戶信息是調(diào)用loadUserbyUsername方法實現(xiàn)的。
所以我們需要實現(xiàn)UserDetailsService接口并重寫該方法:(下面案例中我用的mybatis plus實現(xiàn)的查詢數(shù)據(jù)庫)
@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); //如果沒有查詢到用戶 if(Objects.isNull(user)){ throw new RuntimeException("用戶名或密碼錯誤"); } //封裝為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對該登陸接口放行,不需要登陸就能訪問。在登陸service層接口中需要通過AuthenticationManager的authenticate方法進(jìn)行用戶認(rèn)證,我們先在SecurityConfig中把AuthenticationManager注入容器。
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override @Bean //(name = "") //獲取AuthenticationManager的bean,因為現(xiàn)在只有這一個AuthenticationManager,所以不寫也沒事。 protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } //放開接口 @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf,csrf為跨域策略,不支持post .csrf().disable() //不通過session獲取SecurityContext 前后端分離時session不可用 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() //對于登陸接口,允許匿名訪問,登陸之后不允許訪問,只允許匿名的用戶,可以防止重復(fù)登陸。 .antMatchers("/user/login").anonymous() //permitAll() 登錄能訪問,不登錄也能訪問,一般用于靜態(tài)資源js等 //除了上面外,所有請求需要鑒權(quán)認(rèn)證 .anyRequest().authenticated();//authenticated():任意用戶,認(rèn)證后都可訪問。 } }
然后我們?nèi)バ薷牡顷懡涌诘膕ervice層實現(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)證。 //通過SecurityConfig獲取AuthenticationManager //創(chuàng)建Authentication,第一個參數(shù)為認(rèn)證主體,沒有的話傳用戶名,第二個參數(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)證不通過,authenticate為null //認(rèn)證沒通過,給出提示 if(Objects.isNull(authenticate)){ throw new RuntimeException("登陸失敗"); } //認(rèn)證通過,使用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流程圖中是通過獲取AuthenticationManager的authenticate方法進(jìn)行認(rèn)證。通過SecurityConfig中注入的bean獲取AuthenticationManager。
authenticationManager的authenticate方法需要一個Authentication實現(xiàn)類參數(shù),所以我們創(chuàng)建一個UsernamePasswordAuthenticationToken實現(xiàn)類
其中的JwtUtil.createJWT(userId);方法,是我自定義的根據(jù)userId生成JWT的工具類方法:
public class JwtUtil { //有效期為 public static final Long JWT_TTL = 60*60*1000L;//一個小時 //設(shè)置密鑰明文 。隨便定義,方便記憶和使用即可,但需要長度要為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è)置過期時間 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ā)時間 .signWith(signatureAlgorithm,secretKey) //使用HS256對稱加密算法簽名,第二個參數(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; //緩存對象 //key 緩存鍵值 //value 緩存值 public <T> void setCacheObject(final String key,final T value){ redisTemplate.opsForValue().set(key,value); } //獲取緩存的基本對象 // key 鍵值 // return 緩存鍵對應(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 //繼承這個實現(xiàn)類,保證了請求只會經(jīng)過該過濾器一次 public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //首先需要從請求頭中獲取token String token = request.getHeader("token"); //判斷token是否為Null if(!StringUtils.hasText(token)) { //token沒有的話,直接放行,拋異常的活交給后續(xù)專門的過濾器。 filterChain.doFilter(request, response); //響應(yīng)時還會經(jīng)過該過濾器一次,直接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對象存入SecurityContextHolder // 因為后續(xù)的過濾器會從SecurityContextHolder中獲取信息判斷認(rèn)證情況,而決定是否放行。 // 這里用UsernamePasswordAuthenticationToken三個參數(shù)的構(gòu)造函數(shù),是因為其能設(shè)置已認(rèn)證的狀態(tài)(因為已經(jīng)從redis中獲取了信息,確認(rèn)是認(rèn)證的了) //第一個參數(shù)為用戶信息,第三個參數(shù)為權(quán)限信息,目前還沒獲取,先填null UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null); //默認(rèn)SecurityContextHolder是ThreadLocal線程私有的,這也是為什么上面要用UsernamePasswordAuthenticationToken三個參數(shù)的構(gòu)造方法 SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request,response); } }
這樣登陸后用戶發(fā)送請求,后端會先從請求頭中獲取token,然后解析出userId,然后從redis中查詢該用戶詳細(xì)信息。然后把用戶的詳細(xì)信息存入UsernamePasswordAuthenticationToken三個參數(shù)的構(gòu)造函數(shù),是因為其能設(shè)置已認(rèn)證的狀態(tài)(因為已經(jīng)從redis中獲取了信息,確認(rèn)是認(rèn)證的了),然后把UsernamePasswordAuthenticationToken存入SecurityContextHolder。
因為后續(xù)的過濾器會從SecurityContextHolder中獲取信息判斷認(rèn)證情況,而決定是否放行。
到此這篇關(guān)于springSecurity自定義登陸接口和JWT認(rèn)證過濾器的文章就介紹到這了,更多相關(guān)springSecurity自定義登陸接口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity6.0 如何通過JWTtoken進(jìn)行認(rèn)證授權(quán)
- SpringSecurity+jwt+captcha登錄認(rèn)證授權(quán)流程總結(jié)
- SpringSecurity+Redis+Jwt實現(xiàn)用戶認(rèn)證授權(quán)
- SpringBoot整合SpringSecurity和JWT和Redis實現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringSecurity+jwt+redis基于數(shù)據(jù)庫登錄認(rèn)證的實現(xiàn)
- SpringBoot+SpringSecurity+JWT實現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
- SpringBoot整合SpringSecurity實現(xiàn)JWT認(rèn)證的項目實踐
- SpringSecurity整合jwt權(quán)限認(rèn)證的全流程講解
- SpringSecurity構(gòu)建基于JWT的登錄認(rèn)證實現(xiàn)
- SpringSecurity JWT基于令牌的無狀態(tài)認(rèn)證實現(xiàn)
相關(guān)文章
Java實現(xiàn)拖拽文件上傳dropzone.js的簡單使用示例代碼
本篇文章主要介紹了Java實現(xiàn)拖拽文件上傳dropzone.js的簡單使用示例代碼,具有一定的參考價值,有興趣的可以了解一下2017-07-07Spring Boot的Maven插件Spring Boot Maven plu
Spring Boot的Maven插件Spring Boot Maven plugin以Maven的方式提供Spring Boot支持,Spring Boot Maven plugin將Spring Boot應(yīng)用打包為可執(zhí)行的jar或war文件,然后以通常的方式運(yùn)行Spring Boot應(yīng)用,本文介紹Spring Boot的Maven插件Spring Boot Maven plugin,一起看看吧2024-01-01JAVA數(shù)據(jù)結(jié)構(gòu)之漢諾塔代碼實例
這篇文章主要介紹了JAVA數(shù)據(jù)結(jié)構(gòu)之漢諾塔,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Java邏輯運(yùn)算符之&&、||?與&、?|的區(qū)別及應(yīng)用
這篇文章主要介紹了Java邏輯運(yùn)算符之&&、||?與&、?|的區(qū)別及應(yīng)用的相關(guān)資料,分別是&&、||?與&、?|,并探討了它們在不同應(yīng)用場景中的表現(xiàn)和優(yōu)化效果,需要的朋友可以參考下2025-03-03Spring Boot的listener(監(jiān)聽器)簡單使用實例詳解
監(jiān)聽器(Listener)的注冊方法和 Servlet 一樣,有兩種方式:代碼注冊或者注解注冊。接下來通過本文給大家介紹Spring Boot的listener(監(jiān)聽器)簡單使用,需要的朋友可以參考下2017-04-04Java中jdk1.8和jdk17相互切換實戰(zhàn)步驟
之前做Java項目時一直用的是jdk1.8,現(xiàn)在想下載另一個jdk版本17,并且在之后的使用中可以進(jìn)行相互切換,下面這篇文章主要給大家介紹了關(guān)于Java中jdk1.8和jdk17相互切換的相關(guān)資料,需要的朋友可以參考下2023-05-05