Spring Security 与 Rest API 和 React

Ank*_*wal 3 java spring-security spring-boot spring-session spring-rest

我正在尝试使用 Rest API 和 React 作为前端来实现 Spring Security,因为这是我的第一个全栈开发项目,我对如何实现正确的身份验证机制一无所知。

我搜索了很多,找到了关于 Spring Security with Basic Auth 的文章,但我无法弄清楚如何将该身份验证转换为 Rest api,然后通过会话/cookie 进行相同的管理。即使我得到的任何 github 参考资料都非常旧,或者它们还没有完全迁移到 Spring Security 5。

因此无法找出保护 REST API 安全的正确方法。(只是spring security,spring security + jwt,spring security + jwt + spring session + cookie)

编辑

来自数据库的用户名验证

@Component
CustomUserDetailsService -> loadUserByUsername -> Mongo Db 
Run Code Online (Sandbox Code Playgroud)

通行加密

@Bean
public PasswordEncoder passwordEncoder() { ... }
Run Code Online (Sandbox Code Playgroud)

跨源

@Bean
public WebMvcConfigurer corsConfigurer() { ... }
Run Code Online (Sandbox Code Playgroud)

注册控制器

@RestController
public class RegistrationController {
@PostMapping("/registration")
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public ResponseEntity registerUserAccount(... ) { ... }
]
Run Code Online (Sandbox Code Playgroud)

蒙戈会话

build.gradle
implementation 'org.springframework.session:spring-session-data-mongodb'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

@Configuration
@EnableMongoHttpSession
Run Code Online (Sandbox Code Playgroud)

以上是我已经实现的。之后,我陷入了如何让用户保持会话状态并不断验证用户的问题。

AzJ*_*zJa 5

基本授权:

(我假设您知道如何创建端点,并且具有创建简单 Spring Boot 应用程序和 React 应用程序的基本知识,因此我将只讨论授权主题。)

通过基本授权,您的前端应用程序必须在每次调用 API 时发送用户凭据。我们必须考虑到您的后端可能在localhost:8080前端上开放localhost:3000,因此我们必须处理 CORS。(更多关于CORS跨域资源共享(CORS) 和Spring Security中的CORS Spring Security CORS

让我们从看到端点的安全配置开始。

 @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            // by default uses a Bean by the name of corsConfigurationSource
                .cors(withDefaults())
                .csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").authenticated()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers(HttpMethod.GET, "/cars").authenticated()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }
//and cors configuration
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "OPTIONS"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }
Run Code Online (Sandbox Code Playgroud)

我们有/login需要/cars身份验证的端点。如果您运行后端应用程序并打开浏览器localhost:8080/login(或/cars无关紧要),则具有基本授权的窗口将在屏幕中间弹出。Spring Security 中的默认用户名是user,密码是在控制台中生成的。复制粘贴密码就可以通过了。

现在转到前端应用程序。假设我们有一些简单的应用程序,有两个字段:用户名和密码以及按钮:登录。现在我们必须实现逻辑。

...
basicAuthorize = () => {
             let username = this.state.username;
             let password = this.state.password;

            fetch("http://localhost:8080/login", {
                headers: {
                    "Authorization": 'Basic ' + window.btoa(username + ":" + password)
                }
            }).then(resp => {
                console.log(resp);
                if (resp.ok) {
                    this.setState({
                        isLoginSucces: true});
                } else {
                    this.setState({isLoginSucces: false});
                }

                return resp.text();
            });
    }
...
Run Code Online (Sandbox Code Playgroud)

从顶部开始,我们有:

  1. 用户凭证
  2. 根据MDN Web Dock 上的基本授权规范进行授权的标头 授权标头
  3. 如果响应是,ok我们可以在某处存储用户凭据,并且在下次调用 API 时,我们必须再次包含授权标头。(但我们不应该将用户敏感数据存储在适当的位置,例如LocalStorageSessionStorage用于生产,但对于开发来说可以在本地存储中存储凭证

智威汤逊:

什么是 JWT,您可以在Jwt.io网站上阅读。您还可以调试令牌,这对请求有帮助。

制定身份验证端点和逻辑。
JWT 很难实现,因此创建一些有助于实现这一点的类会很有帮助。

就像那里最重要的是:

  • JwtTokenRequest tokenRequest - 这是带有username和 的POJO password,只是为了从前端登录中获取它并进一步发送。
  • JwtTokenResponse,也称为 POJO,只是在 cookie 中发送的令牌字符串
  • 我还使用 TimeZone 来设置令牌过期时间。
@PostMapping("/authenticate")
    public ResponseEntity<String> createJwtAuthenticationToken(@RequestBody JwtTokenRequest tokenRequest, HttpServletRequest request, HttpServletResponse response, TimeZone timeZone)
    {
        try
        {
            JwtTokenResponse accessToken = authenticationService.authenticate(tokenRequest, String.valueOf(request.getRequestURL()), timeZone);

            HttpCookie accessTokenCookie = createCookieWithToken("accessToken", accessToken.getToken(), 10 * 60);


            return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()).body("Authenticated");
        }
        catch (AuthenticationException e)
        {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
        }
    }

//creating cookie
private HttpCookie createCookieWithToken(String name, String token, int maxAge)
    {
        return ResponseCookie.from(name, token)
                .httpOnly(true)
                .maxAge(maxAge)
                .path("/")
                .build();
    }
Run Code Online (Sandbox Code Playgroud)

