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)擊劫持、會(huì)話固定等。
以下是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就會(huì)根據(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)的請(qǐng)求。
- 過濾器鏈:Spring Security利用了一組過濾器(
Filter),這些過濾器在每次HTTP請(qǐng)求時(shí)被調(diào)用,以執(zhí)行各種安全相關(guān)的任務(wù)。開發(fā)者可以根據(jù)需要添加自定義過濾器。 - 密碼編碼:為了安全存儲(chǔ)用戶密碼,Spring Security支持多種加密方式,如BCrypt、PBKDF2等。
- 記住我(Remember-Me):允許系統(tǒng)在用戶關(guān)閉瀏覽器后仍然保持登錄狀態(tài),直到明確登出或cookie過期。
- 注銷(Logout):提供了安全的退出機(jī)制,確保用戶的會(huì)話被正確地銷毀。
- CSRF保護(hù):默認(rèn)啟用跨站請(qǐng)求偽造攻擊防護(hù),確保只有來自合法來源的請(qǐng)求才能修改服務(wù)器端的狀態(tài)。
- Session管理:可以配置會(huì)話創(chuàng)建策略,例如只在需要時(shí)創(chuàng)建會(huì)話,或者限制同一時(shí)間內(nèi)的并發(fā)會(huì)話數(shù)量。
- OAuth2和OpenID Connect支持:內(nèi)置對(duì)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首先會(huì)校驗(yàn)用戶名與密碼,和用戶的角色,然后調(diào)用JwtTokenUtils創(chuàng)建token,然后以u(píng)serId為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)證后才能請(qǐng)求
.anyRequest().authenticated()
.and()
//添加自定義Filter
.addFilter(new JwtAuthorizationFilter(authenticationManager(), stringRedisTemplate))
// 不需要session(不創(chuàng)建會(huì)話)
.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)容請(qǐng)搜索腳本之家以前的文章或繼續(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è)技巧,希望對(duì)大家有所幫助2025-01-01
Spring中的@PathVariable注解詳細(xì)解析
這篇文章主要介紹了Spring中的@PathVariable注解詳細(xì)解析,@PathVariable 是 Spring 框架中的一個(gè)注解,用于將 URL 中的變量綁定到方法的參數(shù)上,它通常用于處理 RESTful 風(fēng)格的請(qǐng)求,從 URL 中提取參數(shù)值,并將其傳遞給方法進(jìn)行處理,需要的朋友可以參考下2024-01-01
mybatis一級(jí)緩存和二級(jí)緩存的區(qū)別及說明
這篇文章主要介紹了mybatis一級(jí)緩存和二級(jí)緩存的區(qū)別及說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
Java設(shè)計(jì)模式之策略模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
策略模式是對(duì)算法的封裝,把一系列的算法分別封裝到對(duì)應(yīng)的類中,并且這些類實(shí)現(xiàn)相同的接口,相互之間可以替換。接下來通過本文給大家分享Java設(shè)計(jì)模式之策略模式,感興趣的朋友一起看看吧2017-08-08
Java中的包(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

