天天看點

spring security使用總結(含csrf和remember)

作者:歌罷江南岸

概述

Spring Security是为Java应用程序提供身份验证和授权 。

一般Web应用的需要进行认证和授权。

认证(Authentication):验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。

​ 授权(Authorization):经过认证后判断当前用户是否有权限进行某个操作(访问某个controller层的方法)。

关键接口

UserDetailsService

可以看做是根据用户名查询数据库或者从内存总获取密码,权限,角色等信息。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}           

PasswordEncoder

进行密码加密和匹配

public interface PasswordEncoder {
    String encode(CharSequence var1);
    boolean matches(CharSequence var1, String var2);
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}           

在passwordEncoder的实现类中,BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器。BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过在构造函数中传入strength 控制加密强度,默认 10。

strength的范围在[4,31],可以在BCryptPasswordEncoder 中可以看到,不同springboot有可能不同。

public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
        this.logger = LogFactory.getLog(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.version = version;
            this.strength = strength == -1 ? 10 : strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }           

设置用户名和密码

  1. application.yaml
server:
  port: 9000
spring:
  security:
    user:
      name: user
      password: 123           

这种环境下不能配置WebSecurityConfigurerAdapter的实现类 否则配置用户名和密码无效

2. 在内存中设置

spring security中默认登录拦截uri是/login

package com.kj.config;

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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        注意这里需要密码需要加密 同时需要设置roles
//        否则会抛出异常 java.lang.IllegalArgumentException: Cannot pass a null GrantedAuthority collection
        auth.inMemoryAuthentication().withUser("root").password(passwordEncoder.encode("123")).roles("");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        开启表单登录验证
        http.formLogin();
//      这里需要设置处理login接口之外的其他请求需要认证
        http.authorizeRequests().antMatchers("/login").permitAll().anyRequest().authenticated();
//        暂时关闭csrf 否则在登录是需要传递其他参数
        http.csrf().disable();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
           

这里csrf默认开启,同时springsecurity默认不开启认证,所有需要针对除了login接口外的其他接口都需要认证。当然,对于某些公共的静态资源这里也可以设置,为了演示简化了设置。

也可以同时设置多个内容中的用户

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("admin").password(passwordEncoder.encode("123")).roles("admin").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password(passwordEncoder.encode("456")).roles("root").build());
        auth.userDetailsService(inMemoryUserDetailsManager).passwordEncoder(passwordEncoder);
    }           

3.自定义UserDetailService

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.core.userdetails.UsernameNotFoundException;

import java.util.ArrayList;
import java.util.List;

public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//        查询数据库 获取用户的密码,角色,权限等信息 填充
        String pwd="";
        String[] roles;
        String[] authorities;
        List<GrantedAuthority> auths=new ArrayList<>();
        for (String role : roles) {
            auths.add(new SimpleGrantedAuthority("ROLE_"+role);
        }
        for (String authority : authorities) {
            auths.add(new SimpleGrantedAuthority(authority));
        }
        return new User(s,pwd,auths);
    }
}           

WebSecurityConfigurerAdapter配置自定义的UserDetailService

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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(passwordEncoder);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin();
        http.authorizeRequests().antMatchers("/login").permitAll();
        http.csrf().disable();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}           

角色和权限

角色和权限也是 Spring Security 中所采用的授权模型。GrantedAuthority 对象代表的就是一种权限对象,是一个接口,它既可以看做是权限,同时也可以看做是角色,需要添加ROLE_前缀。代码如下:

public interface GrantedAuthority extends Serializable {
    String getAuthority();
}           

创建权限

对于root用户具有了create,delete权限

UserDetails user = User.withUsername("root")
     .password("123456")
     .authorities("create", "delete")
     .build();           

创建角色

UserDetails user = User.withUsername("yn")
      .password("123456")
      .authorities("ROLE_ADMIN")
      .build();           

创建角色和权限

List<GrantedAuthority> auths= AuthorityUtils.commaSeparatedStringToAuthorityList("test,ROLE_normal");
User user=new User("test",passwordEncoder.encode("789"),auths);           

角色继承

@Bean
RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
    hierarchy.setHierarchy("ROLE_admin > ROLE_user");
    return hierarchy;
}           

注意,在配置时,需要给角色手动加上 ROLE_ 前缀。上面的配置表示 ROLE_admin 自动具备 ROLE_user 的权限。如果有多个\n进行分割

实际中可以考虑使用权限组的形式来代替这种继承

角色和权限校验

