【工作记录】springsecurity从入门到实战(一)

一、介绍

在web应用开发中,安全无疑是十分重要的,目前最流行的安全框架莫过于shiro和springsecurity了。

以下是二者简单的一个对比:

SpringSecurityShiro
基本功能完善完善
文档完善程度强大强大
社区支持度依托于Spring,社区支持强大强大
集成难度、使用方便度与Spring、springboot、springcloud集成方便、简单简单
用户量趋势上升略有下滑
所属组织SpringApache
对oauth2.0的支持支持需要自行实现
社交登录实现支持需要自行实现
功能扩展性

对于项目中的认证授权模块的实现与扩展,二者都是满足要求的,且集成相对都较为容易。如果你只是想实现一个简单的web应用,shiro更加的轻量级,学习成本也更低;如果您正在开发一个分布式的、微服务的、或者与Spring Cloud系列框架深度集成的项目,笔者还是建议您使用Spring Security。具体选择方案看公司和具体项目情况。

当然现在也有一些比较好的开源的认证授权框架,比较好的像sa-token也是值得学习和尝试的。

本文我们重点介绍的是SpringSecurity以及与Springboot项目的集成和扩展。

二、基本使用及异常处理

我们从一个简单的例子入手:

​ 接口分为两类,一类不需要登录可以直接访问,一类需要登录成功后可以访问,

​ 提供一个可以直接访问的登录接口和一个需要登录的验证接口。

项目采用Springboot+Maven+SpringSecurity+ java实现, 目前整体结构如下:

  • auth-parent-new: 顶级父工程

     - auth-common: 公共依赖,放一些工具类、通用实体类等
     - auth-framework: 完成认证授权的核心业务
    

依赖框架及版本:

名称版本号
Springboot2.7.8
Spring-Security5
Maven3
JDK1.8

2.1 新建一个项目auth-parent-new

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.7.8</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.zjtx.tech</groupId>
    <artifactId>auth-parent-new</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>auth-parent-new</name>
    <description>parent pom for auth-parent</description>
    <properties>
        <java.version>8</java.version>
        <spring-boot.version>2.7.8</spring-boot.version>
        <lombok.version>1.18.22</lombok.version>
    </properties>
    <modules>
        <module>auth-common</module>
        <module>auth-framework</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2.2 新建auth-common子模块

命名为auth-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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.zjtx.tech</groupId>
        <artifactId>auth-parent-new</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>auth-common</artifactId>
    <description>公共模块代码</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.7.10</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

auth-common模块中放置一个公共响应类R

package com.zjtx.tech.auth.common.domain;

import lombok.Data;

@Data
public class R<T> {

    private static final int SUCCESS = 200;    //成功
    private static final int ERROR = 1;    //错误
    private static final int FAIL = 2;    //失败
    private static final int INFO = 101;    //信息
    private static final int PROMPT = 102;    //提示
    private static final int WARNING = 103;    //警告

    private int code;

    private String msg;

    private T data;

    public static <T> R<T> ok() {
        return restResult(SUCCESS, "操作成功", null);
    }

    public static <T> R<T> ok(T data) {
        return restResult(SUCCESS, "操作成功", data);
    }

    public static <T> R<T> ok(String msg, T data) {
        return restResult(SUCCESS, msg, data);
    }

    public static <T> R<T> fail() {
        return restResult(FAIL, "操作失败", null);
    }

    public static <T> R<T> fail(String msg) {
        return restResult(FAIL, msg, null);
    }

    private static <T> R<T> restResult(int code, String msg, T data) {
        R<T> apiResult = new R<>();
        apiResult.setCode(code);
        apiResult.setData(data);
        apiResult.setMsg(msg);
        return apiResult;
    }

}

2.3 新建auth-framework子模块

整体结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFmk9p9P-1684286301725)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230410212547701.png)]

目录分为如下几种:

  • exception 全局异常处理
  • service 提供服务的
  • config 配置相关
  • controller 提供给前台访问的controller

具体内容如下:

2.3.1 全局异常处理类GlobalExceptionHandler.java

package com.zjtx.tech.auth.security.exception;

import com.zjtx.tech.auth.common.domain.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(AuthenticationException.class)
    public R<String> authEx(AuthenticationException ex) {
        //这里是用抽象异常类接的,不能使用ex.getCause().getMessage()会报空指针异常
        //ex.getMessage()这个方法返回的就是具体的异常信息
        log.info("捕获到全局异常, {}", ex.getMessage());
        return R.fail(ex.getMessage());
    }

}

