学习 SpringSecurity 这一篇就够了

本文详细介绍了Spring Security的基础知识,包括它在Web安全中的作用、认证与授权流程、过滤器原理、用户登录与权限管理、自定义登录页面、403错误处理以及微服务环境下的权限方案。通过实例展示了如何配置自定义登录页面、用户认证、权限授权、CSRF保护,并探讨了微服务场景下的认证授权过程和数据模型。
摘要由CSDN通过智能技术生成


一、简介

1.1、摘要

中文文档

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

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

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

1.2、对比

1、SpringSecurity

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

2、Shiro

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

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

  • SSM + Shiro
  • Spring Boot/Spring Cloud + Spring Security

以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。


二、入门案例

1、新建SpringBoot工程 securitydemo12.2.1.RELEASE

2、POM

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

3、新建控制层

@RestController
public class TestController {

    @GetMapping("/hello")
    public String test() {
        return "hello World";
    }

}

4、启动并访问 localhost:8080/hello 进入如下验证页面

在这里插入图片描述
默认用户名为 user,密码为如下默认密码
在这里插入图片描述


三、基本原理

3.1、过滤器

本质是一个过滤器链,有很多过滤器如以下

  • FilterSecurityInterceptor:方法级的权限过滤器, 基本位于过滤链的最底部
  • ExceptionTranslationFilter:异常过滤器,用来处理在认证授权过程中抛出的异常
  • UsernamePasswordAuthenticationFilter:对/login 的 POST 请求做拦截,校验表单中用户名,密码。

3.2、如何加载

过滤器是如何加载的

1、Springboot自动配置 DelegatingFilterProxy 过滤器
在这里插入图片描述
2、获取 FilterChainProxy
在这里插入图片描述
3、FilterChainProxy
在这里插入图片描述
4、获取所有的过滤器
在这里插入图片描述
在这里插入图片描述


3.3、接口

1、UserDetailsService 接口讲解

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

2、PasswordEncoder 接口讲解

对密码进行加密

@Test
public void test01(){
	// 创建密码解析器
	BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
	// 对密码进行加密
	String laptoy = bCryptPasswordEncoder.encode("laptoy");
	// 打印加密之后的数据
	System.out.println("加密之后数据:\t"+laptoy);
	//判断原字符加密后和加密之前是否匹配
	boolean result = bCryptPasswordEncoder.matches("laptoy", laptoy);
	// 打印比较结果
	System.out.println("比较结果:\t"+result);
}

四、web权限方案

4.1、用户认证

设置登录的用户名和密码

1、配置文件方式

spring:
  security:
    user:
      name: laptoy
      password: laptoy

2、配置类方式

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String passwd = bCryptPasswordEncoder.encode("laptoy");
        auth.inMemoryAuthentication().withUser("laptoy").password(passwd).roles("admin");
    }

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

3、自定义编写实现类方式(常用)

@Service
public class MyUserDetailService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    
    	// 赋予当前用户权限
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
		
		// 这里的密码可以通过注入 mapper层 从数据库中查找
        return new User(
                "laptoy",
                new BCryptPasswordEncoder().encode("laptoy"),
                auths);
    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

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

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

4.2、整合MybatisPlus

1、POM、YML

<!--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>
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?serverTimezone=GMT%2B8
    username: root
    password: root

2、创建数据库 security

create table users (
	id bigint primary key auto_increment,
	username varchar(20) unique not null,
	password varchar(100)
);
insert into users values(1,'laptoy','laptoy');

3、实体类

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

4、数据交互层

public interface UserMapper extends BaseMapper<Users> {
}

5、业务层

@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper<Users> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        Users users = userMapper.selectOne(wrapper);

        if (users == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
		
		// 授予权限(后面有用)
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

        return new User(
                users.getUsername(),
                new BCryptPasswordEncoder().encode(users.getPassword()),
                auths);
    }
}

6、配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

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

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

7、控制层

@RestController
public class TestController {

    @GetMapping("/hello")
    public String test() {
        return "hello World";
    }
}

8、启动类