api 作用
anonymous 允许匿名访问
authenticated 允许认证用户访问
denyAll 无条件禁止一切访问
hasAnyAuthority 允许具有任一权限的用户进行访问
hasAnyRole 允许具有任一角色的用户进行访问
hasAuthority 允许具有特定权限的用户进行访问
hasIpAddress 允许来自特定 IP 地址的用户进行访问
hasRole 允许具有特定角色的用户进行访问
permitAll 无条件允许一切访问

api式校验

eg:

@Override
    protected void configure(HttpSecurity http) throws Exception 
        http.formLogin();
//      这里Role不用加前缀
        http.authorizeRequests().antMatchers("/login").permitAll()
                .antMatchers("/port").hasRole("admin");//
        http.csrf().disable();
    }           

注解校验

@Secured

只能用于权限用户具有某个权限(角色(需要加前缀)或者权限)才能访问该方法,首先需要在主启动类上开启(),然后在对应controller方法上开启

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class,args);
    }
}


    @GetMapping("/port")
    @Secured("ROLE_root")
    public String getPort(){
        return this.port;
    }           

PreAuthorize

方法之前检验

主启动类上添加@EnableGlobalMethodSecurity(prePostEnabled =true)

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class,args);
    }
}
    @GetMapping("/port")
    @PreAuthorize("hasRole('root')")
    public String getPort(){
        return this.port;
    }

           

PostAuthroize

方法之后校验

主启动类上添加@EnableGlobalMethodSecurity(prePostEnabled =true),内部调用之前的api

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class,args);
    }
}
    @GetMapping("/port")
    @PostAuthorize("hasRole('root')")
    public String getPort(){
        return this.port;
    }
           

PreFilter

进入控制器之前对数据进行过滤,要求传入的必须是collection或者是数组 ,迭代时元素别名为filterObject

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class,args);
    }
}

//student中id为偶数的保留
 @GetMapping("/port")
    @PreFilter(value = "filteObject.id%2==0")
    public String getPort(List<Student> students){
        return this.port;
    }           

PostFilter

权限验证之后对数据进行过滤

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled =true)
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class,args);
    }
}

//student中id为偶数的返回
 @GetMapping("/port")
    @PreFilter(value = "filteObject.id%2==0")
    public List<Student> getPort(){
        return null;
    }           

登出

退出登录后需要设置清除cookie,session,认证信息 这些都是默认的,可以不用配置。

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin();
        http.authorizeRequests().antMatchers("/login").permitAll();
//设置退出登录的接口以及方法,默认是get,同时可以设置登出成功回调
        http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout", "DELETE"))
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//                前后端分离 中可以通过HttpServletResponse返回 json信息
                    }
                });
        http.csrf().disable();
    }           

自定义访问界面

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //    自定义登陆界面和成功后的页面
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.formLogin()
                .loginPage("/index")
                .loginProcessingUrl("/login") //和表单里的action属性要一样 表单一定是post请求
                .defaultSuccessUrl("/success") //设置了成功和失败的URL 就跳转到原来要访问的地址
                .failureForwardUrl("/fail")
            	.passwordParameter("pwd") //指定表单里的属性名
                .usernameParameter("uname");

        http.authorizeRequests().antMatchers("/login").permitAll()
                        .anyRequest().authenticated();
        http.csrf().disable();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        String encode = passwordEncoder().encode("123");
        auth.inMemoryAuthentication().withUser("user").password(encode).roles("admin");
    }
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
           

登录成功和失败的URL

在 Spring Security 中,和登录成功重定向 URL 相关的方法有两个:

  • defaultSuccessUrl(url)(如果一开始访问的是登录接口,会跳转到url中,否则一开始访问的是其他接口,就会跳转到对应的接口)
  • successForwardUrl(url) (不管登录前访问的是什么接口,登录成功后都跳转到指定的URL)

失败的URL

与登录成功相似,登录失败也是有两个方法:

  • failureForwardUrl
  • failureUrl

这两个方法在设置的时候也是设置一个即可。failureForwardUrl 是登录失败之后会发生服务端跳转,failureUrl 则在登录失败之后,会发生重定向。

在前后端分离的项目中,页面调整是由前端控制的,相对来说用的少。

一些回调接口

接口

常用的一些扩展接口

  • AuthenticationSuccessHandler(登录成功处理)
  • AuthenticationFailureHandler(登录失败处理)
  • logoutSuccessHandler(注销成功处理)
  • logHandler(注销处理)
  • AccessDeniedHandler(访问拒绝处理)
  • AuthenticationEntryPoint(身份验证入口点失败处理)

AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常

AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin();
        http.authorizeRequests().antMatchers("/login").permitAll();
        http.formLogin().successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//                登录成功对调
            }
        });
        http.formLogin().failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//                登录失败回调
            }
        });
        http.logout().logoutSuccessHandler(new LogoutSuccessHandler() {
            @Override
            public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//               登出成功回调
            }
        });
