SpringSecurity学习笔记

一、什么是SpringSecurity?

1、它基于Spring,提供了一整套完整的web应用安全性的解决方案

2、主要包括两个部分:认证和授权

  • 认证:通俗点说就是系统认为用户是否能登录

  • 授权:通俗点就是系统判断用户是否有权限去做某些事情

二、为什么要使用SpringSecurity?

与Spring框架类似,简化权限开发的过程,更加高效、快捷并且安全的实现项目的权限管理。本质上就是一个权限管理框架。

三、相比于其他安全框架有什么优势?

SpringSecurity的特点:

  • 和Spring无缝整合
  • 全面的权限控制
  • 重量级
  • 使用更加方便

四、如何使用?

1、入门案例

1、新建一个SpringBoot项目


2、导入依赖

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

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

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

3、编写conroller测试

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

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

4、浏览器控制端访问:localhost:8080/test/hello

用户名默认为:user

密码在控制台已经自动生成:

在这里插入图片描述

五、SpringSecurity基本原理

1、本质上就是一个过滤器链

2、提供给用户自定义实现功能的接口有两个,一个是UserDetailsService,另外一个是PasswordEncoder

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

六、web权限解决方案

SpringSecurity实现web的权限管理,有三种方式,

1、直接在配置文件中配置

spring.security.user.name=beim
spring.security.user.password=123

2、通过配置类实现

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");  // 要设置的密码
        /*
            withUser() : 用户名
            password() : 密码
            roles() : 角色
         */
        auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
    }

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

3、自定义类(常用)

  • 先创建一个配置类

    @Configuration
    public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        /**
         *
         * @param auth
         * @throws Exception
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(password());
        }
    
        @Bean
        protected PasswordEncoder password(){
            return new BCryptPasswordEncoder();
        }
    
    }
    
    
  • 自定义业务接口实现UserDetailsService接口

    @Service("userDetailsService")
    public class MyUserDetailService implements UserDetailsService {
    
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // auths:权限集合
            List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
    
            return new User("marry",new BCryptPasswordEncoder().encode("123"),auths);
        }
    }
    
    

4、使用数据库查询

使用mybatisplus完成数据库操作(运用mybatisX插件),基于上文代码进行的增加以及修改

1、第一步:导入相关的依赖

<!--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>

2、第二步:创建数据库和数据库表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `uname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `upass` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`uid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `users` VALUES (1, 'lisi', '123456');
INSERT INTO `users` VALUES (2, 'zhangsan', '123456');

SET FOREIGN_KEY_CHECKS = 1;

3、第三步:创建user表对应的实体类

@TableName(value ="users")
@Data
public class Users implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer uid;

    private String uname;

    private String upass;
}

4、第四步:创建接口继承BaseMapper

@Repository
public interface UsersMapper extends BaseMapper<Users> {
    Users selectAllByUname(@Param("uname") String uname);
}

5、第五步:在UsersServiceImpl调用mapper中的方法

@Service("userDetailsService")
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users>
    implements UsersService, UserDetailsService {

    @Autowired
    private UsersMapper usersMapper;

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

        Users users = usersMapper.selectAllByUname(username);
        if (users == null){
            throw new UsernameNotFoundException("用户未找到!");
        }else {

            // auths:权限集合
            List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");

            return new User(users.getUname(),new BCryptPasswordEncoder().encode(users.getUpass()),auths);
        }
    }
}

6、在启动类上添加mapper扫描器

@SpringBootApplication
@MapperScan("com.beim.springsecuritydemo.mapper")
public class SpringsecuritydemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringsecuritydemoApplication.class, args);
    }
}

7、配置properties数据库信息

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

mybatis-plus.mapper-locations=classpath:mapper/**/*.xml

5、自定义设置登录页面

1、在配置类中实现相关的配置,(基于上文代码进行的修改)

@Override
protected void configure(HttpSecurity http) throws Exception {
	// 自定义登录页面,默认是在static目录下
    http.formLogin().loginPage("/login.html")  
        /*
       登录页访问路径,即action属性的值,当页面提交后给此路径后,框架会对其进去拦截,并做登录验证
       如果验证成功,则进入到后台的登录成功controller路径,如果验证失败,则依旧跳转回登录页面
       */
        .loginProcessingUrl("/user/login")
		// 登录成功之后跳转路径,后台controller对应处理
        .defaultSuccessUrl("/test/index").permitAll()   
        .and().authorizeRequests()
        // 设置哪些路径可以直接访问,不需要认证
        .antMatchers("/","/test/hello","/user/login").permitAll()  
        .anyRequest().authenticated()
        .and().csrf().disable();  // 关闭csrf防护
}