@MapperScan("com.laptoy.mapper")
@SpringBootApplication
public class SecurityDemo1Application {
    public static void main(String[] args) {
        SpringApplication.run(SecurityDemo1Application.class, args);
    }
}

4.3、自定义用户登录页面

1、配置类配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MyUserDetailService myUserDetailService;

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义登录页面
        http.formLogin()
                .loginPage("/login.html")           // 自定义页面设置
                .loginProcessingUrl("/login") 		// 登录表单提交路径
                .defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
       	
       	// 权限认证
        http.authorizeRequests()
                .antMatchers("/", "/hello").permitAll() // 设置忽略的路径
                .anyRequest().authenticated()       // 设置所有路径都需认证(不包括忽略的路径)
                
        // 关闭csrf保护功能        
		http.csrf().disable();

    }
}

2、resource/static/login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    <input type="submit" value="login">
</form>

</body>
</html>

3、控制层

@RestController
public class TestController {

    @GetMapping("/hello")
    public String test() {
        return "hello World";
    }

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

4、测试

1、/hello 可以直接访问
2、可以配置自定义登录页面
3、登录成功跳转到 /index
在这里插入图片描述


4.4、用户授权

1、hasAuthority 方法

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

配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
	    // 自定义登录页面
	    http.formLogin()
	            .loginPage("/login.html")          // 自定义页面设置
	            .loginProcessingUrl("/login")	   // 登录访问路径
	            .defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
	         
	    // 权限认证
	    http.authorizeRequests()
	            .antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
	            
	        $   .antMatchers("/index").hasAuthority("admin") // 当前登录用户只有具有admin权限才能访问
	            
	            .anyRequest().authenticated()      // 设置所有路径都需认证(不包括忽略的路径)
	            
	    // 关闭csrf保护功能        
		http.csrf().disable();
	}
}

当前登录用户只有具有admin权限才能访问

.antMatchers("/index").hasAuthority("admin")

接口

@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper<Users> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        Users users = userMapper.selectOne(wrapper);

        if (users == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
		
		// 授予admin权限
    $   List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

        return new User(
                users.getUsername(),
                new BCryptPasswordEncoder().encode(users.getPassword()),
                auths);
    }
}

授予admin权限

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

若当前登录用户无对应权限,返回 403状态码


2、hasAnyAuthority 方法

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

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定义登录页面
    http.formLogin()
            .loginPage("/login.html")           // 自定义页面设置
            .loginProcessingUrl("/login") 		// 登录访问路径
            .defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
            
    http.authorizeRequests()
            .antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
            //.antMatchers("/index").hasAuthority("admin")  // 当前登录用户只有具有admin权限才能访问
        $   .antMatchers("/index").hasAnyAuthority("admin", "root") // 有其中一个权限就能访问
        
            .anyRequest().authenticated()      // 设置所有路径都需认证(不包括忽略的路径)
    
    http.csrf().disable();
}
.antMatchers().hasAnyAuthority("admin", "root") // 有其中一个权限就能访问

3、hasRole 方法

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

源码

private static String hasRole(String role) {
    Assert.notNull(role, "role cannot be null");
    if (role.startsWith("ROLE_")) {
        throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
    } else {
        return "hasRole('ROLE_" + role + "')";
    }
}    

源码显示拼接用户名时为 ROLE_xxx

return "hasRole('ROLE_" + role + "')";

配置类添加角色授权

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定义登录页面
    http.formLogin()
            .loginPage("/login.html")          // 自定义页面设置
            .loginProcessingUrl("/login") // 登录访问路径
            .defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
            
    http.authorizeRequests()
            .antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
            
       $    .antMatchers("/index").hasRole("admin") 
            
            .anyRequest().authenticated()      // 设置所有路径都需认证(不包括忽略的路径)
            
    http.csrf().disable();
}

此时接口方法授予权限必须为

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");

