初体验

  1. 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

spring-security会自动添加一个登陆页面

  • 用户 user
  • 密码 会在控制台打印

springSecurity基本原理

spring-security本质上是过滤器链

FilterSecurityInterceptor 是一个方法级的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//过滤器体
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.invoke(new FilterInvocation(request, response, chain));
}
//过滤器的执行体
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} else {
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}

// 判断之前过滤器的结果
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);

try {
//执行当前过滤器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
//执行下一个过滤器
super.afterInvocation(token, (Object)null);
}
}

ExceptionTranslationFilter异常转换过滤器

处理权限认证过程种抛出的异常

UsernamePasswordAuthenticationFilter 判断用户post请求种的用户名和密码

image-20220414120714351

基本

不使用springboot配置security

DelegatingFilterProxy

自定义用户名和密码匹配获取权限

获取用户数据

  1. 在配置文件中配置用户名和密码

    1
    2
    3
    4
    5
    spring:
    security:
    user:
    name: admin
    password: admin
  2. 通过配置类设置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //继承 WebSecurityConfigurerAdapter
    // 设置的密码必须加密,使用passwordEncoder相同的加密器

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String encode = passwordEncoder.encode("123");
    auth.inMemoryAuthentication().withUser("wangzhe")
    .password(encode)
    .roles("admin");
    }

    //必须设置加密器
    //exception:id null...
    @Bean
    PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }
    }


    配置类设置的密码必须加密,必须设置passwordEncoder

    1. 通过数据库查找来获取用户名和密码,需要实现UserDetailService 接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30

      @Configuration
      public class SecurityConfig extends WebSecurityConfigurerAdapter {

      @Autowired
      MyUserDetailService myUserDetailService;

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

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



      @Service
      public class MyUserDetailService implements UserDetailsService {
      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("roles");
      return new User("xiaomi", new BCryptPasswordEncoder().encode("123"),authorities);
      }
      }

自定义登录页面

  登录页面必须以post请求,表单的name必须设置为username,password。

  必须设置登录页面放行策略

  
1
2
3
4
5
6
7
8
9
10
11
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 登录页面
.loginProcessingUrl("/user/login") //表单登录按钮路径
.defaultSuccessUrl("/test/index") //登录成功的默认跳转路径
.and().authorizeHttpRequests()
.antMatchers("/hello","/user/login","/login.html").permitAll() //放行路径,需要添加登录页面到放行路径中
.anyRequest().authenticated()
.and().csrf().disable();//关闭csrf 防护
}

用户授权

  > 单权限

  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 登录页面
.loginProcessingUrl("/user/login") //表单登录按钮路径
.defaultSuccessUrl("/test/index") //登录成功的默认跳转路径
.and().authorizeHttpRequests()
.antMatchers("/hello","/user/login","/login.html").permitAll() //放行路径,需要添加登录页面到放行路径中
.antMatchers("/test/authority").hasAuthority("admin") //添加权限校验
.antMatchers("/test/authorities").hasAnyAuthority("admin,girl")
.anyRequest().authenticated()
.and().csrf().disable();//关闭csrf 防护
}

  > 多权限

  
1
2
.antMatchers("/test/authorities").hasAnyAuthority("admin","girl")

  **权限不足**

  
1
2
3
4
5
6
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Fri Apr 15 16:07:39 CST 2022
There was an unexpected error (type=Forbidden, status=403).
Forbidden

用户角色分配

  > 分配权限是 字符串需要在前面拼接 `ROLE_`

  
1
2
3
4
//单角色分配
.antMatchers("/test/authority").hasRole("producer")
.antMatchers("/test/authorities").hasAnyRole("producer","consumer")

自定义403 错误页面

security配置类中添加

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

注解使用

@Secured

指定访问目标 所需的 角色

注解可以被用在方法上,controller或者service层都可以

开启注解使用

1
2
3
//在启动类或者是配置类上添加 启动注解
@EnableGlobalMethodSecurity(securedEnabled = true)

访问目标上添加角色校验

1
2
3
4
5
6
@GetMapping("/users")
@ResponseBody
@Secured({"ROLE_admin", "ROLE_manager"})
public List<Map<String, Object>> getUserList() {
return userService.getUserList();
}

@PreAuthorize

可以在进入方法前 进行校验

开启注解支持

1
@EnableGlobalMethodSecurity(prePostEnabled = true)

在访问目标上添加角色or权限校验

1
2
3
4
5
6
@GetMapping("/user")
@ResponseBody
@PreAuthorize("hasAnyAuthority('admin') and hasAnyRole('ROLE_manager')")
public User getUser() {
return userService.getById(1);
}

@PostAuthorize

在方法执行后校验

开启注解支持

1
@EnableGlobalMethodSecurity(prePostEnabled = true )

在访问目标上添加角色or权限校验

1
2
3
4
5
6
@GetMapping("/user")
@ResponseBody
@PostAuthorize("hasAnyRole('ROLE_manager') and hasAnyAuthority('admin')")
public User getUser() {
return userService.getById(1);
}

@PostFilter

对方法的返回结果进行过滤

在访问目标上添加结果过滤

