Spring Security 学习

Spring Security 官网

中文文档

学习 Spring Security 的前置知识

  • Spring 框架
  • Spring Boot 框架
  • Java Web


一、Spring Security 介绍

1、概要

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


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

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点就是说系统认为用户是否能登录

(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说系统会为不同的用户分配不同的角色,而每个角色对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情



2、特点

Spring Security 特点

  • 和 Spring 无缝整合
  • 全面的权限控制
  • 专门为 Web 开发而设计
  • 重量级

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

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

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




二、Spring Security 的入门案例

1、项目环境搭建

第一步:

创建一个 Spring Boot 工程

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

第二步:

引入相关依赖

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

第三步:

编写 Controller 进行测试

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello Spring Security ";
    }
}

第四步:

进行访问

访问结果为


在这里插入图片描述



2、基本原理

2.1、过滤器链

Spring Security 本质是一个过滤器链。

Debug 走一遍,看源码,重点关注三个过滤器。(查找类快捷键:Ctrl + Shift + N)


(1)FilterSecurityInterceptor:是一个方法级别的权限过滤器,位于过滤链的最底部。

源码如下:

在这里插入图片描述

// 实现了 Filter 接口
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

// 业务逻辑
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    FilterInvocation fi = new FilterInvocation(request, response, chain);
    this.invoke(fi);
}

// 业务逻辑调用的方法
public void invoke(FilterInvocation fi) throws IOException, ServletException {
    if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
        fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    } else {
        if (fi.getRequest() != null && this.observeOncePerRequest) {
            fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
        }

        InterceptorStatusToken token = super.beforeInvocation(fi);

        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.finallyInvocation(token);
        }

        super.afterInvocation(token, (Object)null);
    }
}

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

源码如下:
在这里插入图片描述

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;

    try {
        chain.doFilter(request, response);
        this.logger.debug("Chain processed normally");
    } catch (IOException var9) {
        throw var9;
    } catch (Exception var10) {
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
        RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
        if (ase == null) {
            ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        }

        if (ase == null) {
            if (var10 instanceof ServletException) {
                throw (ServletException)var10;
            }

            if (var10 instanceof RuntimeException) {
                throw (RuntimeException)var10;
            }

            throw new RuntimeException(var10);
        }

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

        this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
    }

}

(3)UsernamePasswordAuthenticationFilter :对 /login 的 post 请求进行拦截,校验表单用户名,密码。

源码如下:

在这里插入图片描述

public Authentication attemptAuthentication(HttpServletRequest request,
        HttpServletResponse response) throws AuthenticationException {
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
                "Authentication method not supported: " + request.getMethod());
    }

    String username = obtainUsername(request);
    String password = obtainPassword(request);

    if (username == null) {
        username = "";
    }

    if (password == null) {
        password = "";
    }

    username = username.trim();

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
            username, password);

    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
}

2.2、过滤器加载过程


在这里插入图片描述


2.3 两个重要的接口


在这里插入图片描述


(1)UserDetailsService

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

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

(2)PasswordEncoder




三、Web 权限

1、用户认证

1.1 设置用户名密码

第一种方式:通过配置文件

application.properties 配置文件中配置:

spring.security.user.name=yanghui
spring.security.user.password=yanghui

第二种方式:通过配置类

继承 WebSecurityConfigurerAdapter 类,然后重写 configure() 方法

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 进行加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("yang");
        auth.inMemoryAuthentication().withUser("yang").password(password).roles("admin");
    }

    /**
     * 需要用到这个,否则会报错
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

第三种方式:自定义编写实现类(常用)

编写类实现 UserDetailsService 接口,实现 loadUserByUsername() 方法

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> authorities =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role");

        return new User("yanghui",
                new BCryptPasswordEncoder().encode("yanghui"), authorities);
    }
}

Config 配置类:

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;


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

    /**
     * 需要用到这个,否则会报错
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

1.2 查询数据库完成认证

第一步:引入依赖

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

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

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

第二步:创建数据库表

DROP TABLE IF EXISTS `users`;

CREATE TABLE `users` (
  `id` int NOT NULL,
  `username` varchar(50) NOT NULL,
  `password` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Users {

    private Integer id;

    private String username;

    private String password;
}

第四步:整合 MybatisPlus,创建接口

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

第五步:开始使用(在 MyUserDetailsService 类里面查询数据库)

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

    @Autowired
    private UserMapper userMapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 调用 UserMapper 中的方法查询数据库(根据用户名)
        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        Users user = userMapper.selectOne(queryWrapper);

        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        List<GrantedAuthority> authorities =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role");

        return new User(user.getUsername(),
                new BCryptPasswordEncoder().encode(user.getPassword()), authorities);
    }
}

第六步:配置数据库

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=1234567890

第七步:启动项目,开始测试


1.3 自定义用户登录界面

在 Spring Security 配置类中重写 configure 方法

@Override
protected void configure(HttpSecurity http) throws Exception {
    /**
     * 自定义自己编写的登录页面
     * loginPage:登录页面路径
     * loginProcessingUrl:登录访问路径
     * defaultSuccessUrl:登录成功之后跳转路径
     * csrf:关闭 csrf 防护
     * 注意:路径前面必须要加 / ,否则会报错
     */
    http.formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/user/login")
            .defaultSuccessUrl("/test/index").permitAll()
            .and().authorizeRequests()
            .antMatchers("/", "/test/hello", "/user/login").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable();
}