4、hasAnyRole

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

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 自定义登录页面
    http.formLogin()
            .loginPage("/login.html")          // 自定义页面设置
            .loginProcessingUrl("/login") // 登录访问路径
            .defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
            
    http.authorizeRequests()
            .antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
            
     $      .antMatchers("/index").hasAnyRole("admin", "root") 
            
            .anyRequest().authenticated()      // 设置所有路径都需认证(不包括忽略的路径)
            
    http.csrf().disable();
}

4.5、自定义403页面

1、新建页面 /static/unauth.html

<body>
<h1>抱歉,你没有权限访问</h1>
</body>

2、配置类

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 配置没有权限访问跳转自定义页面
    http.exceptionHandling().accessDeniedPage("/unauth.html");
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 配置没有权限访问跳转自定义页面
    http.exceptionHandling().accessDeniedPage("/unauth.html");
    // 自定义登录页面
    http.formLogin()
            .loginPage("/login.html")           // 自定义页面设置
            .loginProcessingUrl("/login") 		// 登录访问路径
            .defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
            
    http.authorizeRequests()
            .antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
            .antMatchers("/index").hasAuthority("admin")
            .anyRequest().authenticated()      // 设置所有路径都需认证(不包括忽略的路径)
            
    http.csrf().disable();
}

3、授权

List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("root");

4、测试
在这里插入图片描述


4.6、注解使用

1、@Secured:判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 ROLE_

主启动类开启注解功能:@EnableGlobalMethodSecurity(securedEnabled=true)

控制层

@GetMapping("/update")
@Secured({"ROLE_sale", "ROLE_xxx"})
public String update() {
    return "hello update";
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");

2、@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中

主启动类开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)

@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('admin')")
public String update() {
    return "hello update";
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

3、@PostAuthorize:在方法执行后再进行权限验证,适合验证带有返回值的权限

主启动类开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)

@GetMapping("/update")
@PostAuthorize("hasAnyAuthority('xxx')")
public String update() {
    System.out.println("@@@@@@@@@@@@@@@@@@@@");
    return "hello update";
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

虽然无法访问页面,但是控制台输出了业务逻辑
在这里插入图片描述


4、@PostFilter:权限验证之后对返回值进行过滤

@GetMapping("/getAll")
@PreAuthorize("hasAnyAuthority('admin')")
@PostFilter("filterObject.username == 'laptoy'")
public List<Users> getAll() {

    ArrayList<Users> list = new ArrayList<>();
    list.add(new Users(1, "laptoy", "123"));
    list.add(new Users(2, "jiali", "123"));
    
    System.out.println(list);
    
    return list;
}

控制台输出两个 user
在这里插入图片描述
返回值只有 laptoy
在这里插入图片描述


5、@PreFilter:进入控制器之前对数据进行过滤

@GetMapping("/getAll")
@PreAuthorize("hasAnyAuthority('admin')")
@PreFilter("filterObject.username == 'laptoy'")
public List<Users> getAll(@RequestBody List<Users> list) {
    list.forEach(l -> {
        System.out.println(l.getUsername());
    });
    return list;
}

先登录后使用PostMan进行测试


4.7、用户注销

1、login.html

<body>
<form action="/login" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    <input type="submit" value="login">
</form>
</body>

2、success.html

<body>
<h1>登录成功</h1>
<a href="/logout">登出</a>
</body>

3、配置类

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 登出
    http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();

    // 自定义登录页面
    http.formLogin()
            .loginPage("/login.html")           // 自定义页面设置
            .loginProcessingUrl("/login")       // 登录访问路径
            .defaultSuccessUrl("/success.html").permitAll() // 登录成功后跳转到
            
    http.authorizeRequests()
            .antMatchers("/", "/login").permitAll() // 设置忽略的路径
            .anyRequest().authenticated()      // 设置所有路径都需认证(不包括忽略的路径)
            
    http.csrf().disable();
}

4、控制层

@RestController
public class TestController {

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

5、测试

  1. 访问 localhost:8080/login.html 登录成功后 点击 登出
  2. 访问 /index 需要重新进行登录验证

4.8、记住我功能

1、实现原理
在这里插入图片描述
2、认证及存放 Token 过程
在这里插入图片描述


代码实现

1、建表

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;

2、配置类 注入数据源,配置操作数据库对象

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	...
	
