SpringSecurity+JWT实现登录流程分析

 更新时间:2024年12月17日 11:32:05   作者: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登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot自动配置的8个技巧分享

    SpringBoot自动配置的8个技巧分享

    在 SpringBoot 2.x中,一个很核心的功能是自动配置机制,这篇文章主要为大家详细介绍了Spring Boot 2.x 实现自动配置的8个技巧,希望对大家有所帮助
    2025-01-01
  • SpringBoot中的文件上传与下载详解

    SpringBoot中的文件上传与下载详解

    这篇文章主要介绍了SpringBoot中的文件上传与下载详解,springboot是spring家族中的一个全新框架,用来简化spring程序的创建和开发过程,本文我们就一起来看看上传与下载的操作,需要的朋友可以参考下
    2023-08-08
  • springboot 配置日志 打印不出来sql的解决方法

    springboot 配置日志 打印不出来sql的解决方法

    这篇文章主要介绍了springboot 配置日志 打印不出来sql的解决方法,帮助大家更好的理解和使用springboot框架,感兴趣的朋友可以了解下
    2020-11-11
  • Spring中的@PathVariable注解详细解析

    Spring中的@PathVariable注解详细解析

    这篇文章主要介绍了Spring中的@PathVariable注解详细解析,@PathVariable 是 Spring 框架中的一个注解,用于将 URL 中的变量绑定到方法的参数上,它通常用于处理 RESTful 风格的请求,从 URL 中提取参数值,并将其传递给方法进行处理,需要的朋友可以参考下
    2024-01-01
  • mybatis一级缓存和二级缓存的区别及说明

    mybatis一级缓存和二级缓存的区别及说明

    这篇文章主要介绍了mybatis一级缓存和二级缓存的区别及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • 图文详解SpringBoot中Log日志的集成

    图文详解SpringBoot中Log日志的集成

    这篇文章主要给大家介绍了关于SpringBoot中Log日志的集成的相关资料,文中通过实例代码以及图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2021-12-12
  • Java设计模式之策略模式_动力节点Java学院整理

    Java设计模式之策略模式_动力节点Java学院整理

    策略模式是对算法的封装,把一系列的算法分别封装到对应的类中,并且这些类实现相同的接口,相互之间可以替换。接下来通过本文给大家分享Java设计模式之策略模式,感兴趣的朋友一起看看吧
    2017-08-08
  • Java中的包(Package)与导入(Import)示例详解

    Java中的包(Package)与导入(Import)示例详解

    这篇文章主要详细介绍了Java中的包(Package)和导入(Import)概念,包括包的定义、作用、JDK中主要的包、导入的目的与用法、特殊情况的导入、静态导入、包的访问权限和命名规范,文章通过丰富的解释和代码示例,帮助读者深入理解这些概念的实际应用,需要的朋友可以参考下
    2024-11-11
  • Java匿名类和匿名函数的概念和写法

    Java匿名类和匿名函数的概念和写法

    匿名函数写法和匿名类写法的前提必须基于函数式接口匿名函数写法和匿名类写法其本质是同一个东西,只是简化写法不同使用Lambda表达式简写匿名函数时,可以同时省略实现类名、函数名,这篇文章主要介绍了Java匿名类和匿名函数的概念和写法,需要的朋友可以参考下
    2023-06-06
  • 关于JDK+Tomcat+eclipse+MyEclipse的配置方法,看这篇够了

    关于JDK+Tomcat+eclipse+MyEclipse的配置方法,看这篇够了

    关于JDK+Tomcat+eclipse+MyEclipse的配置问题,很多朋友都搞不太明白,网上一搜配置方法多种哪种最精简呢,今天小编给大家分享一篇文章帮助大家快速掌握JDK Tomcat eclipse MyEclipse配置技巧,需要的朋友参考下吧
    2021-06-06

最新评论