SpringSecurity+JWT實(shí)現(xiàn)登錄流程分析
1. SpringSecurity介紹
Spring Security 是一個(gè)功能強(qiáng)大且高度可定制的身份驗(yàn)證和訪問控制框架。它是為Java應(yīng)用程序設(shè)計(jì)的,特別是那些基于Spring的應(yīng)用程序。Spring Security是一個(gè)社區(qū)驅(qū)動(dòng)的開源項(xiàng)目,它提供了全面的安全性解決方案,包括防止常見的安全漏洞如CSRF、點(diǎn)擊劫持、會話固定等。
以下是Spring Security的一些關(guān)鍵特性和概念:
- 認(rèn)證(Authentication):Spring Security可以處理用戶的身份驗(yàn)證過程,即確認(rèn)用戶是否是他們聲稱的人。它可以使用多種機(jī)制來進(jìn)行身份驗(yàn)證,例如表單登錄、HTTP基本認(rèn)證、OAuth2、JWT等。
- 授權(quán)(Authorization):一旦用戶通過了身份驗(yàn)證,Spring Security就會根據(jù)用戶的權(quán)限來決定他們可以訪問哪些資源。這可以通過定義角色、權(quán)限或更細(xì)粒度的訪問規(guī)則來實(shí)現(xiàn)。
- 安全配置:Spring Security可以通過Java配置或XML配置來設(shè)置安全策略。通常推薦使用Java配置,因?yàn)樗c現(xiàn)代Spring應(yīng)用更為集成,并提供編譯時(shí)檢查。
- 攔截URL模式:可以定義哪些URL需要特定的權(quán)限才能訪問,以及如何處理未認(rèn)證或未經(jīng)授權(quán)的請求。
- 過濾器鏈:Spring Security利用了一組過濾器(
Filter
),這些過濾器在每次HTTP請求時(shí)被調(diào)用,以執(zhí)行各種安全相關(guān)的任務(wù)。開發(fā)者可以根據(jù)需要添加自定義過濾器。 - 密碼編碼:為了安全存儲用戶密碼,Spring Security支持多種加密方式,如BCrypt、PBKDF2等。
- 記住我(Remember-Me):允許系統(tǒng)在用戶關(guān)閉瀏覽器后仍然保持登錄狀態(tài),直到明確登出或cookie過期。
- 注銷(Logout):提供了安全的退出機(jī)制,確保用戶的會話被正確地銷毀。
- CSRF保護(hù):默認(rèn)啟用跨站請求偽造攻擊防護(hù),確保只有來自合法來源的請求才能修改服務(wù)器端的狀態(tài)。
- Session管理:可以配置會話創(chuàng)建策略,例如只在需要時(shí)創(chuàng)建會話,或者限制同一時(shí)間內(nèi)的并發(fā)會話數(shù)量。
- OAuth2和OpenID Connect支持:內(nèi)置對OAuth2客戶端和資源服務(wù)器的支持,方便集成第三方認(rèn)證服務(wù)。
使用Spring Security,開發(fā)者可以專注于業(yè)務(wù)邏輯的開發(fā),而將安全問題交給這個(gè)成熟可靠的框架來處理。同時(shí),由于其高度可擴(kuò)展性和靈活性,Spring Security也適合用于構(gòu)建復(fù)雜的安全需求。
2. 登錄流程
登錄API無需攔截,SpringSecurity直接放行。
/** * @description 認(rèn)證授權(quán) **/ @RestController @RequestMapping("/auth") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @Api(tags = "認(rèn)證") public class AuthController { private final AuthService authService; @PostMapping("/login") @ApiOperation("登錄") public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest) { String token = authService.createToken(loginRequest); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set(SecurityConstants.TOKEN_HEADER, token); return new ResponseEntity<>(httpHeaders, HttpStatus.OK); } }
AuthService首先會校驗(yàn)用戶名與密碼,和用戶的角色,然后調(diào)用JwtTokenUtils創(chuàng)建token,然后以userId為key,token作為value存在Redis中。
@Service @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class AuthService { private final UserService userService; private final StringRedisTemplate stringRedisTemplate; private final CurrentUserUtils currentUserUtils; public String createToken(LoginRequest loginRequest) { User user = userService.find(loginRequest.getUsername()); if (!userService.check(loginRequest.getPassword(), user.getPassword())) { throw new BadCredentialsException("The user name or password is not correct."); } JwtUser jwtUser = new JwtUser(user); if (!jwtUser.isEnabled()) { throw new BadCredentialsException("User is forbidden to login"); } List<String> authorities = jwtUser.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); String token = JwtTokenUtils.createToken(user.getUserName(), user.getId().toString(), authorities, loginRequest.getRememberMe()); stringRedisTemplate.opsForValue().set(user.getId().toString(), token); return token; } public void removeToken() { stringRedisTemplate.delete(currentUserUtils.getCurrentUser().getId().toString()); } }
JwtTokenUtils負(fù)責(zé)創(chuàng)建token、解析token與獲取userId。
public class JwtTokenUtils { /** * 生成足夠的安全隨機(jī)密鑰,以適合符合規(guī)范的簽名 */ private static final byte[] API_KEY_SECRET_BYTES = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY); private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(API_KEY_SECRET_BYTES); public static String createToken(String username, String id, List<String> roles, boolean isRememberMe) { long expiration = isRememberMe ? SecurityConstants.EXPIRATION_REMEMBER : SecurityConstants.EXPIRATION; final Date createdDate = new Date(); final Date expirationDate = new Date(createdDate.getTime() + expiration * 1000); String tokenPrefix = Jwts.builder() .setHeaderParam("type", SecurityConstants.TOKEN_TYPE) .signWith(SECRET_KEY, SignatureAlgorithm.HS256) .claim(SecurityConstants.ROLE_CLAIMS, String.join(",", roles)) .setId(id) .setIssuer("SnailClimb") .setIssuedAt(createdDate) .setSubject(username) .setExpiration(expirationDate) .compact(); return SecurityConstants.TOKEN_PREFIX + tokenPrefix; // 添加 token 前綴 "Bearer "; } // userId public static String getId(String token) { Claims claims = getClaims(token); return claims.getId(); } // 得到 userName、token與 authorities public static UsernamePasswordAuthenticationToken getAuthentication(String token) { Claims claims = getClaims(token); List<SimpleGrantedAuthority> authorities = getAuthorities(claims); String userName = claims.getSubject(); return new UsernamePasswordAuthenticationToken(userName, token, authorities); } private static List<SimpleGrantedAuthority> getAuthorities(Claims claims) { String role = (String) claims.get(SecurityConstants.ROLE_CLAIMS); return Arrays.stream(role.split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } private static Claims getClaims(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }
3. JWT認(rèn)證流程
// 啟用 SpringSecurity @EnableWebSecurity // 啟用 SpringSecurity 注解開發(fā) @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private final StringRedisTemplate stringRedisTemplate; public SecurityConfiguration(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 密碼編碼器 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors(withDefaults()) // 禁用 CSRF .csrf().disable() .authorizeRequests() // 指定的接口直接放行 // swagger .antMatchers(SecurityConstants.SWAGGER_WHITELIST).permitAll() .antMatchers(SecurityConstants.H2_CONSOLE).permitAll() .antMatchers(HttpMethod.POST, SecurityConstants.SYSTEM_WHITELIST).permitAll() // 其他的接口都需要認(rèn)證后才能請求 .anyRequest().authenticated() .and() //添加自定義Filter .addFilter(new JwtAuthorizationFilter(authenticationManager(), stringRedisTemplate)) // 不需要session(不創(chuàng)建會話) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 授權(quán)異常處理 .exceptionHandling().authenticationEntryPoint(new JwtAuthenticationEntryPoint()) .accessDeniedHandler(new JwtAccessDeniedHandler()); // 防止H2 web 頁面的Frame 被攔截 http.headers().frameOptions().disable(); } /** * Cors配置優(yōu)化 **/ @Bean CorsConfigurationSource corsConfigurationSource() { org.springframework.web.cors.CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(singletonList("*")); // configuration.setAllowedOriginPatterns(singletonList("*")); configuration.setAllowedHeaders(singletonList("*")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT", "OPTIONS")); configuration.setExposedHeaders(singletonList(SecurityConstants.TOKEN_HEADER)); configuration.setAllowCredentials(false); configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
自定義Filter
@Slf4j public class JwtAuthorizationFilter extends BasicAuthenticationFilter { private final StringRedisTemplate stringRedisTemplate; // 不是 Bean, 需要手動(dòng)注入 public JwtAuthorizationFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate) { super(authenticationManager); this.stringRedisTemplate = stringRedisTemplate; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token = request.getHeader(SecurityConstants.TOKEN_HEADER); if (token == null || !token.startsWith(SecurityConstants.TOKEN_PREFIX)) { SecurityContextHolder.clearContext(); chain.doFilter(request, response); return; } String tokenValue = token.replace(SecurityConstants.TOKEN_PREFIX, ""); UsernamePasswordAuthenticationToken authentication = null; try { // token是否有效 String previousToken = stringRedisTemplate.opsForValue().get(JwtTokenUtils.getId(tokenValue)); if (!token.equals(previousToken)) { SecurityContextHolder.clearContext(); chain.doFilter(request, response); return; } authentication = JwtTokenUtils.getAuthentication(tokenValue); } catch (JwtException e) { logger.error("Invalid jwt : " + e.getMessage()); } // 將userName, token, authorities保存在Context中 SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }
SecurityContextHolder是基于ThreadLocal實(shí)現(xiàn)的,可以實(shí)現(xiàn)不同線程之間的隔離。
public class SecurityContextHolder { public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL"; public static final String MODE_GLOBAL = "MODE_GLOBAL"; public static final String SYSTEM_PROPERTY = "spring.security.strategy"; private static String strategyName = System.getProperty("spring.security.strategy"); private static SecurityContextHolderStrategy strategy; private static int initializeCount = 0; public SecurityContextHolder() { } private static void initialize() { if (!StringUtils.hasText(strategyName)) { strategyName = "MODE_THREADLOCAL"; } if (strategyName.equals("MODE_THREADLOCAL")) { strategy = new ThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) { strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals("MODE_GLOBAL")) { strategy = new GlobalSecurityContextHolderStrategy(); } else { try { Class<?> clazz = Class.forName(strategyName); Constructor<?> customStrategy = clazz.getConstructor(); strategy = (SecurityContextHolderStrategy)customStrategy.newInstance(); } catch (Exception var2) { Exception ex = var2; ReflectionUtils.handleReflectionException(ex); } } ++initializeCount; } }
4. 全局異常處理器
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { /** * 當(dāng)用戶嘗試訪問需要權(quán)限才能的REST資源而不提供Token或者Token錯(cuò)誤或者過期時(shí), * 將調(diào)用此方法發(fā)送401響應(yīng)以及錯(cuò)誤信息 */ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); } }
public class JwtAccessDeniedHandler implements AccessDeniedHandler { /** * 當(dāng)用戶嘗試訪問需要權(quán)限才能的REST資源而權(quán)限不足的時(shí)候, * 將調(diào)用此方法發(fā)送403響應(yīng)以及錯(cuò)誤信息 */ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { accessDeniedException = new AccessDeniedException("Sorry you don not enough permissions to access it!"); response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } }
5. 注銷流程
刪除Redis中保存的token。
@Service @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class AuthService { private final UserService userService; private final StringRedisTemplate stringRedisTemplate; private final CurrentUserUtils currentUserUtils; public String createToken(LoginRequest loginRequest) { User user = userService.find(loginRequest.getUsername()); if (!userService.check(loginRequest.getPassword(), user.getPassword())) { throw new BadCredentialsException("The user name or password is not correct."); } JwtUser jwtUser = new JwtUser(user); if (!jwtUser.isEnabled()) { throw new BadCredentialsException("User is forbidden to login"); } List<String> authorities = jwtUser.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); String token = JwtTokenUtils.createToken(user.getUserName(), user.getId().toString(), authorities, loginRequest.getRememberMe()); stringRedisTemplate.opsForValue().set(user.getId().toString(), token); return token; } public void removeToken() { stringRedisTemplate.delete(currentUserUtils.getCurrentUser().getId().toString()); } }
@Component @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class CurrentUserUtils { private final UserService userService; public User getCurrentUser() { return userService.find(getCurrentUserName()); } private String getCurrentUserName() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getPrincipal() != null) { return (String) authentication.getPrincipal(); } return null; } }
6. 權(quán)限管理
基于@PreAuthorize實(shí)現(xiàn)權(quán)限管理
@RestController @RequiredArgsConstructor(onConstructor = @__(@Autowired)) @RequestMapping("/users") @Api(tags = "用戶") public class UserController { private final UserService userService; @GetMapping // 有任意角色的權(quán)限都可以訪問 @PreAuthorize("hasAnyRole('ROLE_USER','ROLE_MANAGER','ROLE_ADMIN')") @ApiOperation("獲取所有用戶的信息(分頁)") public ResponseEntity<Page<UserRepresentation>> getAllUser(@RequestParam(value = "pageNum", defaultValue = "0") int pageNum, @RequestParam(value = "pageSize", defaultValue = "10") int pageSize) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); System.out.println("auth信息: " + authentication.getPrincipal().toString() + " 鑒權(quán)" + authentication.getAuthorities().toString()); System.out.println("***********"); Page<UserRepresentation> allUser = userService.getAll(pageNum, pageSize); return ResponseEntity.ok().body(allUser); } @PutMapping @PreAuthorize("hasAnyRole('ROLE_ADMIN')") @ApiOperation("更新用戶") public ResponseEntity<Void> update(@RequestBody @Valid UserUpdateRequest userUpdateRequest) { userService.update(userUpdateRequest); return ResponseEntity.ok().build(); } @DeleteMapping @PreAuthorize("hasAnyRole('ROLE_ADMIN')") @ApiOperation("根據(jù)用戶名刪除用戶") public ResponseEntity<Void> deleteUserByUserName(@RequestParam("username") String username) { userService.delete(username); return ResponseEntity.ok().build(); } }
到此這篇關(guān)于SpringSecurity+JWT實(shí)現(xiàn)登錄流程分析的文章就介紹到這了,更多相關(guān)SpringSecurity JWT登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity+jwt+redis基于數(shù)據(jù)庫登錄認(rèn)證的實(shí)現(xiàn)
- spring security結(jié)合jwt實(shí)現(xiàn)用戶重復(fù)登錄處理
- Spring Security中用JWT退出登錄時(shí)遇到的坑
- Spring Boot 2結(jié)合Spring security + JWT實(shí)現(xiàn)微信小程序登錄
- springboot+jwt+springSecurity微信小程序授權(quán)登錄問題
- SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法
- Spring Security基于JWT實(shí)現(xiàn)SSO單點(diǎn)登錄詳解
相關(guān)文章
SpringBoot自動(dòng)配置的8個(gè)技巧分享
在 SpringBoot 2.x中,一個(gè)很核心的功能是自動(dòng)配置機(jī)制,這篇文章主要為大家詳細(xì)介紹了Spring Boot 2.x 實(shí)現(xiàn)自動(dòng)配置的8個(gè)技巧,希望對大家有所幫助2025-01-01Spring中的@PathVariable注解詳細(xì)解析
這篇文章主要介紹了Spring中的@PathVariable注解詳細(xì)解析,@PathVariable 是 Spring 框架中的一個(gè)注解,用于將 URL 中的變量綁定到方法的參數(shù)上,它通常用于處理 RESTful 風(fēng)格的請求,從 URL 中提取參數(shù)值,并將其傳遞給方法進(jìn)行處理,需要的朋友可以參考下2024-01-01Java設(shè)計(jì)模式之策略模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
策略模式是對算法的封裝,把一系列的算法分別封裝到對應(yīng)的類中,并且這些類實(shí)現(xiàn)相同的接口,相互之間可以替換。接下來通過本文給大家分享Java設(shè)計(jì)模式之策略模式,感興趣的朋友一起看看吧2017-08-08Java中的包(Package)與導(dǎo)入(Import)示例詳解
這篇文章主要詳細(xì)介紹了Java中的包(Package)和導(dǎo)入(Import)概念,包括包的定義、作用、JDK中主要的包、導(dǎo)入的目的與用法、特殊情況的導(dǎo)入、靜態(tài)導(dǎo)入、包的訪問權(quán)限和命名規(guī)范,文章通過豐富的解釋和代碼示例,幫助讀者深入理解這些概念的實(shí)際應(yīng)用,需要的朋友可以參考下2024-11-11關(guān)于JDK+Tomcat+eclipse+MyEclipse的配置方法,看這篇夠了
關(guān)于JDK+Tomcat+eclipse+MyEclipse的配置問題,很多朋友都搞不太明白,網(wǎng)上一搜配置方法多種哪種最精簡呢,今天小編給大家分享一篇文章幫助大家快速掌握J(rèn)DK Tomcat eclipse MyEclipse配置技巧,需要的朋友參考下吧2021-06-06