spring-security-上手

spring-security-上手

起男 1,405 2021-06-11

spring-security-上手

概要

spring是非常流行和成功的java应用开发框架,spring security正是spring家族中的成员。spring security基于spinrg框架,提供了一套web应用安全性的完整解决方案

正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,web应用的安全性包括用户认证(authentication)和用户授权(authorization)两个部分,这两点也是spring security重要的核心功能

  • 用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统任务用户是否能登录
  • 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权去做某些事

特点

  • 和spring无缝整合
  • 全面的权限控制
  • 专门为web开发设计
    • 旧版本不能脱离web环境使用
    • 新版本对整个框架进行了分层抽取,分成了核心模块和web模块。单独引入核心模块就可以脱离web环
  • 重量级

shiro

apache旗下的轻量级权限控制框架

  • 轻量级:shiro主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好的表现
  • 通用性
    • 好处:不局限于web环境,可以脱离web环境使用
    • 缺陷:在web环境下一些特定的需求要手动编写代码制定

原理

springsecurity本质是一个过滤器链(很多过滤器)

主要过滤器

FilterSecurityInterceptor:是一个方法级的权限过滤器,基本位于过滤链的最底部

ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常

UsernamePasswordAuthenticationFilter:对/login的post请求做拦截,校验表单中用户名,密码

加载过程

  1. 使用springsecurity需要配置过滤器DelegatingFilterProxy
  2. DelegatingFilterProxy的doFilter中有initDelegate方法得到一个FilterChainProxy过滤器,然后执行FilterChainProxy的init方法
  3. FilterChainProxy的doFilter中有个doFilterInternal,方法里有List<Filter> filters = getFilters(firewallRequest);获取所有过滤器,然后进行加载

主要接口用于自定义开发

UserDetailsService:当什么也没有配置的时候,账号和密码是由spring security定义生成的。而在实际项目中账号密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑(查询数据库用户名密码的过程)

  • 创建类继承UsernamePasswordAuthenticationFilter,重写三个方法
  • 创建类实现UserDetailsService,编写查询数据库的过程,返回User对象(安全框架提供的)

PasswordEncoder:数据加密的接口,用于返回user对象里密码的加密

认证

在不做任何设置时系统自动提供用户名密码

用户名:user

密码:项目启动时生成

配置文件

spring:
  security:
    user:
      name: dqn
      password: dqn

配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //加密密码
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String password = bCryptPasswordEncoder.encode("123");
        //设置用户
        auth.inMemoryAuthentication()
                .withUser("zhangsan")
                .password(password)
                .roles("admin");
    }
    
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

查数据库

  1. 创建一个配置类,设置使用哪个UserDetailsService实现类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
  1. 编写实现类,返回User对象,对象里有用户名密码和权限
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");

        return new User("lisi",new BCryptPasswordEncoder().encode("123"),auths);
    }
}

授权

基于角色或授权进行访问控制

hasAuthority

如果当前的主体具有指定的权限,有则返回true

  1. 在配置类里设置当前访问地址有哪些权限

    .antMatchers("/test/index").hasAuthority("admin") //只有当前用户有admin权限才能访问
    
  2. 在UserDetailsService,把返回User对象设置权限

hasAnyAuthority

如果当前的主体有任何提供的角色的话(有其中之一),返回true

.antMatchers("/test/index").hasAnyAuthority("admin","admin1")//有其中之一就可以访问

hasRole

如果用户具备给定角色就允许访问,否则403

.antMatchers("/test/index").hasRole("role")

和Authority的区别是开头添加ROLE_

hasAnyRole

表示用户具备任何一个条件都可以访问

自定义没有权限页面

http
                .exceptionHandling()
                .accessDeniedPage("/unauth.html");

注解

@Secured

判断是否具有角色,另外需要注意的是这里匹配的字符串需要加前缀“ROLE_”

开启注解

@EnableGlobalMethodSecurity(securedEnabled = true)
@Secured("ROLE_role")//用户要有某个角色

@PreAuthorize

适合进入方法前的权限校验,可以将登录用户的roles/permissions参数传到方法中

开启注解

@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize("hasAnyAuthority('admin')")//是否有指定权限

@PostAuthorize

此注解使用不多,在方法执行后再进行权限验证,适合验证带有返回值的权限

开启注解

@EnableGlobalMethodSecurity(prePostEnabled = true)

@PreFilter

进入控制器之前对数据进行过滤

@PreFilter("filterObject%2==0")

@PostFilter

权限验证之后对数据进行过滤,留下指定的数据

表达式中的filterObject引用的方法返回值

@PostFilter("filterObject.username == 'admin1'")

注销

在配置类中添加退出映射地址

http
                .logout()
                .logoutUrl("/logout") //注销url 不需要自己写
                .logoutSuccessUrl("/test/add") //注销后跳转url
                .permitAll();

页面发送退出请求

<a href="/logout">退出</a>

记住我

原理

  1. 用户验证成功之后,会在浏览器已cookie形式保存字符串
  2. 再次访问,首先获取cookie信息,然后到服务器进行比对,如果查到对应信息,则认证成功

实现

  1. 修改配置类,注入数据源,配置操作数据库对象

        @Autowired
        private DataSource dataSource;
    
        @Bean
        PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            //注入数据源
            jdbcTokenRepository.setDataSource(dataSource);
            //自动创建表
            //jdbcTokenRepository.setCreateTableOnStartup(true);
            return jdbcTokenRepository;
        }
    
  2. 修改configure(HttpSecurity http)

    .and().rememberMe().tokenRepository(persistentTokenRepository()) //设置记住我
                    .tokenValiditySeconds(600) //设置token过期时间,单位秒
                    .userDetailsService(userDetailsService)//设置操作数据库
    
  3. 登录页面添加复选框,name固定remember-me

    <input type="checkbox" name="remember-me" />
    

csrf

跨站请求伪造,通常缩写为csrf或xsrf,是一种控制用户在当前已登录的web应用程序上执行非本意的操作的攻击方法。根跨站脚本(xss)相比,xss利用的是用户对指定网站的信任,csrf利用的是网站对用户网页浏览器的信任

跨站请求攻击,简单的说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾认为认证过的网站并运行一些操作。由于浏览器认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的

从spring security4.0开始,默认情况下会启用csrf保护,以防止csrf攻击应用程序,spring security csrf会针对patch、post、put和delete方法进行防护

使用

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

关闭csrf

http.csrf().disable(); //关闭csrf防护

原理

  1. 生成csrfToken保存到HttpSession或者Cookie中
  2. 之后请求需要携带csrfToken
  3. 然后服务器进行比较,如果一样则允许访问