2、用户授权

2.1 基于权限访问控制

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


第一步:在 Spring Security 配置类设置当前访问地址有哪些权限

@Override
protected void configure(HttpSecurity http) throws Exception {
    /**
     * 自定义自己编写的登录页面
     * loginPage:登录页面路径
     * loginProcessingUrl:登录访问路径
     * defaultSuccessUrl:登录成功之后跳转路径
     * csrf:关闭 csrf 防护
     * 注意:路径前面必须要加 / ,否则会报错
     */
    http.formLogin()
            .loginPage("/login.html")
            .loginProcessingUrl("/user/login")
            .defaultSuccessUrl("/test/index").permitAll()
            .and().authorizeRequests()
            .antMatchers("/", "/test/hello", "/user/login").permitAll()
            // 当前登录用户,只有具有 admin 权限才可以访问这个路径
            .antMatchers("/test/index").hasAuthority("admin")
            .anyRequest().authenticated()
            .and().csrf().disable();

}

第二步:在 MyUserDetailsService 类中给用户添加权限

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

第三步:进行测试


hasAnyAuthority() 方法,如果当前的主体有多个权限,用逗号分隔,设置多个权限。


2.2 基于角色访问控制

hasRole() 方法:如果用户具备给定角色就允许访问,否则出现 403。如果当前主体具有指定的角色,则返回 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 + "'");
    }
    // 会帮我们加上一个前缀 ROLE_
    return "hasRole('ROLE_" + role + "')";
}

第一步:在配置类中配置(与权限类似)

.antMatchers("/test/index").hasRole("sale")

第二步:在 MyUserDetailsService 类中配置

// 注意这个 ROLE_ 前缀
List<GrantedAuthority> authorities =
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");

2.3 自定义 403 页面

在 Spring Security 配置类中进行配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    /**
     * 配置没有权限跳转的页面
     */
    http.exceptionHandling().accessDeniedPage("/error.html");
}

2.4 注解的使用

四个注解

首先开启注解功能,在 Spring Security 配置类或者 启动类上添加

@EnableGlobalMethodSecurity(securedEnabled = true)

  • @Secured,用户具有某个角色,可以访问方法,否则不能访问
@Secured({"ROLE_sale", "ROLE_manager"})
@GetMapping("/update")
public String update() {
    return "hello index";
}
  • @PreAuthorize,适合进入方法前的权限验证
@PreAuthorize("hasAnyAuthority('admin')")
@GetMapping("/update")
public String update() {
    return "hello index";
}
  • @PostAuthorize,使用并不多,在方法执行之后再进行权限验证
@PostAuthorize("hasAnyAuthority('admin')")
@GetMapping("/update")
public String update() {
    return "hello index";
}
  • @PostFilter,方法返回数据进行过滤
  • @PreFilter,传入方法数据进行过滤

2.5 用户注销

在 Spring Security 配置类中配置:

/**
 * logoutUrl:注销的地址
 * logoutSuccessUrl:注销后要跳转的地址
 */
http.logout()
        .logoutUrl("/logout")
        .logoutSuccessUrl("/").permitAll();

2.5 实现记住我

在这里插入图片描述


在这里插入图片描述


具体实现:

第一步:建表 SQL(Spring Security 提供的,在 JdbcTokenRepositoryImpl 类 37 行

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)

第二步:在 Spring Security 配置类中添加:

@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);
    // 自动创建表
    jdbcTokenRepository.setCreateTableOnStartup(true);
    return jdbcTokenRepository;
}

第三步:在 configure() 方法中配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.rememberMe().tokenRepository(persistentTokenRepository())
            .tokenValiditySeconds(60).userDetailsService(userDetailsService);
}

第四步:在登录页面中添加表单项,name 必须为 remember-me

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

2.6 CSRF 功能

在这里插入图片描述

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值