负责身份验证和令牌创建的服务

 @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            // by default uses a Bean by the name of corsConfigurationSource
                .cors(withDefaults())
                .csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").authenticated()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers(HttpMethod.GET, "/cars").authenticated()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }
//and cors configuration
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "OPTIONS"));
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }
Run Code Online (Sandbox Code Playgroud)

管理身份验证。你不需要手动检查密码是否属于用户名,因为如果你已经loadByUsername实现了,Spring将使用此方法来加载用户并检查密码。使用 Spring Security 手动验证用户身份

private UserDetails managerAuthentication(String username, String password) throws AuthenticationException
    {
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

        return (UserDetails) authenticate.getPrincipal();
    }
Run Code Online (Sandbox Code Playgroud)

如果没有抛出异常,则意味着用户凭据正确,然后我们可以生成 JWT 令牌。

在此示例中,我使用Java JWT库,您可以将其添加到pom.xml文件中。

该方法根据请求的时区生成令牌,并存储请求 url 信息。

private String generateToken(String username, String url, TimeZone timeZone)
    {
        try
        {
            Instant now = Instant.now();

            ZonedDateTime zonedDateTimeNow = ZonedDateTime.ofInstant(now, ZoneId.of(timeZone.getID()));

            Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
            String token = JWT.create()
                    .withIssuer(url)
                    .withSubject(username)
                    .withIssuedAt(Date.from(zonedDateTimeNow.toInstant()))
                    .withExpiresAt(Date.from(zonedDateTimeNow.plusMinutes(10).toInstant()))
                    .sign(algorithm);

            return token;
        }
        catch (JWTCreationException e)
        {
            e.printStackTrace();
            throw new JWTCreationException("Exception creating token", e);
        }
    }
Run Code Online (Sandbox Code Playgroud)

如果一切正常,则令牌将存储在仅限 http 的 cookie 中。

当我们有令牌时,如果向经过身份验证的端点发出请求,我们必须先过滤该请求。我们需要添加自定义过滤器:

...
basicAuthorize = () => {
             let username = this.state.username;
             let password = this.state.password;

            fetch("http://localhost:8080/login", {
                headers: {
                    "Authorization": 'Basic ' + window.btoa(username + ":" + password)
                }
            }).then(resp => {
                console.log(resp);
                if (resp.ok) {
                    this.setState({
                        isLoginSucces: true});
                } else {
                    this.setState({isLoginSucces: false});
                }

                return resp.text();
            });
    }
...
Run Code Online (Sandbox Code Playgroud)
  • 实现父类的方法
  • 取决于您从哪里获取令牌,我们只需加载它即可。在此示例中,我使用 HttpOnly cookie
  • 如果 cookie 存在则进行授权
@PostMapping("/authenticate")
    public ResponseEntity<String> createJwtAuthenticationToken(@RequestBody JwtTokenRequest tokenRequest, HttpServletRequest request, HttpServletResponse response, TimeZone timeZone)
    {
        try
        {
            JwtTokenResponse accessToken = authenticationService.authenticate(tokenRequest, String.valueOf(request.getRequestURL()), timeZone);

            HttpCookie accessTokenCookie = createCookieWithToken("accessToken", accessToken.getToken(), 10 * 60);


            return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()).body("Authenticated");
        }
        catch (AuthenticationException e)
        {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
        }
    }

//creating cookie
private HttpCookie createCookieWithToken(String name, String token, int maxAge)
    {
        return ResponseCookie.from(name, token)
                .httpOnly(true)
                .maxAge(maxAge)
                .path("/")
                .build();
    }
Run Code Online (Sandbox Code Playgroud)
  • 如果所有验证都通过,则在 SecurityContextHolder 中设置该用户已通过身份验证 什么是 SecurityContextHolder 您可以在此处阅读10.1。安全上下文持有者
private void cookieAuthentication(Cookie cookie)
    {
        UsernamePasswordAuthenticationToken auth = getTokenAuthentication(cookie.getValue());

        SecurityContextHolder.getContext().setAuthentication(auth);
    }

private UsernamePasswordAuthenticationToken getTokenAuthentication(String token)
    {
        DecodedJWT decodedJWT = decodeAndVerifyJwt(token);

        String subject = decodedJWT.getSubject();

        Set<SimpleGrantedAuthority> simpleGrantedAuthority = Collections.singleton(new SimpleGrantedAuthority("USER"));

        return new UsernamePasswordAuthenticationToken(subject, null, simpleGrantedAuthority);
    }

    private DecodedJWT decodeAndVerifyJwt(String token)
    {
        DecodedJWT decodedJWT = null;
        try
        {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET_KEY))
                    .build();

            decodedJWT = verifier.verify(token);

        } catch (JWTVerificationException e)
        {
            //Invalid signature/token expired
        }

        return decodedJWT;
    }
Run Code Online (Sandbox Code Playgroud)

现在,请求通过 cookie 中的 token 进行过滤。我们必须在 Spring Security 中添加自定义过滤器:

@Override
    protected void configure(HttpSecurity http) throws Exception
    {
...
//now 'session' is managed by JWT        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class);
    }
Run Code Online (Sandbox Code Playgroud)

在前端,你没有太多工作。
在您的请求中,您只需添加withCredentials: 'include',然后 cookie 将随请求一起发送。您必须使用,'include'因为它是跨源请求。请求凭证

请求示例:

fetch('http://localhost:8080/only-already-authenticated-users', {
      method: "GET",
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
    })
Run Code Online (Sandbox Code Playgroud)