SpringSecurity+JWT实现登录流程分析
作者:CRE_MO
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,它是为Java应用程序设计的,特别是那些基于Spring的应用程序,下面给大家介绍SpringSecurity+JWT实现登录流程,感兴趣的朋友一起看看吧
1. SpringSecurity介绍
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是为Java应用程序设计的,特别是那些基于Spring的应用程序。Spring Security是一个社区驱动的开源项目,它提供了全面的安全性解决方案,包括防止常见的安全漏洞如CSRF、点击劫持、会话固定等。
以下是Spring Security的一些关键特性和概念:
- 认证(Authentication):Spring Security可以处理用户的身份验证过程,即确认用户是否是他们声称的人。它可以使用多种机制来进行身份验证,例如表单登录、HTTP基本认证、OAuth2、JWT等。
- 授权(Authorization):一旦用户通过了身份验证,Spring Security就会根据用户的权限来决定他们可以访问哪些资源。这可以通过定义角色、权限或更细粒度的访问规则来实现。
- 安全配置:Spring Security可以通过Java配置或XML配置来设置安全策略。通常推荐使用Java配置,因为它与现代Spring应用更为集成,并提供编译时检查。
- 拦截URL模式:可以定义哪些URL需要特定的权限才能访问,以及如何处理未认证或未经授权的请求。
- 过滤器链:Spring Security利用了一组过滤器(
Filter
),这些过滤器在每次HTTP请求时被调用,以执行各种安全相关的任务。开发者可以根据需要添加自定义过滤器。 - 密码编码:为了安全存储用户密码,Spring Security支持多种加密方式,如BCrypt、PBKDF2等。
- 记住我(Remember-Me):允许系统在用户关闭浏览器后仍然保持登录状态,直到明确登出或cookie过期。
- 注销(Logout):提供了安全的退出机制,确保用户的会话被正确地销毁。
- CSRF保护:默认启用跨站请求伪造攻击防护,确保只有来自合法来源的请求才能修改服务器端的状态。
- Session管理:可以配置会话创建策略,例如只在需要时创建会话,或者限制同一时间内的并发会话数量。
- OAuth2和OpenID Connect支持:内置对OAuth2客户端和资源服务器的支持,方便集成第三方认证服务。
使用Spring Security,开发者可以专注于业务逻辑的开发,而将安全问题交给这个成熟可靠的框架来处理。同时,由于其高度可扩展性和灵活性,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创建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负责创建token、解析token与获取userId。
public class JwtTokenUtils { /** * 生成足够的安全随机密钥,以适合符合规范的签名 */ 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 注解开发 @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(不创建会话) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 授权异常处理 .exceptionHandling().authenticationEntryPoint(new JwtAuthenticationEntryPoint()) .accessDeniedHandler(new JwtAccessDeniedHandler()); // 防止H2 web 页面的Frame 被拦截 http.headers().frameOptions().disable(); } /** * Cors配置优化 **/ @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实现的,可以实现不同线程之间的隔离。
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错误或者过期时, * 将调用此方法发送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资源而权限不足的时候, * 将调用此方法发送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实现权限管理
@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实现登录流程分析的文章就介绍到这了,更多相关SpringSecurity JWT登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!