	// 注入数据源
	@Autowired
	private DataSource dataSource;
	// 配置对象
	@Bean
	public PersistentTokenRepository persistentTokenRepository() {
	    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
	    jdbcTokenRepository.setDataSource(dataSource);
	    return jdbcTokenRepository;
	}
	
	...
}

3、配置类配置自动登录

protected void configure(HttpSecurity http) throws Exception {

	http.formLogin()
		.and().rememberMe().tokenRepository(persistentTokenRepository())
		.tokenValiditySeconds(60) // 设置有效时长 秒
		.userDetailsService(userDetailsService())
}

4、login.html

<form action="/login" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    记住我:<input type="checkbox" name="remember-me"><br>
    <input type="submit" value="login">
</form>在这里插入代码片

配置类全部代码展示

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailService myUserDetailService;

    // 注入数据源
    @Autowired
    private DataSource dataSource;

    // 配置对象
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 登出
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();

        // 配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");

        // 自定义登录页面
        http.formLogin()
                .loginPage("/login.html")                        // 自定义页面设置
                .loginProcessingUrl("/login")                    // 登录访问路径
                .defaultSuccessUrl("/success.html").permitAll(); // 登录成功后跳转到

        // 权限认证
        http.authorizeRequests()
                .antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
                .anyRequest().authenticated();       // 设置所有路径都需认证(不包括忽略的路径)

        // 记住我
        http.rememberMe().tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(60) // 设置有效时长 秒
                .userDetailsService(userDetailsService());

        // 关闭csrf保护功能
        http.csrf().disable();
    }
}

4.9、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 方法进行防护。


1、登录页面添加隐藏域

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

2、配置类开启CSRF

// http.csrf().disable();

五、微服务权限方案

5.1、认证授权过程分析

在这里插入图片描述
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token 的形式进行授权与认证

  1. 用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值
  2. 用户名为 key,权限列表为 value 的形式存入 redis 缓存中
  3. 根据用户名相关信息 生成 token返回
  4. 浏览器将 token记录到 cookie 中,每次调用 api 接口都默认将 token 携带 到 header 请求头中
  5. Spring-security 解析 header 头获取 token 信息,解析 token 获取当前 用户名,根据用户名就可以从 redis 中获取权限列表,
  6. 这样 Spring-security 就能够判断当前 请求是否有权限访问

5.2、数据模型

执行资料里的 SQL脚本
在这里插入图片描述


5.3、搭建项目工程

1、新建Springboot父工程 acl_parent (2.2.1.RELEASE)

  • 删除 src目录
  • POM添加 <packaging>pom</packaging>

2、在父工程创建子模块(以下都是maven工程)

