• Spring Security 之基本概念


    Spring Security 是一个安全框架, 可以简单地认为 Spring Security 是放在用户和 Spring 应用之间的一个安全屏障, 每一个 web 请求都先要经过 Spring Security 进行 Authenticate 和 Authoration 验证.

    不得不说, Spring Security 是一个非常庞大的框架, 非常值得花时间好好学习.

    ===================================
    参考文档
    ===================================
    Spring Security 4 官方文档翻译 
    Spring Security 架构概述 
    Spring Security JWT Authentication architecture 

    ===================================
    核心类
    ===================================

    --------------------------------------
    SecurityContextHolder
    --------------------------------------
    为了方便我们访问 SecurityContext 对象, Spring Security 提供了 SecurityContextHolder 类, 通过 SecurityContextHolder.getContext() 即可获取 SecurityContext 对象. SecurityContextHolder 采用 ThreadLocal 方式存储 SecurityContext 对象, 这样就能保证我们调用 SecurityContextHolder.getContext() 得到的永远是当前用户的 SecurityContext.


    --------------------------------------
    SecurityContext
    --------------------------------------
    SecurityContext 是 Spring Security 的核心, 保存着当前用户是谁, 该用户是否被认证, 具有哪些角色.

    获取 SecurityContext 对象是代码为:
    SecurityContext context= SecurityContextHolder.getContext();


    --------------------------------------
    Authentication
    --------------------------------------
    Authentication 接口是 SecurityContext 中的核心, 包含着 sessionId/IP 、用户 UserDetails 信息、用户的角色等等, 它有很多实现类(主要是 AbstractAuthenticationToken 的子类), 每种类都对应着一个认证方式.

    获取 Authentication 对象是代码为:
    Authentication auth=SecurityContextHolder.getContext().getAuthentication();

    Authentication 类成员有:
    Collection<? extends GrantedAuthority> getAuthorities() 用来获取操作权限清单
    Object getCredentials() 用来获取密码信息. 为了防止密码泄漏, 在认证通过后, 密码通常会被移除.
    Object getPrincipal() 获取用户身份信息, 大部分返回的是 UserDetails 接口类型.
    boolean isAuthenticated() 判断是否已经通过验证.
    Object getDetails() 细节信息, 比如, 对于web应用, 返回类型通常是 WebAuthenticationDetails 接口类型, 包含 IP 和 sessionId.

    --------------------------------------
    AbstractAuthenticationToken 的子类
    --------------------------------------
    AnonymousAuthenticationToken 和 AbstractAuthenticationToken 的很多子类(类名都已Token) 都实现了 Authentication 接口, 每一个子类都代表着一个具体的认证方式, 主要的子类有:
    UsernamePasswordAuthenticationToken
    RunAsUserToken
    RememberMeAuthenticationToken
    JwtAuthenticationToken
    OAuth2LoginAuthenticationToken
    CasAuthenticationToken

    --------------------------------------
    GenericFilterBean Filter 类
    --------------------------------------
    用来拦截认证的 filter, 可以在这个filter上注册认证成功的handler, 认证失败的handler.
    子类有 OncePerRequestFilter, CasAuthenticationFilter, OpenIDAuthenticationFilter, UsernamePasswordAuthenticationFilter, LogoutFilter 等等.

    一般情况下, 我们不需要为 http 请求增加新的filter, 直接基于已有的 filter 做一些定制化既能满足绝大多数需求(比如定制化 Handler). 增加 filter 的方式是, 在 Security Config 类的 configure(HttpSecurity http) 方法加入, 如下代码:

    @override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(myAuthFilter(), UsernamePasswordAuthenticationFilter.class)
    }

    --------------------------------------
    AuthenticationManager 和 AuthenticationProvider
    --------------------------------------
    用户访问网页, Spring Security 将通过 SecurityFilter 来调用 AuthenticationManager 进行验证, 缺省情况下, AuthenticationManager 将验证工作交给 ProviderManager 去做, 而 ProviderManager 会通过一系列 AuthenticationProvider 完成具体的验证.

    调用链是:
    AuthenticationManager --委托--> ProviderManager --委托--> 几个 AuthenticationProvider ----> 调用 AbstractAuthenticationToken.Authenticate()

    AuthenticationProvider 和 AbstractAuthenticationToken 子类是一一对应的, 每种 AuthenticationProvider 都需要 一个 AbstractAuthenticationToken 来支持, AuthenticationProvider 接口是对 AbstractAuthenticationToken 类的一个二次封装, 保留了 AbstractAuthenticationToken.Authenticate() 方法, 另外增加了一个检测函数用来反映是否支持传入的认证token.

    和 AbstractAuthenticationToken 一样, AuthenticationProvider 也有很多很多实现类, 比如:
    DaoAuthenticationProvider
    RunAsImplAuthenticationProvider
    LdapAuthenticationProvider
    ActiveDirectoryLdapAuthenticationProvider
    CasAuthenticationProvider

    ProviderManager 可以设置1个或0个 Parent Provider, 当所有成员 Provider 都不支持当前 Authentication 请求对象, 由 Parent Provider 提供缺省验证.

    Authentication authenticate() 函数的执行过程是:
    ProviderManager 会依次让成员 AuthenticationProvider 去验证, 如果第一个 AuthenticationProvider 不支持这个 Authentication 具体对象, 将使用第二个 AuthenticationProvider 验证, 如果全部的 AuthenticationProvider 都不支持, 再看是否设置了 Parent Provider, 如果 Parent Provider 的验证方法返回了 null, 最终会抛出 AuthenticationException 异常.

    向 ProviderManager 注册一个 AuthenticationProvider 的方式是, 在 SecurityConfig 配置类的 configure(AuthenticationManagerBuilder auth) 函数中, 使用 AuthenticationManagerBuilder 对象的 authenticationProvider() 方法注册.

    @EnableWebSecurity
    public class SecurityConfig  extends WebSecurityConfigurerAdapter {
        @Autowired
        CustomAuthenticationProvider customAuthProvider;
     
        @Override
        public void configure(AuthenticationManagerBuilder auth) 
          throws Exception {
     
            //注册一个自定义的 AuthenticationProvider
            auth.authenticationProvider(customAuthProvider);
            
            //再注册一个基于内存的 AuthenticationProvider
            auth.inMemoryAuthentication()
                .withUser("memuser")
                .password(encoder().encode("pass"))
                .roles("USER");
        }


    --------------------------------------
    UserDetails 和 UserDetailsService
    --------------------------------------
    UserDetails 包含着 Authentication 需要的用户信息(用户名/密码/操作权限), 这些信息往往是从 Jdbc 或其他数据源获取到的.

    UserDetailsService 是用来获取指定username 对应的 UserDetails 的接口.

    --------------------------------------
    常用 Java 代码
    --------------------------------------
    在 controller 接口函数中, 获取用户身份信息

    1. 使用 @AuthenticationPrincipal 注解参数的方式:

    @RequestMapping("/foo")
    public String foo(@AuthenticationPrincipal User user) {
      ... // do stuff with user
    }


    2. 使用 HttpServletRequest 中 Principal 对象的方式:
    注意这时的 Principal 对应的是 Spring Security 的 Authentication 对象.

    @RequestMapping("/foo")
    public String foo(Principal principal) {
      Authentication authentication = (Authentication) principal;
      User = (User) authentication.getPrincipal();
      ... // do stuff with user
    }

    3. 注销的写法

    @RequestMapping(value="/logout", method = RequestMethod.GET)
    public String logoutPage (HttpServletRequest request, HttpServletResponse response) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null){    
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        return "redirect:/login?logout";
    }

    4. 使用 SecurityContextHolder 获取 Authentication 对象

    SecurityContext context = SecurityContextHolder.getContext();
    Authentication authentication = context.getAuthentication();

    5. 获取 principal 的通用方法

    private String getPrincipal(){
        String userName = null;
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    
        if (principal instanceof UserDetails) {
            userName = ((UserDetails)principal).getUsername();
        } else {
            userName = principal.toString();
        }
        return userName;
    }

    ===================================
    认证过程中的 Handler
    ===================================
    一: AuthenticationSuccessHandler 接口, 用来设置验证成功后的处理动作, 有下面几个实现类, 分别是:
    ForwardAuthenticationSuccessHandler, SimpleUrlAuthenticationSuccessHandler, SavedRequestAwareAuthenticationSuccessHandler

    二: LogoutHandler 接口,设置 logout 过程中必须处理动作, logout后的重定向建议使用 LogoutSuccessHandler, 有下面几个实现类:
    AbstractRememberMeServices, CompositeLogoutHandler, CookieClearingLogoutHandler, CsrfLogoutHandler, PersistentTokenBasedRememberMeServices, SecurityContextLogoutHandler, TokenBasedRememberMeServices

    三: LogoutSuccessHandler 接口, 设置 logout完成后需要处理动作, LogoutSuccessHandler 是在 LogoutHandler 之后被执行, LogoutHandler 完成必要的动作(该过程不应该抛异常), LogoutSuccessHandler 定位是处理后续更多的步骤, 比如重定向等, 它有下面几个实现类:
    DelegatingLogoutSuccessHandler, HttpStatusReturningLogoutSuccessHandler, SimpleUrlLogoutSuccessHandler

    四: AuthenticationFailureHandler 接口, 用来设置用户验证失败后的处理动作, 有下面几个实现类, 分别是:
    1. SimpleUrlAuthenticationFailureHandler, Spring Security 缺省使用该Handler, 处理的机制是: 如果指定了 failureUrl 则跳转到该url, 如果未指定, 则返回 401 错误代码
    2. ForwardAuthenticationFailureHandler, 不管是报哪种 AuthenticationException 类型, 总是重定向到指定的 url.
    3. DelegatingAuthenticationFailureHandler, 这是一个代理类, 可以根据不同的AuthenticationException 类型, 设置不同的 AuthenticationFailureHandlers
    4. ExceptionMappingAuthenticationFailureHandler, 可以根据不同的AuthenticationException 类型,设置不同的跳转 url.

    五: AccessDeniedHandler 接口, 用来设置访问拒绝后的处理动作, 有下面几个实现类, 分别是:
    AccessDeniedHandlerImpl, DelegatingAccessDeniedHandler, InvalidSessionAccessDeniedHandler
       参考 http://www.mkyong.com/spring-security/customize-http-403-access-denied-page-in-spring-security/


    ===================================
    Granted Authority 和 Role 的概念
    ===================================
    在Spring security 中, 有 Granted Authority 和 role 两个概念. 我们既可以使用 Granted Authority 控制权限, 也可以通过 role 控制权限.

    Role 是 coarse-grained 的权限管控机制, 比较典型的角色有 ROLE_ADMIN/ROLE_USER 等.
    Granted Authority 是 fine-grained 的权限管控机制, 比较典型的权限有: OP_DELETE_ACCOUNT/OP_CREATE_USER/OP_RUN_BATCH_JOB 等.
    从另一个角度看, Role 应该是面向业务的, 而 Granted Authority 应该是面向实现的.

    需要注意的是, 在Spring security 中, 如果在 hasRole 等函数中使用到 role 名称, 不能加上 ROLE_ 前缀, spring security 会自动强制加上该前缀. 而对于 hasAuthority 等函数, spring security 并不会为权限名加任何前缀.

    在一般的项目中, 我认为没有必要区分 role 和 Granted Authority, 可以将它们认为等同起来, 统一认为它们都是角色, 以减少概念的混淆.

    @PreAuthorize("hasAuthority('OP_DELETE_ACCOUNT')")
    @PreAuthorize("hasRole('ADMIN')")

    https://stackoverflow.com/questions/19525380/difference-between-role-and-grantedauthority-in-spring-security
    https://www.baeldung.com/spring-security-granted-authority-vs-role


    ===================================
    SpringBoot 集成 spring security
    ===================================
    项目需加上 security starter 包,

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    SpringBoot 使用 SecurityAutoConfiguration 导入 DefaultConfigurerAdapter 配置类, 该配置类完成了缺省的用户认证/和鉴权工作, 这包括:
    1. 自动配置一个内存用户, 账号为 user, 密码在程序启动后动态生成并打印出来.
    2. 忽略 /css/**, /js/**, /images/**, **/favicon.ico 静态文件的拦截
    3. 向 servletContext 注册 securityFilterChain
    另外, 在application.properties 文件中, 可以设置相关的配置项

    ecurity.user.name=user  #默认用户的账号名
    security.user.password=  #默认用户的密码, 不设定的话就动态生成
    security.user.role=USER  #默认的用户的角色
    security.enable-csrf=false #是否开启"跨站请求伪造", 默认关闭
    security.ignored= #用逗号隔开的无需拦截的路径

    SpringBoot 已经为我们做了很多与Spring Security的集成工作, 在实际项目中, DefaultConfigurerAdapter 肯定是不够的, 需要我们做的是, 像 DefaultConfigurerAdapter 一样开发一个配置类(比如名为 WebSecurityConfig), 该类也继承自 WebSecurityConfigurerAdapter 即可, 并分别实现两个configure 方法, 分别完成用户级的认证和http请求级的验证.


    下面就是一个空的 WebSecurityConfigurerAdapter 框架.

    @Configuration
    // @EnableWebSecurity //@EnableWebSecurity 可以省略
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
        @Override
        //设置用户级的认证机制
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        }
        
        @Override
        //设置http请求级的验证
        protected void configure(HttpSecurity http) throws Exception {
        }
    }


    ===================================
    Authentication 用户认证
    ===================================
    用户认证的方法签名如下:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    }

    ------------------------

    1.1 内存用户验证
    ------------------------

    @Configuration
    // @EnableWebSecurity //@EnableWebSecurity 可以省略
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //链式写法
            auth.inMemoryAuthentication()
            .withUser("user1").password("password1").roles("ADMIN")
            .and()
            .withUser("user2").password("password2").roles("USER");
    
            //常规写法
            auth.inMemoryAuthentication()
            .withUser("user3").password("password3").roles("ADMIN");
            auth.inMemoryAuthentication()
            .withUser("user4").password("password4").roles("USER");
    }
    
        @SuppressWarnings("deprecation")
        @Bean
        public NoOpPasswordEncoder passwordEncoder() {
            return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
        }
    
    //    @Bean
    //    public BCryptPasswordEncoder passwordEncoder() {
    //        return new BCryptPasswordEncoder();
    //    }
    }  //end of class

    auth.inMemoryAuthentication()用来创建一个基于内存的用户清单(包括账号名/密码/角色), 两个用户之间使用 .add() 链式方法连接.
    需要注意的是:
    1. spring security 角色名称会自动在角色名前加上 ROLE_ 前缀, 所以代码中的角色名不能以 ROLE_ 开头.
    2. 推荐的角色名全部为大写字母.
    3. 内存用户清单必须再提供一个 PasswordEncoder bean, 程序将使用该bean 进行密码验证, 这里使用了 NoOpPasswordEncoder, 还有 Md5PasswordEncoder, 在生产环境推荐使用 BCryptPasswordEncoder.
    使用BCryptPasswordEncoder后, 如果代码中是明文密码, 需要先做一下encode, 因为 Spring security 将使用encoding 后的密码进行验证. auth.inMemoryAuthentication().withUser("user1").password(passwordEncoder().encode("user1Pass")).roles("USER")


    ------------------------
    1.2 JDBC用户验证
    ------------------------

    @Configuration
    // @EnableWebSecurity //@EnableWebSecurity 可以省略
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
        @Autowired
        DataSource dataSource;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            String userQuery = "select username, password, enable from "
                    + "( select 'user1' username, 'password1' password, true enable ) t " + "where username=?";
    
            String roleQuery = "select username, rolename authority from"
                    + "(select 'user1' username,'ADMIN' rolename ) t" + " where username=?";
            auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery(userQuery)
                    .authoritiesByUsernameQuery(roleQuery);
        }
        
        @SuppressWarnings("deprecation")
        @Bean
        public NoOpPasswordEncoder passwordEncoder() {
            return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
        }                
    } //end of class

    auth.jdbcAuthentication()用来创建一个基于JDBC数据库用户验证, 需要提供两个查询, 一个是账号/密码查询(使用 usersByUsernameQuery 方法), 另一个是角色查询(使用 authoritiesByUsernameQuery 方法).


    ------------------------
    1.3 通用用户验证
    ------------------------
    实际项目中基本上是采用通用用户验证, 这种方式需要定义一个能完成用户检索的类, 该类需实现 UserDetailsService 接口, 只要实现一个 loadUserByUsername() 方法即可, 该方法的返回类型是 UserDetails 接口, 我们没有必要再定义一个专门的类去实现 UserDetails 接口, Spring Securtiy 已经提供了一个这样的类, 类名全称为 org.springframework.security.core.userdetails.User.

    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 使用自定义的 UserDetailsService 接口认证
            auth.userDetailsService(new MyUserDetailsService()); 
        }
    
        @SuppressWarnings("deprecation")
        @Bean
        public NoOpPasswordEncoder passwordEncoder() {
            return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
        }
    } //end of class
    
    class MyUserDetailsService implements UserDetailsService {
        private Map<String, User> userRepository = new HashMap<String, User>();
        
        //模拟真实的用户清单    
        public MyUserDetailsService() {
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority("Admin"));
            authorities.add(new SimpleGrantedAuthority("User"));
            User user1 = new User("user1", "password1", authorities);
            userRepository.put("user1", user1);
        }
    
        // 需要实现的方法  
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepository.get(username);
            return user;
        }
    }

    ===================================
    理解http 请求级验证代码中的链式调用
    ===================================

    // @formatter:off
    protected void configure(HttpSecurity http) throws Exception {
        /*
         * HttpSecurity对象支持来复合链式调用写法.
         * 比如 http.authorizeRequests() 可以加任意多二级 antMatchers()链式调用, 每一个 antMatchers() 以授权函数结尾,
         *                               二级 antMatchers() 之间无需 and() 连接.
         * 和 authorizeRequests() 同层次的还有 httpBasic()/sessionManagement()等, 多个一级函数的链式调用需使用 and() 连接.
         * */
    
        //一级函数
        //http.authorizeRequests();  //url 模式赋权
        //http.formLogin();          //登陆 form
        //http.httpBasic();          //Basic Authentication 设置
        //http.sessionManagement();  //session 管理
        //http.cors();               //cors 跨源资源共享
        //http.csrf()                //Cross Site Request Forgery 跨站域请求伪造
        
        //关于路径的配置,
        //应该是先配置具体的路径, 然后再配置宽泛的路径
        //antMatchers()
    
        http
           .authorizeRequests()
               // 对于/api 路径下的访问需要有 ROLE_ADMIN 的权限
              .antMatchers("/api/**").hasRole("ADMIN")
              // 对于/api2 路径下的访问需要有 ROLE_ADMIN或ROLE_DBA或ROLE_USER 的权限
              .antMatchers("/api2/**").access("hasRole('USER') or hasRole('ADMIN') or hasRole('DBA')")
               // 对于/guest 路径开放访问
              .antMatchers("/guest/**").permitAll()
               // 其他url路径之需要登陆即可.
               .anyRequest().authenticated()
               .and()
           //启用 basic authentication
          .httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthenticationEntryPoint())
               .and()
           //不创建 session
          .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    // @formatter:on
  • 相关阅读:
    三剑客之grep命令
    expect
    信号控制
    数组
    LaTex: Cetx +Winedit之文献引用---Elsevier模板
    vue系列--【animate.css、过滤器、组件基础】
    vue系列--【生命周期、侦听器watch、计算属性、jsonp解决跨域】
    vue系列--【动态样式、表单数据绑定、表单修饰符、事件处理、$set】
    vue系列--【vue核心、vue实例、指令】
    node系列--【socket.io框架】
  • 原文地址:https://www.cnblogs.com/harrychinese/p/SpringBoot_security_basics.html
Copyright © 2020-2023  润新知