MST

星途 面试题库

面试题:Java框架中如何实现安全的认证与授权机制

以Spring Security框架为例,描述如何在Java Web应用中实现完整的用户认证和授权功能。包括如何配置用户存储、认证流程、授权策略制定以及如何处理不同角色的访问权限控制,需详细说明关键配置和代码逻辑。
18.9万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

1. 配置用户存储

  1. 基于内存存储
    • 在Spring Boot项目中,引入Spring Security依赖后,可以在SecurityConfig类中配置基于内存的用户存储。
    • 示例代码:
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
               .authorizeRequests()
                   .antMatchers("/", "/home").permitAll()
                   .anyRequest().authenticated()
                   .and()
               .formLogin()
                   .loginPage("/login")
                   .permitAll()
                   .and()
               .logout()
                   .permitAll();
        }
    
        @Bean
        @Override
        public UserDetailsService userDetailsService() {
            UserDetails user =
                User.withDefaultPasswordEncoder()
                   .username("user")
                   .password("password")
                   .roles("USER")
                   .build();
    
            UserDetails admin =
                User.withDefaultPasswordEncoder()
                   .username("admin")
                   .password("admin")
                   .roles("ADMIN")
                   .build();
    
            return new InMemoryUserDetailsManager(user, admin);
        }
    }
    
    • 在上述代码中,通过InMemoryUserDetailsManager创建了两个用户,一个是普通用户user,一个是管理员用户admin,并为他们分配了相应的角色。
  2. 基于数据库存储
    • 首先,创建一个实现UserDetailsService接口的类,用于从数据库中加载用户信息。假设使用Spring Data JPA,定义一个User实体类和UserRepository接口。
    • User实体类示例:
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import java.util.Set;
    
    @Entity
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String username;
        private String password;
        private Set<Role> roles;
    
        // getters and setters
    }
    
    • Role实体类示例:
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    
    @Entity
    public class Role {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        // getters and setters
    }
    
    • UserRepository接口示例:
    import org.springframework.data.jpa.repository.JpaRepository;
    import com.example.demo.model.User;
    
    public interface UserRepository extends JpaRepository<User, Long> {
        User findByUsername(String username);
    }
    
    • 实现UserDetailsService接口:
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import com.example.demo.model.User;
    import com.example.demo.repository.UserRepository;
    
    @Service
    public class CustomUserDetailsService implements UserDetailsService {
    
        @Autowired
        private UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepository.findByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("User not found with username: " + username);
            }
            // 将数据库中的用户信息转换为Spring Security的UserDetails
            org.springframework.security.core.userdetails.User springUser =
                    org.springframework.security.core.userdetails.User.withDefaultPasswordEncoder()
                           .username(user.getUsername())
                           .password(user.getPassword())
                           .roles(user.getRoles().stream().map(Role::getName).toArray(String[]::new))
                           .build();
            return springUser;
        }
    }
    
    • SecurityConfig类中使用自定义的UserDetailsService
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
               .authorizeRequests()
                   .antMatchers("/", "/home").permitAll()
                   .anyRequest().authenticated()
                   .and()
               .formLogin()
                   .loginPage("/login")
                   .permitAll()
                   .and()
               .logout()
                   .permitAll();
        }
    
        @Bean
        @Override
        public UserDetailsService userDetailsService() {
            return userDetailsService;
        }
    }
    

2. 认证流程

  1. 请求拦截
    • Spring Security通过过滤器链来拦截请求。当一个请求进入应用时,首先经过一系列的过滤器,其中UsernamePasswordAuthenticationFilter用于处理基于表单的认证。
    • 该过滤器会从请求中提取用户名和密码(通常是从login表单的usernamepassword字段),并将其封装成一个UsernamePasswordAuthenticationToken对象。
  2. 认证处理
    • 这个UsernamePasswordAuthenticationToken对象会被传递给AuthenticationManager进行认证。AuthenticationManager是Spring Security的核心接口之一,负责实际的认证逻辑。
    • 通常情况下,AuthenticationManager会委托给UserDetailsService的实现类(如前面配置的基于内存或数据库的UserDetailsService)来加载用户信息,并与提交的用户名和密码进行比对。
    • 如果认证成功,AuthenticationManager会返回一个已认证的Authentication对象,该对象包含了用户的详细信息,如用户名、角色等。
    • 如果认证失败,会抛出相应的异常,如BadCredentialsException(用户名或密码错误)等,Spring Security会根据配置来处理这些异常,通常会返回一个HTTP 401 Unauthorized响应,并引导用户重新登录。