1
2
3
4
5
6
7
8
//filterObject 是返回List中的单个对象,可以使用方法,或者是.属性 
@GetMapping("/users")
@ResponseBody
@Secured({"ROLE_admin", "ROLE_manager"})
@PostFilter("filterObject.get('username') == 'admin'")
public List<Map<String, Object>> getUserList() {
return userService.getUserList();
}

@PreFilter

对方法的入参进行校验

用户注销

  1. security配置 登出路径登出成功页面

    1
    2
    http.logout().logoutUrl("/logout").logoutSuccessUrl("/logoutSuccess.html");
    需要放行logoutSuccess.html页面,但是只能有一个permitALL

自动登录

  1. 创建token-userinfo 表

    建表语句在org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl类中

    1
    2
    3
    4
    5
    CREATE TABLE persistent_logins (
    username VARCHAR ( 64 ) NOT NULL,
    series VARCHAR ( 64 ) PRIMARY KEY,
    token VARCHAR ( 64 ) NOT NULL,
    last_used TIMESTAMP NOT NULL)
  2. 配置数据源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //配置 PersistentTokenRepository 的数据源
    //在配置类中添加

    @Autowired
    DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    return jdbcTokenRepository;
    }
  3. configure(HttpSecurity http)中配置自动登录的数据库,token持续时间,userservice

    1
    2
    3
    http.and().rememberMe().tokenRepository(persistentTokenRepository())//设置自动登录,数据库操作对象
    .tokenValiditySeconds(60) //设置token的有效时间,单位秒
    .userDetailsService(userDetailsService())
  4. 在登录页面添加复选框:十天免登录

    1
    <input type="checkbox" name="remember-me"> 5分钟之内免登录
  5. 测试结果查看,客户端的cookie,数据库中的数据

    1
    2
    数据库信息
    admin A1m2uLrSTC3CRQ9IRzyIgQ== Ieotk5N33+/5I6MNODR3gQ== 2022-05-13 09:44:44

    客户端cookie信息

    image-20220513095136449

CSRF

CSRF简介

1
2
3
4
  CSRF(Cross-site request forgery)跨站请求伪造,也被称为"One Click Attack"或者Session Riding,通常缩写为CSRF或者
XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信
任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比
XSS更具危险性。

SpringSecurity 4之后

默认开启CSRF保护,会针对POST,PUT,PATCH,DELETE这些请求进行保护。

开启后,只能在同站进行访问,跨站是不能访问的。

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

csrf的实现在过滤器中

org.springframework.security.web.csrf.CsrfFilterspringSecurity 中的csrf功能时在这个过滤器中

开启csrf校验

  1. 在配置类中开启CSRF校验
  2. 需要在请求的时候传递_csrf的token

单点登录

补充

登录成功后的行为

  1. defaultSuccessUrl,successForwardUrl
1
http.formLogin().defaultSuccessUrl("/index",true).successForwardUrl("/index")
  1. http.successHandler(AuthenticationSuccessHandler successHandler)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
http.successHandler(AuthenticationSuccessHandler successHandler);
//配置
.successHandler(new authenticationSuccessHandler());
private static class authenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString(principal));
writer.flush();
writer.close();
}
}
//Lamda写法
.successHandler((req, resp, authentication) -> {
Object principal = authentication.getPrincipal();
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(principal));
out.flush();
out.close();
})

登录失败后的行为

  1. ```java
    .failureUrl(“/failure”) //跳转登录失败页面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    2. ```java
    //登录失败,处理方法,前后端分离时使用
    .failureHandler((req, resp, e) -> {
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write(e.getMessage());
    out.flush();
    out.close();
    });

未认证时访问其他页面是的行为

1
2
3
4
5
6
7
8
//这里主要是针对,前后端分离的情况,后台没有办法重定向。 
http.exceptionHandling().authenticationEntryPoint((req,resp,authException) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write("尚未登录,清闲登录");
writer.flush();
writer.close();
});

注销登录后返回json数据

1
2
3
4
5
6
7
8
9
10
11
http.logout()   .logoutUrl("/logout")
.logoutSuccessUrl("/logoutSuccess")
.logoutSuccessHandler((req,resp,auth) -> {
Object principal = auth.getPrincipal();
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write(new ObjectMapper().writeValueAsString(principal));
writer.write("登出成功");
writer.flush();
writer.close();
});

配置用户的方式

  1. 配置在内存中

    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
    .withUser("aoa")
    .password(passwordEncoder()
    .encode("123"))
    .roles("admin");
    }
  2. 配置UserDetailsService

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //1. 重写方法
    @Override
    protected UserDetailsService userDetailsService() {
    //在这里创建
    InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
    inMemoryUserDetailsManager.createUser(User.withUsername("bin").password(passwordEncoder().encode("123")).roles("bin").build());
    inMemoryUserDetailsManager.createUser(User.withUsername("sofm").password(passwordEncoder().encode("123")).roles("sofm").build());
    return inMemoryUserDetailsManager;
    }

    //2. 在configure(auth)中配置
    auth.userDetailsService((userName)-> {
    return null;
    });

权限继承

1
2
3
4
5
6
7
//新版本可能配置的字符串中使用 \n 分割   
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_bin");
return hierarchy;
}