SpringSecurity+JWT實現(xiàn)登錄流程分析
1. SpringSecurity介紹
Spring Security 是一個功能強大且高度可定制的身份驗證和訪問控制框架。它是為Java應用程序設計的,特別是那些基于Spring的應用程序。Spring Security是一個社區(qū)驅動的開源項目,它提供了全面的安全性解決方案,包括防止常見的安全漏洞如CSRF、點擊劫持、會話固定等。
以下是Spring Security的一些關鍵特性和概念:
- 認證(Authentication):Spring Security可以處理用戶的身份驗證過程,即確認用戶是否是他們聲稱的人。它可以使用多種機制來進行身份驗證,例如表單登錄、HTTP基本認證、OAuth2、JWT等。
- 授權(Authorization):一旦用戶通過了身份驗證,Spring Security就會根據用戶的權限來決定他們可以訪問哪些資源。這可以通過定義角色、權限或更細粒度的訪問規(guī)則來實現(xiàn)。
- 安全配置:Spring Security可以通過Java配置或XML配置來設置安全策略。通常推薦使用Java配置,因為它與現(xiàn)代Spring應用更為集成,并提供編譯時檢查。
- 攔截URL模式:可以定義哪些URL需要特定的權限才能訪問,以及如何處理未認證或未經授權的請求。
- 過濾器鏈:Spring Security利用了一組過濾器(
Filter),這些過濾器在每次HTTP請求時被調用,以執(zhí)行各種安全相關的任務。開發(fā)者可以根據需要添加自定義過濾器。 - 密碼編碼:為了安全存儲用戶密碼,Spring Security支持多種加密方式,如BCrypt、PBKDF2等。
- 記住我(Remember-Me):允許系統(tǒng)在用戶關閉瀏覽器后仍然保持登錄狀態(tài),直到明確登出或cookie過期。
- 注銷(Logout):提供了安全的退出機制,確保用戶的會話被正確地銷毀。
- CSRF保護:默認啟用跨站請求偽造攻擊防護,確保只有來自合法來源的請求才能修改服務器端的狀態(tài)。
- Session管理:可以配置會話創(chuàng)建策略,例如只在需要時創(chuàng)建會話,或者限制同一時間內的并發(fā)會話數(shù)量。
- OAuth2和OpenID Connect支持:內置對OAuth2客戶端和資源服務器的支持,方便集成第三方認證服務。
使用Spring Security,開發(fā)者可以專注于業(yè)務邏輯的開發(fā),而將安全問題交給這個成熟可靠的框架來處理。同時,由于其高度可擴展性和靈活性,Spring Security也適合用于構建復雜的安全需求。
2. 登錄流程
登錄API無需攔截,SpringSecurity直接放行。
/**
* @description 認證授權
**/
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Api(tags = "認證")
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首先會校驗用戶名與密碼,和用戶的角色,然后調用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負責創(chuàng)建token、解析token與獲取userId。
public class JwtTokenUtils {
/**
* 生成足夠的安全隨機密鑰,以適合符合規(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認證流程
// 啟用 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()
// 其他的接口都需要認證后才能請求
.anyRequest().authenticated()
.and()
//添加自定義Filter
.addFilter(new JwtAuthorizationFilter(authenticationManager(), stringRedisTemplate))
// 不需要session(不創(chuàng)建會話)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 授權異常處理
.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, 需要手動注入
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實現(xiàn)的,可以實現(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 {
/**
* 當用戶嘗試訪問需要權限才能的REST資源而不提供Token或者Token錯誤或者過期時,
* 將調用此方法發(fā)送401響應以及錯誤信息
*/
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}public class JwtAccessDeniedHandler implements AccessDeniedHandler {
/**
* 當用戶嘗試訪問需要權限才能的REST資源而權限不足的時候,
* 將調用此方法發(fā)送403響應以及錯誤信息
*/
@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. 權限管理
基于@PreAuthorize實現(xiàn)權限管理
@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@RequestMapping("/users")
@Api(tags = "用戶")
public class UserController {
private final UserService userService;
@GetMapping
// 有任意角色的權限都可以訪問
@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() + " 鑒權" + 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("根據用戶名刪除用戶")
public ResponseEntity<Void> deleteUserByUserName(@RequestParam("username") String username) {
userService.delete(username);
return ResponseEntity.ok().build();
}
}到此這篇關于SpringSecurity+JWT實現(xiàn)登錄流程分析的文章就介紹到這了,更多相關SpringSecurity JWT登錄內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java設計模式之策略模式_動力節(jié)點Java學院整理
策略模式是對算法的封裝,把一系列的算法分別封裝到對應的類中,并且這些類實現(xiàn)相同的接口,相互之間可以替換。接下來通過本文給大家分享Java設計模式之策略模式,感興趣的朋友一起看看吧2017-08-08
Java中的包(Package)與導入(Import)示例詳解
這篇文章主要詳細介紹了Java中的包(Package)和導入(Import)概念,包括包的定義、作用、JDK中主要的包、導入的目的與用法、特殊情況的導入、靜態(tài)導入、包的訪問權限和命名規(guī)范,文章通過豐富的解釋和代碼示例,幫助讀者深入理解這些概念的實際應用,需要的朋友可以參考下2024-11-11
關于JDK+Tomcat+eclipse+MyEclipse的配置方法,看這篇夠了
關于JDK+Tomcat+eclipse+MyEclipse的配置問題,很多朋友都搞不太明白,網上一搜配置方法多種哪種最精簡呢,今天小編給大家分享一篇文章幫助大家快速掌握JDK Tomcat eclipse MyEclipse配置技巧,需要的朋友參考下吧2021-06-06

