SpringSecurity?認(rèn)證實現(xiàn)流程分析
一、初步理解
SpringSecurity的原理其實就是一個過濾器鏈,內(nèi)部包含了提供各種功能的過濾器。
當(dāng)前系統(tǒng)中SpringSecurity過濾器鏈中有哪些過濾器及它們的順序。

核心過濾器:
- (認(rèn)證)UsernamePasswordAuthenticationFilter:負(fù)責(zé)處理我們在登陸頁面填寫了用戶名密碼后的登陸請求
- ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException和 AuthenticationException
- (授權(quán))FilterSecurityInterceptor:負(fù)責(zé)權(quán)限校驗的過濾器
二、Token(Jwt)登錄校驗流程

三、具體認(rèn)證授權(quán)細(xì)節(jié)
下圖是UsernamePasswordAuthenticationFilter處理用戶名、密碼,然后將用戶名、密碼、權(quán)限信息封裝到Authentication對象中,再放到SecurityContextHolder中。

Authentication接口: 它的實現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
AuthenticationManager接口:定義了認(rèn)證Authentication的方法
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的 方法。
UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝 成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。
認(rèn)證
- 當(dāng)用戶登錄時,前端將用戶輸入的用戶名、密碼信息傳輸?shù)胶笈_,后臺用一個類對象將其封裝起來,通常使用的是UsernamePasswordAuthenticationToken這個類。
- 程序負(fù)責(zé)驗證這個類對象。驗證方法是調(diào)用Service根據(jù)username從數(shù)據(jù)庫中取用戶信息到實體類的實例中,比較兩者的密碼,如果密碼正確就成功登陸,同時把包含著用戶的用戶名、密碼、所具有的權(quán)限等信息(用戶id、昵稱、是否管理員)的類對象放到SecurityContextHolder(安全上下文容器,類似Session)中去。
- 用戶訪問一個資源的時候,首先判斷是否是受限資源。如果是的話還要判斷當(dāng)前是否未登錄,沒有的話就跳到登錄頁面。
- 如果用戶已經(jīng)登錄,訪問一個受限資源的時候,程序要根據(jù)url去數(shù)據(jù)庫中取出該資源所對應(yīng)的所有可以訪問的角色,然后拿著當(dāng)前用戶的所有角色一一對比,判斷用戶是否可以訪問(這里就是和權(quán)限相關(guān))。
授權(quán)
- 在SpringSecurity中,會使用默認(rèn)的FilterSecurityInterceptor來進行權(quán)限校驗。在FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權(quán)限信息。當(dāng)前用戶是否擁有訪問當(dāng)前資源所需的權(quán)限。
- 所以我們在項目中只需要把當(dāng)前登錄用戶的權(quán)限信息也存入Authentication。然后設(shè)置我們的資源所需要的權(quán)限即可。
自定義登錄認(rèn)證接口:①調(diào)用ProviderManager的方法進行認(rèn)證;②如果認(rèn)證通過生成jwt;③把用戶信息存入redis中
自定義權(quán)限信息查詢:在UserDetailsService這個實現(xiàn)類中去查詢數(shù)據(jù)庫
四、自定義權(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ù)庫校驗登錄用戶
從之前的分析我們可以知道,我們可以自定義一個UserDetailsService,讓SpringSecurity使用我們的 UserDetailsService。我們自己的UserDetailsService可以從數(shù)據(jù)庫中查詢用戶名和密碼。
創(chuàng)建一個類實現(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("用戶名或密碼錯誤");
}
//TODO 查詢用戶對應(yīng)的權(quán)限信息
細(xì)節(jié)見SpringSecurity(二)——授權(quán)實現(xiàn)
//如果有,把數(shù)據(jù)封裝成UserDetails對象返回
return new LoginUser(user);
}
}五、Jwt認(rèn)證過濾器(自定義過濾器)

