SpringSecurity?認(rèn)證實(shí)現(xiàn)流程分析
一、初步理解
SpringSecurity的原理其實(shí)就是一個(gè)過(guò)濾器鏈,內(nèi)部包含了提供各種功能的過(guò)濾器。
當(dāng)前系統(tǒng)中SpringSecurity過(guò)濾器鏈中有哪些過(guò)濾器及它們的順序。
核心過(guò)濾器:
- (認(rèn)證)UsernamePasswordAuthenticationFilter:負(fù)責(zé)處理我們?cè)诘顷戫?yè)面填寫了用戶名密碼后的登陸請(qǐng)求
- ExceptionTranslationFilter:處理過(guò)濾器鏈中拋出的任何AccessDeniedException和 AuthenticationException
- (授權(quán))FilterSecurityInterceptor:負(fù)責(zé)權(quán)限校驗(yàn)的過(guò)濾器
二、Token(Jwt)登錄校驗(yàn)流程
三、具體認(rèn)證授權(quán)細(xì)節(jié)
下圖是UsernamePasswordAuthenticationFilter處理用戶名、密碼,然后將用戶名、密碼、權(quán)限信息封裝到Authentication對(duì)象中,再放到SecurityContextHolder中。
Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問(wèn)系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
AuthenticationManager接口:定義了認(rèn)證Authentication的方法
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的 方法。
UserDetails接口:提供核心用戶信息。通過(guò)UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝 成UserDetails對(duì)象返回。然后將這些信息封裝到Authentication對(duì)象中。
認(rèn)證
- 當(dāng)用戶登錄時(shí),前端將用戶輸入的用戶名、密碼信息傳輸?shù)胶笈_(tái),后臺(tái)用一個(gè)類對(duì)象將其封裝起來(lái),通常使用的是UsernamePasswordAuthenticationToken這個(gè)類。
- 程序負(fù)責(zé)驗(yàn)證這個(gè)類對(duì)象。驗(yàn)證方法是調(diào)用Service根據(jù)username從數(shù)據(jù)庫(kù)中取用戶信息到實(shí)體類的實(shí)例中,比較兩者的密碼,如果密碼正確就成功登陸,同時(shí)把包含著用戶的用戶名、密碼、所具有的權(quán)限等信息(用戶id、昵稱、是否管理員)的類對(duì)象放到SecurityContextHolder(安全上下文容器,類似Session)中去。
- 用戶訪問(wèn)一個(gè)資源的時(shí)候,首先判斷是否是受限資源。如果是的話還要判斷當(dāng)前是否未登錄,沒有的話就跳到登錄頁(yè)面。
- 如果用戶已經(jīng)登錄,訪問(wèn)一個(gè)受限資源的時(shí)候,程序要根據(jù)url去數(shù)據(jù)庫(kù)中取出該資源所對(duì)應(yīng)的所有可以訪問(wèn)的角色,然后拿著當(dāng)前用戶的所有角色一一對(duì)比,判斷用戶是否可以訪問(wèn)(這里就是和權(quán)限相關(guān))。
授權(quán)
- 在SpringSecurity中,會(huì)使用默認(rèn)的FilterSecurityInterceptor來(lái)進(jìn)行權(quán)限校驗(yàn)。在FilterSecurityInterceptor中會(huì)從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權(quán)限信息。當(dāng)前用戶是否擁有訪問(wèn)當(dāng)前資源所需的權(quán)限。
- 所以我們?cè)陧?xiàng)目中只需要把當(dāng)前登錄用戶的權(quán)限信息也存入Authentication。然后設(shè)置我們的資源所需要的權(quán)限即可。
自定義登錄認(rèn)證接口:①調(diào)用ProviderManager的方法進(jìn)行認(rèn)證;②如果認(rèn)證通過(guò)生成jwt;③把用戶信息存入redis中
自定義權(quán)限信息查詢:在UserDetailsService這個(gè)實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫(kù)
四、自定義權(quán)限查詢
修改UsernamePasswordAuthenticationFilter上圖最右邊的授權(quán)部分。
1.自定義登陸接口
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public R login(@RequestBody User user) { String jwt = userService.login(user); if (StringUtils.hasLength(jwt)) { return R.ok().message("登陸成功").data("token", jwt); } return R.error().message("登陸失敗"); } }
2.配置數(shù)據(jù)庫(kù)校驗(yàn)登錄用戶
從之前的分析我們可以知道,我們可以自定義一個(gè)UserDetailsService,讓SpringSecurity使用我們的 UserDetailsService。我們自己的UserDetailsService可以從數(shù)據(jù)庫(kù)中查詢用戶名和密碼。
創(chuàng)建一個(gè)類實(shí)現(xiàn)UserDetailsService接口,重寫loadUserByUsername方法
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查詢用戶信息 QueryWrapper<User> queryWrapper=new QueryWrapper<>(); queryWrapper.eq("user_name",username); User user = userMapper.selectOne(queryWrapper); //如果沒有查詢到用戶,就拋出異常 if(Objects.isNull(user)){ throw new RuntimeException("用戶名或密碼錯(cuò)誤"); } //TODO 查詢用戶對(duì)應(yīng)的權(quán)限信息 細(xì)節(jié)見SpringSecurity(二)——授權(quán)實(shí)現(xiàn) //如果有,把數(shù)據(jù)封裝成UserDetails對(duì)象返回 return new LoginUser(user); } }
五、Jwt認(rèn)證過(guò)濾器(自定義過(guò)濾器)
(1)在接口中我們通過(guò)AuthenticationManager的authenticate方法來(lái)進(jìn)行用戶認(rèn)證,所以需要在 SecurityConfig中配置把AuthenticationManager注入容器。
@EnableWebSecurity @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig{ /** * 登錄時(shí)需要調(diào)用AuthenticationManager.authenticate執(zhí)行一次校驗(yàn) */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }
(2)登錄的業(yè)務(wù)邏輯層實(shí)現(xiàn)類
第一次登錄,生成jwt存入redis
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Autowired private AuthenticationManager authenticationManager; @Autowired private StringRedisTemplate stringRedisTemplate; @Override public String login(User user) { //1.封裝Authentication對(duì)象 ,密碼校驗(yàn),自動(dòng)完成 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()); //2.進(jìn)行校驗(yàn) Authentication authenticate = authenticationManager.authenticate(authentication); //3.如果authenticate為空 if (Objects.isNull(authenticate)) { throw new RuntimeException("登錄失敗"); //TODO 登錄失敗 } //4.得到用戶信息 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); //生成jwt,使用fastjson的方法,把對(duì)象轉(zhuǎn)成字符串 String loginUserString = JSON.toJSONString(loginUser); //調(diào)用JWT工具類,生成jwt令牌 String jwt = JwtUtils.createJWT(loginUserString, null); //5.把生成的jwt存到redis String tokenKey = "token_" + jwt; stringRedisTemplate.opsForValue().set(tokenKey, jwt, JwtUtils.JWT_TTL / 1000); Map<String, Object> map = new HashMap<>(); map.put("token", jwt); map.put("username", loginUser.getUsername()); return jwt; } }
(3)jwt認(rèn)證校驗(yàn)過(guò)濾器
我們需要自定義一個(gè)過(guò)濾器,這個(gè)過(guò)濾器會(huì)去獲取請(qǐng)求頭中的token,對(duì)token進(jìn)行解析取出其中的 userid。 使用userid去redis中獲取對(duì)應(yīng)的LoginUser對(duì)象。
然后封裝Authentication對(duì)象存入SecurityContextHolder
/** * token驗(yàn)證過(guò)濾器 //每一個(gè)servlet請(qǐng)求,只會(huì)執(zhí)行一次 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private LoginFailureHandler loginFailureHandler; @Autowired private StringRedisTemplate stringRedisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { //1.獲取當(dāng)前請(qǐng)求的url地址 String url = request.getRequestURI(); //如果當(dāng)前請(qǐng)求不是登錄請(qǐng)求,則需要進(jìn)行token驗(yàn)證 if (!url.equals("/user/login")) { //2.驗(yàn)證token this.validateToken(request); } } catch (AuthenticationException e) { System.out.println(e); loginFailureHandler.onAuthenticationFailure(request, response, e); } //3.登錄請(qǐng)求不需要驗(yàn)證token doFilter(request, response, filterChain); } /** * 驗(yàn)證token */ private void validateToken(HttpServletRequest request) throws AuthenticationException { //1.獲取token String token = request.getHeader("Authorization"); //如果請(qǐng)求頭部沒有獲取到token,則從請(qǐng)求的參數(shù)中進(jìn)行獲取 if (ObjectUtils.isEmpty(token)) { token = request.getParameter("Authorization"); } if (ObjectUtils.isEmpty(token)) { throw new CustomerAuthenticationException("token不存在"); } //2.redis進(jìn)行校驗(yàn) String redisStr = stringRedisTemplate.opsForValue().get("token_" + token); if(ObjectUtils.isEmpty(redisStr)) { throw new CustomerAuthenticationException("token已過(guò)期"); } //3.解析token Claims claims = null; try { claims = JwtUtils.parseJWT(token); } catch (Exception e) { throw new CustomerAuthenticationException("token解析失敗"); } //4.獲取到用戶信息 String loginUserString = claims.getSubject(); //把字符串轉(zhuǎn)成loginUser對(duì)象 LoginUser loginUser = JSON.parseObject(loginUserString, LoginUser.class); //創(chuàng)建身份驗(yàn)證對(duì)象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); //5.設(shè)置到Spring Security上下文 SecurityContextHolder.getContext().setAuthentication(authenticationToken); } }
(4)把jwt過(guò)濾器注冊(cè)到springsecurity過(guò)濾器鏈中
放在UsernamePasswordAuthenticationFilter前面
@EnableWebSecurity @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig{ //自定義jwt校驗(yàn)過(guò)濾器 @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //配置關(guān)閉csrf機(jī)制 http.csrf(csrf -> csrf.disable()); //登陸失敗處理器 http.formLogin(configurer -> { configurer.failureHandler(loginFailureHandler); }); http.sessionManagement(configurer -> // STATELESS(無(wú)狀態(tài)): 表示應(yīng)用程序是無(wú)狀態(tài)的,不會(huì)創(chuàng)建會(huì)話。 configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); //請(qǐng)求攔截方式 http.authorizeHttpRequests(auth -> auth .requestMatchers("/user/login").permitAll() .anyRequest().authenticated() ); //?。。。?!注冊(cè)jwt過(guò)濾器?。。。。。?! http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //異常處理器 http.exceptionHandling(configurer -> { configurer.accessDeniedHandler(customerAccessDeniedHandler); configurer.authenticationEntryPoint(anonymousAuthenticationHandler); }); return http.build(); //允許跨域 } /** * 登錄時(shí)需要調(diào)用AuthenticationManager.authenticate執(zhí)行一次校驗(yàn) */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }
到此這篇關(guān)于SpringSecurity 認(rèn)證實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringSecurity 認(rèn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?Feign超時(shí)設(shè)置深入了解
Spring?Cloud中Feign客戶端是默認(rèn)開啟支持Ribbon的,最重要的兩個(gè)超時(shí)就是連接超時(shí)ConnectTimeout和讀超時(shí)ReadTimeout,在默認(rèn)情況下,也就是沒有任何配置下,F(xiàn)eign的超時(shí)時(shí)間會(huì)被Ribbon覆蓋,兩個(gè)超時(shí)時(shí)間都是1秒2023-03-03MybatisPlus使用排序查詢時(shí)將null值放到最后
按照更新時(shí)間排序,但是更新時(shí)間可能為null,因此將null的數(shù)據(jù)放到最后,本文主要介紹了MybatisPlus使用排序查詢時(shí)將null值放到最后,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08Mybatis中where標(biāo)簽與if標(biāo)簽結(jié)合使用詳細(xì)說(shuō)明
mybatis中if和where用于動(dòng)態(tài)sql的條件拼接,在查詢語(yǔ)句中如果缺失某個(gè)條件,通過(guò)if和where標(biāo)簽可以動(dòng)態(tài)的改變查詢條件,下面這篇文章主要給大家介紹了關(guān)于Mybatis中where標(biāo)簽與if標(biāo)簽結(jié)合使用的詳細(xì)說(shuō)明,需要的朋友可以參考下2023-03-03教你用IDEA配置JUnit并進(jìn)行單元測(cè)試
今天教各位小伙伴怎么用IDEA配置JUnit并進(jìn)行單元測(cè)試,文中有非常詳細(xì)的圖文介紹及代碼示例,對(duì)正在學(xué)習(xí)IDEA的小伙伴有很好的幫助,需要的朋友可以參考下2021-05-05使用Java將字節(jié)數(shù)組轉(zhuǎn)成16進(jìn)制形式的代碼實(shí)現(xiàn)
在很多場(chǎng)景下,需要進(jìn)行分析字節(jié)數(shù)據(jù),但是我們存起來(lái)的字節(jié)數(shù)據(jù)一般都是二進(jìn)制的,這時(shí)候就需要我們將其轉(zhuǎn)成16進(jìn)制的方式方便分析,本文主要介紹如何使用Java將字節(jié)數(shù)組格式化成16進(jìn)制的格式并輸出,需要的朋友可以參考下2024-05-05SpringBoot項(xiàng)目啟動(dòng)健康檢查的操作方法
在現(xiàn)代的微服務(wù)架構(gòu)中,容器化技術(shù)已經(jīng)成為一種主流的部署方式,Docker 作為容器化技術(shù)的代表,提供了一種輕量級(jí)、可移植的解決方案,然而,僅僅將應(yīng)用容器化是不夠的,我們還需要確保這些容器在運(yùn)行時(shí)能夠保持健康狀態(tài),這就是健康檢查發(fā)揮作用的地方2024-12-12