1)common (删除 src目录 POM添加 <packaging>pom</packaging>

  • service_base:工具类
  • spring_security:权限配置

2)infrastructure(删除 src目录 POM添加 <packaging>pom</packaging>

  • api_gateway:网关

3)service(删除 src目录 POM添加 <packaging>pom</packaging>

  • service_acl:权限管理微服务模块

在这里插入图片描述


1、acl_parent父工程POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.laptoy</groupId>
    <artifactId>acl_parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>acl_parent</name>
    <description>acl_parent</description>

    <modules>
        <module>common</module>
        <module>infrastructure</module>
        <module>service</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
        <mybatis-plus.version>3.0.5</mybatis-plus.version>
        <velocity.version>2.0</velocity.version>
        <swagger.version>2.7.0</swagger.version>
        <jwt.version>0.7.0</jwt.version>
        <fastjson.version>1.2.28</fastjson.version>
        <gson.version>2.8.2</gson.version>
        <json.version>20170516</json.version>
        <cloud-alibaba.version>0.2.2.RELEASE</cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!--Spring Cloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--mybatis-plus 持久层-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>

            <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
            <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity-engine-core</artifactId>
                <version>${velocity.version}</version>
            </dependency>

            <dependency>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>${gson.version}</version>
            </dependency>
            <!--swagger-->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${swagger.version}</version>
            </dependency>
            <!--swagger ui-->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${swagger.version}</version>
            </dependency>
            <!-- JWT -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>${jwt.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>
            <dependency>
                <groupId>org.json</groupId>
                <artifactId>json</artifactId>
                <version>${json.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.1、common 模块 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>acl_parent</artifactId>
        <groupId>com.laptoy</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>common</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>service_base</module>
        <module>spring_security</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>provided </scope>
        </dependency>

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <scope>provided </scope>
        </dependency>

        <!--lombok用来简化实体类:需要安装lombok插件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided </scope>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <scope>provided </scope>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <scope>provided </scope>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- spring2.X集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.0</version>
        </dependency>
    </dependencies>

</project>

1.1.1、service_base

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>common</artifactId>
        <groupId>com.laptoy</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service_base</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

1.1.2、spring_security 模块 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>common</artifactId>
        <groupId>com.laptoy</groupId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring_security</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.laptoy</groupId>
            <artifactId>service_base</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
    </dependencies>

</project>

1.2、infrastructure

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>acl_parent</artifactId>
        <groupId>com.laptoy</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>infrastructure</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>api_gateway</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

1.2.1、api_gateway 模块 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>infrastructure </artifactId>
        <groupId>com.laptoy</groupId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>api_gateway</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.laptoy</groupId>
            <artifactId>service_base</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!--gson-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>

        <!--服务调用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

</project>

1.3、service 模块 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>acl_parent</artifactId>
        <groupId>com.laptoy</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service</artifactId>
    <packaging>pom</packaging>
    <modules>
        <module>service_acl</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>com.laptoy</groupId>
            <artifactId>service_base</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <!--服务注册-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--服务调用-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

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

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
        </dependency>


        <!--lombok用来简化实体类:需要安装lombok插件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--gson-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

</project>

1.3.1、service_acl 模块 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>service</artifactId>
        <groupId>com.laptoy</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service_acl</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.laptoy</groupId>
            <artifactId>spring_security</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>

</project>

5.4、启动 Redis 和 nacos


5.5、编写 common 工具类

1、新建 com.laptoy.utils (将资料中的所有工具类放到该包)
在这里插入图片描述


5.6、编写 security 工具类

1、密码处理工具类

package com.laptoy.security.security;

import com.laptoy.utils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 密码处理工具类
 */
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {

    public DefaultPasswordEncoder() {
        this(-1);
    }

    public DefaultPasswordEncoder(int strength) {

    }

    // 进行 MD5 加密
    @Override
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }

    /**
     * 传入的密码 与 数据库密码进行比较
     *
     * @param rawPassword     传入的密码
     * @param encodedPassword 数据库中的密码
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}

2、Token工具类

package com.laptoy.security.security;

import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class TokenManager {

    // token 有效事件
    private long tokenExpiration = 24 * 60 * 60 * 1000;

    // 编码密钥用于签名
    private String tokenSignKey = "123456";

    // 1、根据用户名生成token
    public String createToken(String username) {
        String token = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }

    // 2、根据token字符串得到用户信息
    public String getUserInfoFormToken(String token) {
        String userInfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        return userInfo;
    }

    // 3、删除token
    public void removeToken(String token) {
    }
}

3、退出处理器

package com.laptoy.security.security;

import com.laptoy.utils.utils.R;
import com.laptoy.utils.utils.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TokenLogoutHandler implements LogoutHandler {

    private TokenManager tokenManager;

    @Autowired
    private RedisTemplate redisTemplate;

    //构造方法
    public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

        // 从header获取token
        String token = request.getHeader("token");
        if (token != null) {
            // 从header移除token
            tokenManager.removeToken(token);

            // 从token获取用户名
            String username = tokenManager.getUserInfoFormToken(token);
            // 从redis移除token
            redisTemplate.delete(username);
        }
        ResponseUtil.out(response, R.ok());

    }
}

4、未授权统一处理类

public class UnAuthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

5.7、编写Security认证过滤器

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Laptoy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值