【问题标题】:How can I secure my Spring Data Rest endpoints using Spring Security?如何使用 Spring Security 保护我的 Spring Data Rest 端点?
【发布时间】:2019-06-19 09:17:07
【问题描述】:

我正在使用 Spring 应用程序。在浏览器上一切正常。我可以使用现有用户登录,只需提供我的用户名和密码。我也可以用新用户注册,然后用它登录。

我还可以调用一些可用的 REST 端点。我没有手动定义这些端点。它们是自动创建的,因为我使用的是 spring-boot-starter-data-rest 依赖项。

REST 请求的 URL 类似于 http://localhost:8182/api/v1/recipes

我正在尝试使用 Postman 获取食谱列表。我想收到类似“403 Forbidden”之类的错误消息,或者类似的东西,因为我没有提供任何凭据。相反,我收到了登录页面的 HTML 代码,以及“200 OK”的状态代码。

这也适用于我提供用户名和密码作为请求标头之后(也许我需要使用另一种方式来提供凭据)

user:user
password:password

以下列表包含一些 sn-ps 代码,以显示我在项目中编写的有关应用程序安全位的所有内容:

  1. 代码的第一个 sn-p 代表我项目中的 SecurityConfig 类:

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
        @Autowired
        private UserService userService;
    
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
            auth.userDetailsService(userService).passwordEncoder(User.PASSWORD_ENCODER);
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception{
            web.ignoring().antMatchers("/css/**");
            web.ignoring().antMatchers("/images/**");
            web.ignoring().antMatchers("/js/**");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception{
            http.authorizeRequests()
                    .antMatchers("/sign-up").permitAll()
                    .anyRequest()
                    .hasRole("USER")
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .successHandler(loginSuccessHandler())
                    .failureHandler(loginFailureHandler())
                    .and()
                    .logout()
                    .permitAll()
                    .logoutSuccessUrl("/login")
                    .and()
                    .csrf().disable();
        }
    
        public AuthenticationSuccessHandler loginSuccessHandler(){
            return (request, response, authentication) ->{
              response.sendRedirect("/recipes/");
            };
        }
    
        public AuthenticationFailureHandler loginFailureHandler(){
            return (request, response, exception) ->{
              request.getSession().setAttribute("flash",
                      new FlashMessage("Incorrect username and/or password. Try again.",
                              FlashMessage.Status.FAILURE));
                response.sendRedirect("/login");
            };
        }
    
        @Bean
        public EvaluationContextExtension securityExtension(){
            return new EvaluationContextExtensionSupport() {
                @Override
                public String getExtensionId() {
                    return "security";
                }
    
                @Override
                public Object getRootObject(){
                    Authentication authentication =
                            SecurityContextHolder.getContext().getAuthentication();
                    return new SecurityExpressionRoot(authentication) {
                    };
                }
            };
        }
    }
    
  2. 第二个是User实体类:

        @Entity
        public class User implements UserDetails{
            public static final PasswordEncoder PASSWORD_ENCODER =
                    new BCryptPasswordEncoder();
    
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;
    
            @NotNull
            @Column(unique = true)
            @Size(min = 2, max = 20)
            private String username;
    
            @NotNull
            @Column(length = 100)
            @JsonIgnore
            private String password;
    
            @NotNull
            @Column(length = 100)
            @JsonIgnore
            private String matchingPassword;
    
            @Column(nullable = false)
            private boolean enabled;
    
            @OneToOne
            @JoinColumn(name = "role_id")
            @JsonIgnore
            private Role role;
    
            @ManyToMany(targetEntity = Recipe.class, fetch = FetchType.EAGER)
            @JoinTable(name = "users_favorite_recipes",
                    joinColumns = @JoinColumn(name="user_id"),
                    inverseJoinColumns = @JoinColumn(name = "recipe_id"))
            private List<Recipe> favoritedRecipes = new ArrayList<>();
    
            @JsonIgnore
            @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
            private List<Recipe> ownedRecipes = new ArrayList<>();
    
            //constructor ...
            //getters and setters ...
    
            public void encryptPasswords(){
                password = PASSWORD_ENCODER.encode(password);
                matchingPassword = PASSWORD_ENCODER.encode(matchingPassword);
            }
    
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                List<GrantedAuthority> authorities = new ArrayList<>();
                authorities.add(new SimpleGrantedAuthority(role.getName()));
                return authorities;
            }
    
            @Override
            public String getPassword() {
                return password;
            }
    
            @Override
            public String getUsername() {
                return username;
            }
    
            @Override
            public boolean isAccountNonExpired() {
                return true;
            }
    
            @Override
            public boolean isAccountNonLocked() {
                return true;
            }
    
            @Override
            public boolean isCredentialsNonExpired() {
                return true;
            }
    
            @Override
            public boolean isEnabled() {
                return enabled;
            }
        }
    
  3. 第三个 sn-p 代表一个接口,扩展了 用户详情服务:

    public interface UserService extends UserDetailsService{
        UserDetails loadUserByUsername(String username);
        User findByUsername(String username);
        User registerNewUser(String username, boolean enabled, String password, String matchingPassword);
        void save(User user);
        List<User> findAll();
    }
    
  4. 第四个也是最后一个sn-p是前一个的实现 接口(用户服务):

    @Component
    @ComponentScan
    public class UserServiceImpl implements UserService{
        @Autowired
        private UserDao userDao;
    
        @Autowired
        private RoleDao roleDao;
    
        @Override
        public User findByUsername(String username) {
            User user = userDao.findByUsername(username);
            Hibernate.initialize(user.getFavoritedRecipes());
            return user;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException{
            User user = userDao.findByUsername(username);
            if(user ==  null){
                throw new UsernameNotFoundException(
                  username + " was not found"
                );
            }
    
            return user;
        }
    
        @Override
        public void save(User user) {
            userDao.save(user);
        }
    
        @Override
        public User registerNewUser(String username, boolean enabled, String password, String matchingPassword) {
            return userDao.save(new User(username, enabled, password, matchingPassword));
        }
    
        @Override
        public List<User> findAll() {
            return userDao.findAll();
        }
    }
    

在这种情况下我必须修改哪些内容才能获得功能性 REST API 授权?

【问题讨论】:

    标签: java spring spring-boot spring-security spring-data-rest


    【解决方案1】:

    据我了解,您有一个 RESTful API(没有 UI),如果是这样,您可以更新 SecurityConfig #configure(HttpSecurity http)方法替换这个:

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests()
                .antMatchers("/sign-up").permitAll()
                .anyRequest()
                .hasRole("USER")
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .successHandler(loginSuccessHandler())
                .failureHandler(loginFailureHandler())
                .and()
                .logout()
                .permitAll()
                .logoutSuccessUrl("/login")
                .and()
                .csrf().disable();
    }
    

    通过这个:

    @Override
        protected void configure(HttpSecurity http) throws Exception {
                http
                    .cors()
                    .and()
                    .csrf()
                    .disable()
                    .exceptionHandling()
                    .authenticationEntryPoint((request, response, exc) ->
                            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "You are not authorized to access this resource."))
                    .and()
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    // Array of String that contain's all endpoints you want secure
                    .antMatchers(ENDPOINTS_TO_SECURE).access("hasAnyRole('ROLE_USER')")
                    // Array of String that contain's all endpoints you want to permit
                    .antMatchers(WHITE_LIST).permitAll()
                    .anyRequest()
                    .authenticated();
            // disable page caching
            http.headers().cacheControl();
        }
    

    【讨论】:

    • antMatcher 字符串可以使用通配符吗?
    • 我使用了您提供的代码,但不是我可以获取所有配方实体,而无需提供任何用户名或密码(使用 REST)。此外,如果我尝试从浏览器访问根端点 (localhost:8182),则不再显示登录页面
    • 啊哈,我明白了,所以你有一个 UI !就像我在回复中所说的那样,这是为了 RESTful API 安全,是的,您可以使用通配符(例如 "/recipes/**"
    【解决方案2】:

    需要配置自己的认证入口点,获取403消息,可以使用Http403ForbiddenEntryPoint。

    例子:。

    @RestController
    public class Controller {
    
        @GetMapping("/test")
        public String test() {
            return "test";
        }
    }
    

    添加.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint()):

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception{
            http.authorizeRequests()
                    .antMatchers("/sign-up").permitAll()
                    .anyRequest()
                    .hasRole("USER")
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and()
                    .logout()
                    .permitAll()
                    .logoutSuccessUrl("/login")
                    .and()
                    .csrf().disable()
                    .exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint());
        }
    }
    

    现在,当我尝试访问 http://localhost:8080/test 时,我收到了 403 Access Denied 消息。

    【讨论】:

    • 我在configure 方法的末尾添加了.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint()); 行。我收到403 Access Denied 消息。但是,如果我从浏览器访问 localhost:8182 根端点,它会给我同样的错误,而不是将我重定向到登录页面。但是,如果我使用localhost:8182/login 端点,一切都很好,我可以使用任何可用用户登录。我认为我们即将解决这个问题。只有根端点问题。我必须如何在 Postman 等 REST 客户端中提供凭据?
    • 我需要指定我仍然没有添加我自己的任何端点。我仍在使用自动生成的 REST 端点,在这种情况下,这将是我想要的结果。
    • 我添加的 Rest 端点仅用于演示目的(显然您不需要)。您需要编写自定义入口点或覆盖 Http403ForbiddenEntryPoint,因为您需要 root 重定向到登录而不是获取 403。
    • 您能否提供一些有关如何创建该自定义条目或覆盖 Http403ForbiddenEntryPoint 的代码?另外,我如何需要在 Postman 等休息客户端中提供凭据才能获得授权并获取所需数据?
    猜你喜欢
    • 2016-03-04
    • 2014-07-29
    • 2019-06-17
    • 2015-12-09
    • 2014-02-21
    • 2014-04-13
    • 2017-07-12
    • 2013-11-29
    • 2017-05-27
    相关资源
    最近更新 更多