AuthenticationException是springsecurity提供的一个认证异常抽象类,其类结构关系为:

异常类结构图

通过名称大致可以理解到基本都是一些认证异常,如用户名密码错误、账户锁定、账户过期之类的。

这里我们的全局异常类捕获的也是这个抽象父类异常,如果抛出的是子类异常就都可以捕获到。

2.3.2 用户服务实现类UserDetailsServiceImpl.java

springsecurity提供了一个获取用户信息的接口类UserDetailsService,在验证用户的时候会调用这个类的实现类的loadUser方法获取实际的用户信息,同时提供了一个接收返回的用户信息的实体类接口UserDetails以及一个默认的实现类User

UserDetailsServiceImpl.java

package com.zjtx.tech.auth.security.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.stereotype.Service;

@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //这里返回的是数据库的用户信息转换成的UserDetails的实现类
        //SpringSecurity会自动根据用户名和密码以及加密算法进行匹配
        log.info("根据用户名查询用户:{}", username);
        return 					User.builder().username(username).password("$2a$10$wcPhi2iVfRTL2hEJSTg3S.JiVs3hd4OaMNyiixXePePXbtlYUyiGS")
                .authorities(new SimpleGrantedAuthority("ROLE_ALL_USER"))
                .accountExpired(false)
                .credentialsExpired(false)
                .disabled(false)// disabled为true的话会返回状态码403
                .build();
    }

}

这里的逻辑正常来说的话是根据一个用户标识字段去数据库、内存或者LDAP这样的容器中去查询用户并返回相关信息。上面代码简化了这个逻辑,通过User类的静态方法内置了一个用户。

我们知道用户登录如果是表单登录一般会是用户名+密码这样的验证方式,springsecurity也提供了对应的密码验证接口PasswordEncoder,并提供了一系列的默认实现,如果不配置的话使用的是BCryptPasswordEncoder这个实现类,上面代码中的password参数就是原密码通过BCryptPasswordEncoder加密后的值,springsecurity会将用户输入的密码进行加密后与该值比较,如果不一致则抛出BadCrendicialException。

2.3.3 安全配置类SecurityConfig.java