//        登出处理
        http.logout().addLogoutHandler(...)
        http.csrf().disable();
    }           

csrf

csrf是指用户对浏览器的信任,第三方,利用一些技术手段,欺骗用户访问,认证过的链接,认证过,就会携带对应的cookie,对用户恶意攻击。

解决方案之一

令牌同步模式

这是目前主流的CSRF 攻击防御方案。具体的操作方式就是在每一个HTTP请求中,除了默认自动携带的Cookie 参数之外,再提供一个安全的、随机生成的宇符串,我们称之为CSRF令牌。这个CSRF令牌由服务端生成,生成后在HtpSession中保存一份。当前端请求到达后,将请求携带的CSRF令牌信息和服务端中保存的令牌进行对比,如果两者不相等,则拒绝掉该HITTP请求。

注意:考虑到会有一些外部站点链接到我们的网站,所以我们要求请求是幂等的,这样对子HEAD、OPTIONS、TRACE等方法就没有必要使用CSRF令牌了,强行使用可能会导致令牌泄露!

spring security默认开启csrf,不用配置。

前后端分离项目中,一开始应该发送两次请求,第一次会获取csrf令牌 保存在cookie里,之后按照一定方式组装csrf令牌,然后提交。

cookie里key: XSRF-TOKEN           

前后端分离的组装方式

value为token值

1 json格式请求

//请求题里添加
_csrf:value           

2 http-header中添加

//value从cookie中获取
X-XSRF-TOKEN:value           

remember-me

基本配置

记住我的功能实现思路基本是基于token,同时要考虑token是否过期的情况,以及是否更新。

spring security里的token里保留了用户名,过期时间,还有其他的一些信息,返回的是经过Base64编码后的token。

token的生成需要key,key 默认值是一个 UUID 字符串,这样会带来一个问题,就是如果服务端重启,这个 key 会变,这样就导致之前派发出去的所有 remember-me 自动登录令牌失效,所以,我们可以指定这个 key

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .rememberMe()
            .key("salt")
            .and()
            .csrf().disable();
}           

如果要通过表单传递rememberMe key为remember-me

<div> Remember Me:<input type="checkbox" name="remember-me" value="true"/> </div>           

token持久化

token存储默认是基于内存的,服务器如果重启,信息就会丢失。token需要持久化在数据库里。

保存Token立牌的类是PersistentRememberMeToken

public class PersistentRememberMeToken {
	private final String username;
	private final String series;
	private final String tokenValue;
	private final Date date;
}           

对应的SQL脚本为

CREATE TABLE `persistent_logins` 
  `username` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `series` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;           

有关存储的类是JdbcTokenRepositoryImpl

相关的配置

数据库

引入mysql的驱动和在application.yaml里配置数据库地址

<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.24</version>
        </dependency>           

application.yaml

server:
  port: 9000
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1/sec?serverTimezone=UTC&useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
           

security配置

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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

import javax.sql.DataSource;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    DataSource dataSource;

    @Bean
    JdbcTokenRepositoryImpl jdbcTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(new UserDetailsServiceImpl()).passwordEncoder(passwordEncoder);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin();
        http.authorizeRequests().antMatchers("/login").permitAll();
        http.rememberMe().key("salt").tokenRepository(jdbcTokenRepository());
        http.csrf().disable();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}           

二次校验

为了让用户使用方便,我们开通了自动登录功能,但是自动登录功能又带来了安全风险,一个规避的办法就是如果用户使用了自动登录功能,我们可以只让他做一些常规的不敏感操作,例如数据浏览、查看,但是不允许他做任何修改、删除操作,如果用户点击了修改、删除按钮,我们可以跳转回登录页面,让用户重新输入密码确认身份,然后再允许他执行敏感操作。

例如我现在提供三个访问接口:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
    @GetMapping("/admin")
    public String admin() {
        return "admin";
    }
    @GetMapping("/rememberme")
    public String rememberme() {
        return "rememberme";
    }
}           
  1. 第一个 /hello 接口,只要认证后就可以访问,无论是通过用户名密码认证还是通过自动登录认证,只要认证了,就可以访问。
  2. 第二个 /admin 接口,必须要用户名密码认证之后才能访问,如果用户是通过自动登录认证的,则必须重新输入用户名密码才能访问该接口。
  3. 第三个 /rememberme 接口,必须是通过自动登录认证后才能访问,如果用户是通过用户名/密码认证的,则无法访问该接口。

接口的访问配置如下

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/rememberme").rememberMe()
            .antMatchers("/admin").fullyAuthenticated()
            .anyRequest().authenticated()
            .and()
            .formLogin();
}