java - Spring Security with Rest API with React

标签 java spring-boot spring-security spring-rest spring-session

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

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

所以无法找出确保休息 api 的正确方法。 (会不会就是spring security,spring security+jwt,spring security+jwt+spring session+cookie)

编辑

来自数据库的用户名验证

@Component
CustomUserDetailsService -> loadUserByUsername -> Mongo Db 

通过加密
@Bean
public PasswordEncoder passwordEncoder() { ... }

跨源
@Bean
public WebMvcConfigurer corsConfigurer() { ... }

注册 Controller
@RestController
public class RegistrationController {
@PostMapping("/registration")
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public ResponseEntity registerUserAccount(... ) { ... }
]

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

@Configuration
@EnableMongoHttpSession

所以以上是我已经实现的。在那之后,我被困在如何让用户保持 session 状态并继续验证用户。

最佳答案

基本授权:

(我假设您知道如何创建端点,并且您对创建简单的 Spring Boot 应用程序和 React 应用程序有基本的了解,所以我只会坚持授权主题。)

通过基本授权,您的前端应用程序必须在每次调用 API 时发送用户凭据。而且我们必须考虑到您的后端可能在 localhost:8080 上打开和前端 localhost:3000所以我们必须处理 CORS。 (更多关于 CORS Cross-Origin Resource Sharing (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;
    }

我们有 /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();
            });
    }
...

从顶部开始,我们有:
  • 用户凭据
  • 根据 MDN web docks Authorization header 上的基本授权规范进行授权的 header
  • 如果响应是 ok我们可以将用户凭据存储在某个地方,并且在下次调用 API 时,我们必须再次包含授权 header 。 (但我们不应该将用户敏感数据存储在诸如 LocalStorageSessionStorage 之类的地方,用于生产但用于开发是可以的 Storing Credentials in Local Storage )

  • JWT:

    什么是 JWT,您可以在本网站上阅读 Jwt.io .您还可以调试有助于乞讨的 token 。

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

    像那里最重要的是:
  • JwtTokenRequest tokenRequest - 这是带有 username 的 POJO和 password ,只是为了从前端登录获取它并进一步发送。
  • JwtTokenResponse,也是 POJO,只是在 cookie 中发送的 token 字符串
  • 我还让 TimeZone 设置 token 到期时间。
  • @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();
        }
    

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

    @Service
    public class JwtAuthenticationService
    {
        private AuthenticationManager authenticationManager;
    
        private final String SECRET_KEY = "SecretKey";
    
        public JwtAuthenticationService(AuthenticationManager authenticationManager)
        {
            this.authenticationManager = authenticationManager;
        }
    
        public JwtTokenResponse authenticate(JwtTokenRequest tokenRequest, String url, TimeZone timeZone) throws AuthenticationException
        {
            UserDetails userDetails = managerAuthentication(tokenRequest.getUsername(), tokenRequest.getPassword());
    
            String token = generateToken(userDetails.getUsername(), url, timeZone);
    
            return new JwtTokenResponse(token);
        }
    

    管理身份验证。您不需要手动检查密码是否属于用户名,因为如果您有 loadByUsername实现后,Spring 将使用此方法加载用户并检查密码。 Manually Authenticate User with Spring Security
    private UserDetails managerAuthentication(String username, String password) throws AuthenticationException
        {
            Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    
            return (UserDetails) authenticate.getPrincipal();
        }
    

    如果没有抛出异常,则表示用户凭据正确,然后我们可以生成 JWT token 。

    在这个例子中,我使用 Java JWT库,您可以添加到 pom.xml文件。

    此方法根据请求的时区生成 token ,并存储信息请求 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);
            }
        }
    

    如果一切正常,则 token 存储在 http-only cookie 中。

    当我们有 token 时,如果对经过身份验证的端点进行了请求,我们必须先过滤该请求。
    我们需要添加我们的自定义过滤器:
  • 首先扩展过滤器(你可以在这里阅读为什么这个 What is OncePerRequestFilter?)
  • 添加 key

  • public class JwtFilter extends OncePerRequestFilter
    {
        private final String SECRET_KEY = "SecretKey";
    }
    
    //or load from other source
    public class JwtFilter extends OncePerRequestFilter
    {
        private final String SECRET_KEY = ApplicationConstants.SECRET_KEY;
    }
    
  • 从父类实现方法
  • 取决于您从何处获取 token ,我们只需要加载它。在这个例子中,我使用 HttpOnly cookie
  • 如果存在 cookie,则进行授权

  • @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
        {
            Cookie tokenCookie = null;
            if (request.getCookies() != null)
            {
                for (Cookie cookie : request.getCookies())
                {
                    if (cookie.getName().equals("accessToken"))
                    {
                        tokenCookie = cookie;
                        break;
                    }
                }
            }
    
            if (tokenCookie != null)
            {
                cookieAuthentication(tokenCookie);
            }
    
            chain.doFilter(request, response);
        }
    
  • 如果通过了所有验证,则在 SecurityContextHolder 中设置此用户已通过身份验证
    什么是 SecurityContextHolder 您可以在这里阅读 10.1. SecurityContextHolder
  • 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;
        }
    

    现在,请求使用 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);
        }
    

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

    示例请求:
    fetch('http://localhost:8080/only-already-authenticated-users', {
          method: "GET",
          credentials: 'include',
          headers: {
            'Content-Type': 'application/json'
          },
        })
    

    关于java - Spring Security with Rest API with React,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61441508/

    相关文章:

    java - 如何重写 equals to 和 tostring 方法

    java - Spring boot Controller 中的 `new` 是不好的做法吗?

    java - Spring Boot 以自己的对象作为键返回 Map 的问题

    java - 在spring security中使用自定义方法安全注解

    facebook - 为什么Spring Social插件偶尔会在User类上返回空电子邮件?

    java - 使用按位运算来优化 Java Math

    java - 存储 selectOneListbox 中的上一个 SelectedValue

    grails - 在 secuser 上成功注册并自动登录后,Spring Security 重定向到上一页

    java - 如何编写使用tinyMCE的SWT应用程序?

    java - 避免在 Spring MVC 请求映射中使用 URI 中的 float 进行文件扩展名检测