package com.zjtx.tech.auth.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filerChain(HttpSecurity http) throws Exception {
        return http
            // 基于 token,不需要 csrf
            .csrf().disable()
            // 基于 token,不需要 session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            // 下面开始设置权限
            .authorizeRequests()
            //不校验所有以login开头的接口
            .antMatchers("/login/**").permitAll()
            //其他的接口都需要认证后才能访问
            .anyRequest().authenticated()
            .and()
            // 认证用户时用户信息加载配置,注入springAuthUserService
            .userDetailsService(userDetailsService)
            .build();
    }

    /**
     * 密码明文加密方式配置
     * @return 密码加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     * @param authenticationConfiguration 认证配置
     * @return 认证管理器
     * @throws Exception 认证异常
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 配置跨源访问(CORS)
     * @return 跨域配置
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

可以看到这个配置类是相当重要的,配置了访问权限、跨域访问、认证管理器、密码加密方式等。通过这个配置springsecurity可以知道哪些接口需要哪些权限或者角色才能访问,哪些接口不需要校验可以直接访问。

2.3.4 controller

提供两个controller类用于验证,一个需要认证的ResourceController和一个不需要认证的LoginController

ResourceController.java

package com.zjtx.tech.auth.security.controller;

import com.zjtx.tech.auth.common.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("resource")
public class ResourceController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @GetMapping("doTest")
    public R<Object> doTest(String username) {
        System.out.println("username = " + username);
        return R.ok(username);
    }

}

LoginController.java

package com.zjtx.tech.auth.security.controller;

import com.zjtx.tech.auth.common.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("login")
public class LoginController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @GetMapping("doLogin")
    public R<Object> doLogin(String username, String password) {
        Authentication token = new UsernamePasswordAuthenticationToken(username, password);
        Authentication authenticate = authenticationManager.authenticate(token);
        return R.ok(authenticate.getPrincipal());
    }

}

2.3.5 Application类和yml配置文件

package com.zjtx.tech.auth.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AuthFrameworkApplication {

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

}
server:
  port: 8081

2.3.6 接口测试

通过页面访问链接: http://localhost:8081/login/doLogin?username=1&password=1

用户名密码验证

修改password密码为123456,再次验证。

登录成功验证

访问resource下的doTest接口,可以看到返回的是403无权限,前端可根据该响应做对应页面的展示。

无权限验证

到此一个简单的demo应用就完成了。

三、实现原理

3.1 原理概述

Spring Security 认证原理基于过滤器链,其中核心的过滤器是 UsernamePasswordAuthenticationFilter。该过滤器负责处理来自用户的身份验证请求,并尝试对其进行身份验证。如果身份验证成功,它将生成一个 Authentication 对象并存储在安全上下文中。如果身份验证失败,它将生成一个 AuthenticationException,并向客户端返回错误响应。

除了过滤器链外,Spring Security 还使用了一些其他组件来支持身份验证,例如 AuthenticationManager 和 UserDetailsService。AuthenticationManager 用于管理身份验证过程,并根据需要委托给不同的 AuthenticationProvider 来处理不同类型的身份验证。UserDetailsService 负责从特定来源获取用户信息,例如数据库或 LDAP 目录。

3.2 认证过程描述

具体认证过程如下(以用户名密码登录为例):

  1. 用户登录:用户在登录页面输入用户名和密码并提交表单。
  2. 认证请求:Web容器拦截到表单提交请求后,Spring Security会将其转发至认证处理过滤器链(AuthenticationProcessingFilter)。
  3. 身份验证:认证处理过滤器链根据配置的身份验证方式,在内存、关系型数据库或LDAP等数据源中验证用户的身份。
  4. 认证结果处理:如果身份验证成功,则Spring Security会创建一个经过授权的安全上下文(SecurityContext)并将其绑定到当前线程。如果身份验证失败,则会返回相应的错误信息。
  5. 认证成功处理:如果身份验证成功,Spring Security会根据配置的设置,执行一些额外的操作,例如生成令牌、跳转到指定页面等。
  6. 授权检查:所有后续请求都将通过授权检查过滤器链(FilterSecurityInterceptor)以确保用户被授权访问相应资源。
  7. 认证注销:用户在退出时,Spring Security会清除与该用户相关的所有安全上下文。

以上是Spring Security的基本认证流程,其中具体实现可以根据项目需求进行定制化配置。

3.3 核心概念

  1. Authentication(认证):验证用户身份的过程,通常包含用户名和密码。
  2. Authorization(授权):确定用户是否有权限访问某个资源或执行某个操作。
  3. Principal(主体):代表当前被认证的用户,包含用户的信息和凭证。
  4. Granted Authority(授权信息):用于表示用户拥有哪些操作或资源的访问权限。

3.4 核心组件

  1. SecurityContext(安全上下文):存储当前用户的认证信息和授权信息。
  2. AuthenticationManager(认证管理器):处理Authentication对象的认证过程。
  3. UserDetailsService(用户详情服务):用于加载用户信息,通常从数据库中读取用户信息。
  4. UserDetails(用户详情):包含用户的用户名、密码、授权信息等详细信息。
  5. AccessDecisionManager(访问决策管理器):决定当前用户是否有权限访问某个资源或执行某个操作。
  6. FilterChainProxy(过滤器链代理):在请求到达应用程序之前,对请求进行预处理,并将请求传递给相应的安全过滤器。

3.5 认证流程图

认证流程图,图片来源于网络

图中描述的已经很清晰了,认真看完应该对流程和各个组件的关系就心里有数了。

3.6 扩展点

Spring Security 中的一些扩展点包括:

  1. AuthenticationProvider:用于自定义身份验证逻辑。
  2. AbstractAuthenticationToken: 允许用户实现不同的登录方式,AuthenticationProvider根据具体AbstractAuthenticationToken实现类选择Provider的具体实现类
  3. AccessDecisionVoter:用于根据授权策略来决定是否允许访问特定资源。
  4. FilterInvocationSecurityMetadataSource:用于为每个请求提供相应的安全元数据,这些元数据描述了该请求所需的安全配置。
  5. SecurityContextRepository:用于管理用户的安全上下文信息,例如认证和授权状态。
  6. WebSecurityConfigurerAdapter:用于配置 Spring Security 的安全策略,例如指定哪些 URL 路径需要受保护,如何处理登录和注销等操作。

小结

​ 本文完成了springsecurity基础环境的搭建和简单示例以及原理的简单介绍,后文会继续介绍进一步的应用。
就本文涉及的内容有任何疑问或者建议欢迎留言评论~
创作不易,欢迎一键三连~~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

泽济天下

你的鼓励是我最大的动力。

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

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

打赏作者

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

抵扣说明:

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

余额充值