2、controller对应方法

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

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

    @GetMapping(value ="/index")
    public String login(){
        return "hello security1111";
    }
}

3、编写静态页面(默认是在static目录下创建login.html)

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登录页面设置</title>
    </head>
    <body>
        <form method="post" action="user/login">
            用户名:<input type="text" name="username">
            <br>
            密码:<input type="password" name="password">
            <br>
            <input type="submit" value="提交表单">

        </form>
    </body>
</html>

需要注意的是,表单中的name属性必须为username和password,因为这是SpringSecurity框架内部定义的,否则会识别不到

6、角色或权限进行访问控制

(1)hasAuthority方法

如果当前的主体具有**某一个(注意是只有一个)**指定的访问权限,则返回true否则返回false

1、在配置类设置当前访问地址有哪些权限,配置类中重写configure方法

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 访问该页面需要有admins权限
	http.authorizeRequests().antMatchers("/test/index")
                .hasAuthority("admins");
}

2、在UserDetailService中设置返回对象的权限

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

    Users users = usersMapper.selectAllByUname(username);
    if (users == null){
        throw new UsernameNotFoundException("用户未找到!");
    }else {

        // auths:权限集合,   设置权限集合
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");

        return new User(users.getUname(),new BCryptPasswordEncoder().encode(users.getUpass()),auths);  // 给返回的User对象添加权限集合
    }
}

​ 如果页面的访问权限与用户认证的权限不一致,则会出现拒绝访问页面的错误

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C09n4Zhx-1646824781573)(E:\Pictures\Typora-pictures\1646720710069.png)]

(2)hasAnyAuthority方法

如果当前页面的访问权限不止一个,则用此方法来设置多个访问权限。注意:这个是指只要满足两个权限中的某一个就可以进行访问,而不是两个都需要满足才能进行访问

// 设置具有admins或manager两个访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().antMatchers("/test/index")
                .hasAnyAuthority("admins","manager");
}

用户的访问权限为manager:

@Service("userDetailsService")
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users>
    implements UsersService, UserDetailsService {

    @Autowired
    private UsersMapper usersMapper;

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

        Users users = usersMapper.selectAllByUname(username);
        if (users == null){
            throw new UsernameNotFoundException("用户未找到!");
        }else {

            // 访问权限为manager
            List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("manager");

            return new User(users.getUname(),new BCryptPasswordEncoder().encode(users.getUpass()),auths);
        }
    }
}

(3)hasRole方法

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

// 指定当前路径下的访问角色为admins
@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().antMatchers("/test/index")
                .hasRole("admins");
}
// 权限名称前需要添加ROLE_
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admins");

需要注意的是:此方法在添加权限的时候要加上“ROLE_”前缀

(4)hasAnyRole方法

设置多个角色,用户只要具备其中任何一个,都可以进行访问。

// 指定多个角色
@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().antMatchers("/test/index")
        .hasAnyRole("role","role1")
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_role1");

需要注意的是:此方法在添加权限的时候要加上“ROLE_”前缀

7、自定义403页面

第一步,先在static目录下建立一个页面表示自定义的403页面

在这里插入图片描述

第二步:在配置类中配置页面路径即可

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

8、认证授权注解的使用

(1)@Secured

判断是否具有角色,可以访问方法,另外需要注意两点:

  • 注解传入的参数必须添加前缀”ROLE_
  • 使用之前需要在启动类或者配置类中添加启用注解

1、在启动类中添加启动注解

// 开启全局安全验证注解
@EnableGlobalMethodSecurity(securedEnabled=true)

2、在方法上使用此注解

@GetMapping(value ="/insert")
// 表明用户需要具备admin或manager其中一个权限时才能访问
@Secured({"ROLE_admin","ROLE_manager"})  
public String insert(){
	return "hello insert";
}

3、在UserDetailService设置用户角色

// auths:权限集合
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_manager");

(2)@PreAuthorize

表示进入方法前进行权限验证

1、在启动类或者配置类上,开启此注解

@SpringBootApplication
@MapperScan("com.beim.springsecuritydemo.mapper")
// 开启全局安全验证注解
@EnableGlobalMethodSecurity(prePostEnabled = true)