(1)在接口中我們通過AuthenticationManager的authenticate方法來進行用戶認(rèn)證,所以需要在 SecurityConfig中配置把AuthenticationManager注入容器。
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
/**
* 登錄時需要調(diào)用AuthenticationManager.authenticate執(zhí)行一次校驗
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}(2)登錄的業(yè)務(wù)邏輯層實現(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對象 ,密碼校驗,自動完成
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
//2.進行校驗
Authentication authenticate = authenticationManager.authenticate(authentication);
//3.如果authenticate為空
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登錄失敗"); //TODO 登錄失敗
}
//4.得到用戶信息
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
//生成jwt,使用fastjson的方法,把對象轉(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)證校驗過濾器
我們需要自定義一個過濾器,這個過濾器會去獲取請求頭中的token,對token進行解析取出其中的 userid。 使用userid去redis中獲取對應(yīng)的LoginUser對象。
然后封裝Authentication對象存入SecurityContextHolder
/**
* token驗證過濾器 //每一個servlet請求,只會執(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)前請求的url地址
String url = request.getRequestURI();
//如果當(dāng)前請求不是登錄請求,則需要進行token驗證
if (!url.equals("/user/login")) {
//2.驗證token
this.validateToken(request);
}
} catch (AuthenticationException e) {
System.out.println(e);
loginFailureHandler.onAuthenticationFailure(request, response, e);
}
//3.登錄請求不需要驗證token
doFilter(request, response, filterChain);
}
/**
* 驗證token
*/
private void validateToken(HttpServletRequest request) throws AuthenticationException {
//1.獲取token
String token = request.getHeader("Authorization");
//如果請求頭部沒有獲取到token,則從請求的參數(shù)中進行獲取
if (ObjectUtils.isEmpty(token)) {
token = request.getParameter("Authorization");
}
if (ObjectUtils.isEmpty(token)) {
throw new CustomerAuthenticationException("token不存在");
}
//2.redis進行校驗
String redisStr = stringRedisTemplate.opsForValue().get("token_" + token);
if(ObjectUtils.isEmpty(redisStr)) {
throw new CustomerAuthenticationException("token已過期");
}
//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對象
LoginUser loginUser = JSON.parseObject(loginUserString, LoginUser.class);
//創(chuàng)建身份驗證對象
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//5.設(shè)置到Spring Security上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}(4)把jwt過濾器注冊到springsecurity過濾器鏈中
放在UsernamePasswordAuthenticationFilter前面
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
//自定義jwt校驗過濾器
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//配置關(guān)閉csrf機制
http.csrf(csrf -> csrf.disable());
//登陸失敗處理器
http.formLogin(configurer -> {
configurer.failureHandler(loginFailureHandler);
});
http.sessionManagement(configurer ->
// STATELESS(無狀態(tài)): 表示應(yīng)用程序是無狀態(tài)的,不會創(chuàng)建會話。
configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
//請求攔截方式
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login").permitAll()
.anyRequest().authenticated()
);
//!?。。?!注冊jwt過濾器?。。。。。?!
http.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
//異常處理器
http.exceptionHandling(configurer -> {
configurer.accessDeniedHandler(customerAccessDeniedHandler);
configurer.authenticationEntryPoint(anonymousAuthenticationHandler);
});
return http.build(); //允許跨域
}
/**
* 登錄時需要調(diào)用AuthenticationManager.authenticate執(zhí)行一次校驗
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}到此這篇關(guān)于SpringSecurity 認(rèn)證實現(xiàn)的文章就介紹到這了,更多相關(guān)SpringSecurity 認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java代碼輕松實現(xiàn)將HTML轉(zhuǎn)成圖片
HTML 轉(zhuǎn)圖片并非小眾場景,在實際開發(fā)中相當(dāng)常見,這篇文章就來和大家詳細(xì)介紹一下如何通過Java代碼輕松實現(xiàn)將HTML轉(zhuǎn)成圖片,感興趣的小伙伴可以了解下2026-04-04
JavaWeb中請求轉(zhuǎn)發(fā)和請求重定向的區(qū)別以及使用
今天帶大家學(xué)習(xí)JavaWeb的相關(guān)知識,文章圍繞著JavaWeb中請求轉(zhuǎn)發(fā)和請求重定向的區(qū)別以及使用展開,文中有非常詳細(xì)的介紹,需要的朋友可以參考下2021-06-06
Java中的springboot監(jiān)聽事件和處理事件詳解
這篇文章主要介紹了Java中的springboot監(jiān)聽事件和處理事件,這個示例展示了如何在Spring Boot應(yīng)用中定義自定義事件、發(fā)布事件以及監(jiān)聽事件,需要的朋友可以參考下2024-07-07
java.lang.StackOverflowError出現(xiàn)的原因及解決
這篇文章主要介紹了java.lang.StackOverflowError出現(xiàn)的原因及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07
Java學(xué)習(xí)之Lambda表達式的使用詳解
Lambda表達式是Java SE 8中一個重要的新特性,允許通過表達式來代替功能接口。本文將通過一些簡單的示例和大家講講Lamda表達式的使用,感興趣的可以了解一下2022-12-12