3. 授权策略制定

  1. 基于URL的授权
    • SecurityConfig类的configure(HttpSecurity http)方法中,可以通过authorizeRequests()来定义基于URL的授权策略。
    • 示例:
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
           .authorizeRequests()
               .antMatchers("/", "/home").permitAll()
               .antMatchers("/admin/**").hasRole("ADMIN")
               .anyRequest().authenticated()
               .and()
           .formLogin()
               .loginPage("/login")
               .permitAll()
               .and()
           .logout()
               .permitAll();
    }
    
    • 在上述代码中,//home路径允许所有用户访问(permitAll()),/admin/**路径只允许具有ADMIN角色的用户访问(hasRole("ADMIN")),其他所有路径要求用户必须经过认证(authenticated())。
  2. 基于方法的授权
    • 首先,在Spring Boot项目中启用方法级别的安全配置,在SecurityConfig类中添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解。
    • 然后,在需要进行授权控制的服务方法上使用注解,如@PreAuthorize
    • 示例:
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.stereotype.Service;
    
    @Service
    public class SomeService {
    
        @PreAuthorize("hasRole('ADMIN')")
        public void adminOnlyMethod() {
            // 只有ADMIN角色的用户可以调用此方法
        }
    }
    

4. 处理不同角色的访问权限控制

  1. 角色继承
    • Spring Security本身不直接支持角色继承,但可以通过自定义逻辑来实现。例如,可以创建一个自定义的RoleHierarchy实现。
    • 首先,定义角色层次关系,如admin > user,表示admin角色拥有user角色的所有权限。
    • 配置RoleHierarchy
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
    import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        public RoleHierarchy roleHierarchy() {
            RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
            roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
            return roleHierarchy;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
               .authorizeRequests()
                   .antMatchers("/", "/home").permitAll()
                   .antMatchers("/admin/**").hasRole("ADMIN")
                   .antMatchers("/user/**").hasAnyRole("ADMIN", "USER")
                   .anyRequest().authenticated()
                   .and()
               .formLogin()
                   .loginPage("/login")
                   .permitAll()
                   .and()
               .logout()
                   .permitAll();
        }
    }
    
    • 在上述代码中,/user/**路径允许ADMINUSER角色访问,由于角色层次关系ROLE_ADMIN > ROLE_USERADMIN角色自然也能访问USER角色可访问的路径。
  2. 动态权限控制
    • 可以通过实现FilterInvocationSecurityMetadataSource接口来实现动态权限控制。该接口的实现类负责根据当前请求的URL等信息,动态地获取该请求所需的权限。
    • 首先,定义一个SecurityMetadataSource实现类,例如:
    import org.springframework.security.access.ConfigAttribute;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
    import org.springframework.stereotype.Component;
    
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    
        private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
    
        public CustomSecurityMetadataSource() {
            loadResourceDefine();
        }
    
        private void loadResourceDefine() {
            resourceMap = new HashMap<>();
            // 示例配置,/admin/**路径需要ADMIN角色
            Collection<ConfigAttribute> adminAttributes = SecurityConfig.createList("ROLE_ADMIN");
            resourceMap.put("/admin/**", adminAttributes);
            // 其他路径配置...
        }
    
        @Override
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            String requestUrl = ((FilterInvocation) object).getRequestUrl();
            for (String url : resourceMap.keySet()) {
                if (requestUrl.matches(url)) {
                    return resourceMap.get(url);
                }
            }
            return null;
        }
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return true;
        }
    }
    
    • 然后,创建一个AccessDecisionManager实现类,用于根据SecurityMetadataSource返回的权限和当前用户的权限进行决策。
    import org.springframework.security.access.AccessDecisionManager;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.access.ConfigAttribute;
    import org.springframework.security.authentication.InsufficientAuthenticationException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.stereotype.Component;
    
    import java.util.Collection;
    import java.util.Iterator;
    
    @Component
    public class CustomAccessDecisionManager implements AccessDecisionManager {
    
        @Override
        public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            if (configAttributes == null) {
                return;
            }
            Iterator<ConfigAttribute> iterator = configAttributes.iterator();
            while (iterator.hasNext()) {
                ConfigAttribute configAttribute = iterator.next();
                String needRole = configAttribute.getAttribute();
                for (GrantedAuthority ga : authentication.getAuthorities()) {
                    if (needRole.equals(ga.getAuthority())) {
                        return;
                    }
                }
            }
            throw new AccessDeniedException("Access Denied!");
        }
    
        @Override
        public boolean supports(ConfigAttribute attribute) {
            return true;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return true;
        }
    }
    
    • 最后,在SecurityConfig类中配置FilterInvocationSecurityMetadataSourceAccessDecisionManager
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.access.AccessDecisionManager;
    import org.springframework.security.access.SecurityConfig;
    import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
    import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
    import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private FilterInvocationSecurityMetadataSource securityMetadataSource;
    
        @Autowired
        private AccessDecisionManager accessDecisionManager;
    
        @Bean
        public RoleHierarchy roleHierarchy() {
            RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
            roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
            return roleHierarchy;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
               .authorizeRequests()
                   .antMatchers("/", "/home").permitAll()
                   .anyRequest().authenticated()
                   .and()
               .formLogin()
                   .loginPage("/login")
                   .permitAll()
                   .and()
               .logout()
                   .permitAll();
    
            FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
            filterSecurityInterceptor.setSecurityMetadataSource(securityMetadataSource);
            filterSecurityInterceptor.setAccessDecisionManager(accessDecisionManager);
            http.addFilterBefore(filterSecurityInterceptor, FilterSecurityInterceptor.class);
        }
    }
    
    • 通过上述配置,可以根据不同的请求URL动态地决定所需的角色权限,实现更灵活的访问权限控制。