2、在方法上面添加注解

@GetMapping(value ="/insert")
@PreAuthorize("hasAnyAuthority('admins')")
public String insert(){
    return "hello insert";
}

3、在UserDetailService设置用户角色

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

(3)@PostAuthorize

在方法执行之后做一个权限验证,用得比较少

1、在启动类或者是配置类上,开启此注解

@EnableGlobalMethodSecurity(prePostEnabled = true)

2、在方法上添加注解

@GetMapping(value ="/insert")
@PostAuthorize("hasAnyAuthority('admins')")
public String insert(){
        System.out.println("insert......");
        return "hello insert";
    }
}

3、在UserDetailService设置用户角色

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

(4)PostFilter

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

@GetMapping(value ="/update")
@PreAuthorize("hasAnyAuthority('admins')")
@PostFilter("filterObject.uname == 'admin1'")
public List<Users> update(){
    ArrayList<Users> list = new ArrayList<>();
    list.add(new Users(12,"admin1","123",0,1,0));
    list.add(new Users(13,"admin2","456",1,0,1));
    return list;
}

最后返回给前端的数据是admin1的,需要注意的是PostFilter中的参数,filterObject为一个默认的内置对象,必须是这个名称

(5)PreFilter

对传入方法的数据进行过滤

@GetMapping(value ="/update")
@PreAuthorize("hasAnyAuthority('admins')")
@PreFilter("filterObject.uname == 'admin2'")
public List<Users> update(List<Users> list){
    list.forEach(t-> {
    	System.out.println(t.getUid()+"\t"+t.getUname());
    });
    return list;
}

9、用户注销

在配置类添加退出的配置

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置自定义没有权限访问时跳转的页面
        http.exceptionHandling().accessDeniedPage("/error.html");

        // 设置退出的路径
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello");

        http.formLogin().loginPage("/login.html")  // 自定义登录页面,默认是在static目录下
                .loginProcessingUrl("/user/login")  // 登录访问路径
                .defaultSuccessUrl("/success.html").permitAll()   // 登录成功之后跳转路径,后台controller对应处理
                .and().authorizeRequests()
                .antMatchers("/", "/test/hello", "/user/login").permitAll()  // 设置哪些路径可以直接访问,不需要认证
                .anyRequest().authenticated()
                .and().csrf().disable();  // 关闭csrf防护
    }

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

}

测试

1、修改配置类,登录成功之后跳转到成功的页面

2、在登录成功的页面中添加超链接,链接到设置退出的路径

3、登录成功之后再去访问其他的controller中的方法是不能进行访问的。

10、自动登录

SpringSecurity实现自动登录是通过cookie实现的,其基本原理流程可参考下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJUKjUtw-1646824781574)(E:\Pictures\Typora-pictures\1646729168258.png)]

1、创建数据表

drop table if exists persistent_logins;
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、配置类,注入数据源,配置数据库操作对象

JdbcTokenRepositoryImpl实现类里封装了现成的SQL建表语句,因此我们不需要自己建立数据表,如果已经自己建立好了数据表,则需要将setCreateTableOnStartup的参数设置为false。

@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    // 赋值数据源
    jdbcTokenRepository.setDataSource(dataSource);
    /* 
    	自动创建表,第一次执行会创建,以后要执行就要删除掉!
    	需要注意的是,如果数据库中已经创建了persistent_logins表,则需要设置为false,
    	否则会报异常这里因为已经创建了数据表,所以设置为false
    */
    jdbcTokenRepository.setCreateTableOnStartup(false);
    return jdbcTokenRepository;
}

3、配置类,添加记住我功能

http.rememberMe()
    // 传入PersistentTokenRepository对象
    .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(60)  // 设置cookie的有效时长,单位是秒
                .userDetailsService(userDetailsService);  // 将用户的信息传入

4、表单中添加复选框

<input type="checkbox" name="remember-me"> 自动登录

这里唯一需要注意的就是:name字段的值必须为remember-me,否则框架会识别不到。

11、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:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf"/>

2、关闭CSRF防御

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

需要注意的是,CSRF对于:GET、HEAD、TRACE、OPTIONS请求是直接放行的,不会进行安全验证

3、防护的基本原理

第一步:会将隐藏域中生成的csrfToken 保存到 HttpSession 或者 Cookie 中。

第二步:当请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当 前请求是否合法。主要通过 CsrfFilter 过滤器来完成。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值