前置知识

1、掌握Spring框架

2、掌握SpringBoot使用

3、掌握JavaWeb技术

SpringSecurity简介

概要

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

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

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

同款产品对比

SpringSecurity

Spring 技术栈的组成部分。

image-20240619143613134

通过提供完整可扩展的认证和授权支持保护你的应用程序

http://spring.io/projects/spring-security

SpringSecurity的特点:

  • 和 Spring 无缝整合。

  • 全面的权限控制。

  • 专门为Web 开发而设计。

    • 旧版本不能脱离Web 环境使用。

    • 新版本对整个框架进行了分层抽取,分成了核心模块和Web 模块。单独引入核心模块就可以脱离Web 环境。

  • 重量级。

Shiro

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

image-20240619143825995

特点:

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

Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。

相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security。

因此,一般来说,常见的安全管理技术栈的组合是这样的:

  • SSM + Shiro
  • SpringBoot/Spring Cloud + SpringSecurity

SpringSecurity入门程序

  • 创建SpringBoot工程

  • 引入spring-security的依赖

    • <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19

      - 编写controller类

      - ```java
      package com.raehp.controller;

      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;

      @RestController
      @RequestMapping("/test")
      public class HelloSecurity {

      @GetMapping("/hello")
      public String hello() {
      return "Hello Security!";
      }
      }
  • 访问localhost:8080/test/hello

    • image-20240619150001495

默认用户名为:user

密码为:每运行一次 随机生成一个密码image-20240619150042953

SpringSecurity基本原理

SpringSecurity 本质是一个过滤器链:

从启动是可以获取到过滤器链:

1
2
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil ter
org.springframework.security.web.context.SecurityContextPersistenceFilter org.springframework.security.web.header.HeaderWriterFilter org.springframework.security.web.csrf.CsrfFilter org.springframework.security.web.authentication.logout.LogoutFilter org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter org.springframework.security.web.savedrequest.RequestCacheAwareFilter org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter org.springframework.security.web.authentication.AnonymousAuthenticationFilter org.springframework.security.web.session.SessionManagementFilter org.springframework.security.web.access.ExceptionTranslationFilter org.springframework.security.web.access.intercept.FilterSecurityInterceptor

代码底层流程:重点看三个过滤器:

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

    • 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);
              }
          }
      
      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
      31
      32
      33
      34
      35
      36
      37
      38



      - super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。

      - fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。

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

      - ```java
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
      }

      private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
      try {
      chain.doFilter(request, response);
      } catch (IOException var7) {
      IOException ex = var7;
      throw ex;
      } catch (Exception var8) {
      Exception ex = var8;
      Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
      RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
      if (securityException == null) {
      securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
      }

      if (securityException == null) {
      this.rethrow(ex);
      }

      if (response.isCommitted()) {
      throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
      }

      this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);
      }
  • UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。

    • public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
              if (this.postOnly && !request.getMethod().equals("POST")) {
                  throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
              } else {
                  String username = this.obtainUsername(request);
                  username = username != null ? username : "";
                  username = username.trim();
                  String password = this.obtainPassword(request);
                  password = password != null ? password : "";
                  UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                  this.setDetails(request, authRequest);
                  return this.getAuthenticationManager().authenticate(authRequest);
              }
          }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18





      ## UserDetailService接口详解

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

      如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

      ```java
      package org.springframework.security.core.userdetails;

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

返回值:UserDetails

这个类是系统默认的用户“主体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();

// 表示获取密码
String getPassword();

// 表示获取用户名
String getUsername();

// 表示判断账户是否过期
boolean isAccountNonExpired();

// 表示判断账户是否被锁定
boolean isAccountNonLocked();

// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();

// 表示当前用户是否可用
boolean isEnabled();

UserDetails实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();

String getPassword();

String getUsername();

boolean isAccountNonExpired();

boolean isAccountNonLocked();

boolean isCredentialsNonExpired();

boolean isEnabled();
}

image-20240619160448835

以后我们只需要使用 User 这个实体类即可!

1
2
3
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
  • 方法参数username:
    • 表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无法接收。

步骤总结:

  • 创建类继承UsernamePassowrdAuthenticationFilter,重写三个方法
  • 创建类实现UserDetailService,编写查询数据过程,返回User对象

这个User对象是安全框架提供对象

PasswordEncoder接口讲解

1
2
3
4
5
6
7
8
9
10
// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);

// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);

// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}

接口实现类:

image-20240619161305432

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。

BCryptPasswordEncoder 是对bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认10.

方法演示:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test01() {
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String atguigu = bCryptPasswordEncoder.encode("atguigu");
// 打印加密之后的数据
System.out.println("加密之后数据:\t"+atguigu);
//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("atguigu", atguigu);
// 打印比较结果
System.out.println("比较结果:\t"+result);
}

SpringBoot对Security的自动配置

https://docs.spring.io/spring- security/site/docs/5.3.4.RELEASE/reference/html5/#servlet-hello

Web权限方案

设置Security密码

以下有三种方式可以配置username和password

  • 通过配置文件
  • 通过配置类
  • 自定义编写实现类

通过配置文件

创建application.properties文件

1
2
spring.security.user.name=raehp
spring.security.user.password=123456

运行启动类 访问controller 测试

通过配置类

创建securitConfig配置类 标注@Configuration注解 让其继承 WebSecurityConfigurerAdapter

并实现其中的configure方法

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
package com.raehp.config;

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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@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("raehp").password(password).roles("");
}

@Bean
// 因为configure中用到了PasswordEncoder来加密 所以这里要声明一个PasswordEncoder Bean否则会报错
public PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}

访问控制层,输入账户密码验证通过

自定义编写实现类

  • 第一步、创建配置类,设置使用哪个userDetailService实现类
  • 第二步、编写实现类,返回User对象,User对象有用户名密码和操作权限

创建配置类

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
package com.raehp.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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig2 extends WebSecurityConfigurerAdapter {
@Autowired //声明使用哪个userDetailsService
private UserDetailsService userDetailsService; // 指向service/MyUserService

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

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

编写实现类 创建 service/MyUserService 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.raehp.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("userDetailsService")
public class MyUserService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> authos = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("raehp",new BCryptPasswordEncoder().encode("123"),authos);
}
}

运行访问controller、测试通过

连接数据库

  • 引入相关依赖
  • 创建数据库和表
  • 创建对应的实体类
  • 整合mybatis-plus,创建mapper接口 实现BaseMapper接口
  • 在MyUserDetailService中调用mapper中的方法查询函数
  • 数据库配置

一、引入相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>

<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--lombok 用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

二、创建数据库和表

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
31
32
33
create table users(

id bigint primary key auto_increment, username varchar(20) unique not null, password varchar(100)
);
-- 密码 atguigu
insert into users values(1,'张san','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
-- 密码 atguigu
insert into users values(2,'李si','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');

create table role(
id bigint primary key auto_increment, name varchar(20)
);
insert into role values(1,'管理员'); insert into role values(2,'普通用户');

create table role_user(
uid bigint, rid bigint
);

insert into role_user values(1,1); insert into role_user values(2,2);

create table menu(
id bigint primary key auto_increment, name varchar(20),
url varchar(100), parentid bigint, permission varchar(20)
);

insert into menu values(1,'系统管理','',0,'menu:system'); insert into menu values(2,'用户管理','',0,'menu:user');

create table role_menu(
mid bigint, rid bigint
);
insert into role_menu values(1,1);
insert into role_menu values(2,1);
insert into role_menu values(2,2);

三、创建实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.raehp.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users {
public Integer id;
public String username;
public String password;
}

四、创建mapper接口使其继承BaseMapper接口

1
2
3
4
5
6
7
8
9
package com.raehp.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.raehp.pojo.Users;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<Users> {
}

五、在MyUserDetailService中调用mapper中的方法查询函数

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
31
32
33
34
35
36
package com.raehp.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.raehp.mapper.UserMapper;
import com.raehp.pojo.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service("userDetailsService")
public class MyUserService implements UserDetailsService {

@Autowired
public UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users user = userMapper.selectOne(wrapper);
if (user == null) { //如果是空抛出异常
throw new UsernameNotFoundException("用户名不存在");
}

List<GrantedAuthority> authos = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),authos);
}
}

六、数据库配置

1
2
3
4
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/xxx?serverTimezone=GMT%2B8
spring.datasource.username=xxx
spring.datasource.password=xxx

自定义登录页面

  • 编写配置类
  • 创建相关文件

第一步、在配置类实现相关配置

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") // 登录访问路径,即表单提交时 提交到某个controller,这个路径不需要我们自己配置 security会帮我们配置好
.defaultSuccessUrl("/test/index").permitAll() // 登录成功后,跳转路径
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置那些路径可以直接访问,不需要认证
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf防护 类似于跨域
}

第二步、创建相关的页面 static/login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username">
<br>
密码:<input type="password" name="password">
<br>
<input type="submit" name="login">
</form>
</body>
</html>
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
package com.atguigu.securitydemo1.controller;

import com.atguigu.securitydemo1.entity.Users;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequestMapping("/test")
public class TestController {

@GetMapping("hello")
public String hello() {
return "hello security";
}

@GetMapping("index")
public String index() {
return "hello index";
}
}

第三步、访问localhost:8081/test/hello 不会被拦截

访问localhost:8081/test/index 会跳转到login.html 页面 输入账户密码 登陆成功后 会访问到 /test/index

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

hasAuthority方法

如果当前的主题具有指定的权限,则返回true,否则返回false(只针对某一个权限)

1
2
3
4
5
6
7
1. 在配置类设置当前访问地址有哪些权限
// 当前登录用户,只有具有admin权限才可以访问这个路径
.antMatchers("/test/index").hasAuthority("admin")

2. 在UserDetatilsService中,给返回的User对象设置权限
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

没有权限 访问后会返回(type = “forbidden” status=”403”)

hasAnyAuthority方法

如果当前的主题有任何提供的角色(给定的作为一个逗号分割的字符串列表)的话,返回true

1
2
3
4
5
6
7
1. 在配置类设置当前访问地址有哪些权限
// 当前登录用户,只有admin,manager权限才可以访问这个路径
.antMatchers("/test/index").hasAnyAuthority("admin,manager")

2. 在UserDetatilsService中,给返回的User对象设置权限
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

hasRole方法

如果用户具备给定角色就允许访问,否则出现403,如果当前主体具有指定的角色,则返回true

1
2
3
4
5
6
7
1. 在配置类设置当前访问地址那些角色可以访问
// 当前请求要sales角色才能访问
.antMatchers("/test/index").hasRole("sales")

2. 在UserDetatilsService中,给返回的User对象设置权限
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sales");

注意:这里给User -> role权限时 要加 ROLE_ 这是因为源码中自动帮我们添加了

image-20240620181954110

hasAnyRole方法

用户具备任意一个条件即可访问

1
2
3
4
5
6
7
1. 在配置类设置当前访问地址那些角色可以访问
// 只要其中一个角色符合即可访问
.antMatchers("/test/index").hasAnyRole("sales,manager")

2. 在UserDetatilsService中,给返回的User对象设置权限
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_manager");

自定义403页面

1
2
3
4
5
6
    在自定义配置中配置
// 配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html"); // /unauth.html为自定义的403页面
http.formLogin() // 自定义自己编写的登录页面
....;
}

注解使用

@Secured

用户具有某个角色,可以访问方法

  • 在启动类(或配置类)上添加@EnableGlobalMethodSecurity(securedEnabled=true)
  • 在controller上添加@Secured 注解设置角色
  • userDetailService方法设置当前登录用户角色

第一步、@EnableGlobalMethodSecurity(securedEnabled=true)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.atguigu.securitydemo1;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@SpringBootApplication
@MapperScan("com.atguigu.securitydemo1.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true) // 可以加在启动类上,也可以加在配置类上
public class Securitydemo1Application {
public static void main(String[] args) {
SpringApplication.run(Securitydemo1Application.class, args);
}
}

第二步、@Secured

1
2
3
4
5
@GetMapping("update")
@Secured({"ROLE_sale","ROLE_manager"})
public String update() {
return "hello update";
}

第三步、userDetailService设置登录用户权限

1
2
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_manager");

@PreAuthorize

方法执行之前校验

  • 在启动类(或配置了)上添加@EnableGlobalMethodSecurity(prePostEnabled = true)
  • 在controller方法上添加PreAuthorize方法,可以将登录用户的 roles/permissions 参数传到方法中。

第一步、@EnableGlobalMethodSecurity(prePostEnabled = true)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.securitydemo1;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@SpringBootApplication
@MapperScan("com.atguigu.securitydemo1.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class Securitydemo1Application {
public static void main(String[] args) {
SpringApplication.run(Securitydemo1Application.class, args);
}
}

第二步、@PreAuthorize

1
2
3
4
5
@GetMapping("update2")
@PreAuthorize("hasAnyAuthority('admin')")
public String update2() {
return "hello update2";
}

@PostAuthorize

方法执行后校验

  • 在启动类(或配置了)上添加@EnableGlobalMethodSecurity(prePostEnabled = true)
  • 在controller方法上添加@PostAuthorize注解,注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限.

第一步、@EnableGlobalMethodSecurity(prePostEnabled = true)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.securitydemo1;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@SpringBootApplication
@MapperScan("com.atguigu.securitydemo1.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class Securitydemo1Application {
public static void main(String[] args) {
SpringApplication.run(Securitydemo1Application.class, args);
}
}

第二步、@PostAuthorize

1
2
3
4
5
6
@GetMapping("update3")
@PostAuthorize("hasAnyAuthority('admins')")
public String update3() {
System.out.println("我是方法执行后校验的!");
return "hello update3";
}

当我们访问这个请求并且提交后 会出现以下情况:

image-20240622153659000

没有权限访问,但是我们打开控制台发现 方法已经执行完毕

image-20240622153650480

@PostFilter

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

  • 表达式中的 filterObject 引用的是方法返回值List 中的某一个元素
1
2
3
4
5
6
7
8
9
@GetMapping("getAll")
@PostAuthorize("hasAnyAuthority('admin')")
@PostFilter("filterObject.username == 'admin1'") // 代表只返回username是admin1的数据
public List<Users> getAllUser(){
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(11,"admin1","6666"));
list.add(new Users(21,"admin2","888"));
return list;
}

结果如下:

image-20240622154409760

@PreFilter

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

1
2
3
4
5
6
7
8
9
10
@GetMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_管理员')")
@PreFilter(value = "filterObject.id%2==0") // 返回id能被2整除的数据
@ResponseBody
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo> list) {
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}

用户注销

创建一个登录成功的页面 success.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功!
<a href="/logout">退出</a>
</body>
</html>

在securityConfig中添加如下配置

1
2
// 退出url,退出成功后跳转的页面
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();

更新securityConfig 中登录成功后跳转的页面为success.html

1
.defaultSuccessUrl("/success.html").permitAll() // 登录成功后,跳转路径

类似于浏览器中的session

当我们登陆成功后,可以访问别的页面,但是当我们退出后就不能访问其他页面了

退出之前:

image-20240622160958487

退出之后:

image-20240622161027483

基于数据库设置记住我

第一步、创建数据库表(也可以不创建,设置配置文件时 可以帮我们创建)

1
2
3
4
5
6
7
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

第二步、设置数据库配置文件

1
2
3
4
5
#mysql数据库配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/xxx?serverTimezone=GMT%2B8
spring.datasource.username=xxx
spring.datasource.password=xxx

第三步、在安全配置类中编写数据库配置类

1
2
3
4
5
6
7
8
9
10
11
12
// 注入数据源
@Autowired
private DataSource dataSource;

// 自定义PersistentTokenRepository容器
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// jdbcTokenRepository.setCreateTableOnStartup(true); 自动帮我们创建表,第一次执行会创建,后面执行要删掉
return jdbcTokenRepository;
}

第四步、修改安全配置类

1
2
3
4
5
6
7
8
9
10
11
http.formLogin()
....
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) // 设置有效时常,单位秒
.userDetailsService(userDetailsService)
--------------------------------------------------------------------------------------
//也可以写为
http.rememberMe()
.tokenRepository(tokenRepository)
.tokenValiditySeconds(60) // 设置有效时常,单位秒
.userDetailsService(usersService);

第五步、页面中设置记住密码复选框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username">
<br>
密码:<input type="password" name="password">
<br>
<!--这里的name必须是remember-me-->
<input type="checkbox" name="remember-me" title="记住密码">
<br>
<input type="submit" name="login">
</form>
</body>
</html>

测试 查询数据库

image-20240622164854450

CSRF

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已

登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

从 Spring Security 4.0 开始,默认情况下会启用CSRF 保护,以防止CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和DELETE 方法进行防护。

参考尚硅谷**==SpringSecurity-P19 CSRF==**

SpringSecurity微服务权限方案

什么是微服务

微服务由来

微服务最早由Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

微服务的优势

(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。

(2)微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql等),数据库也是单个模块对应自己的数据库。

(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。

微服务的本质

(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过 程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。

(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。

微服务认证于授权实现思路