一、介绍
在web应用开发中,安全无疑是十分重要的,目前最流行的安全框架莫过于shiro和springsecurity了。
以下是二者简单的一个对比:
SpringSecurity | Shiro | |
---|---|---|
基本功能 | 完善 | 完善 |
文档完善程度 | 强大 | 强大 |
社区支持度 | 依托于Spring,社区支持强大 | 强大 |
集成难度、使用方便度 | 与Spring、springboot、springcloud集成方便、简单 | 简单 |
用户量趋势 | 上升 | 略有下滑 |
所属组织 | Spring | Apache |
对oauth2.0的支持 | 支持 | 需要自行实现 |
社交登录实现 | 支持 | 需要自行实现 |
功能扩展性 | 强 | 强 |
对于项目中的认证授权模块的实现与扩展,二者都是满足要求的,且集成相对都较为容易。如果你只是想实现一个简单的web应用,shiro更加的轻量级,学习成本也更低;如果您正在开发一个分布式的、微服务的、或者与Spring Cloud系列框架深度集成的项目,笔者还是建议您使用Spring Security。具体选择方案看公司和具体项目情况。
当然现在也有一些比较好的开源的认证授权框架,比较好的像sa-token也是值得学习和尝试的。
本文我们重点介绍的是SpringSecurity以及与Springboot项目的集成和扩展。
二、基本使用及异常处理
我们从一个简单的例子入手:
接口分为两类,一类不需要登录可以直接访问,一类需要登录成功后可以访问,
提供一个可以直接访问的登录接口和一个需要登录的验证接口。
项目采用Springboot+Maven+SpringSecurity+ java实现, 目前整体结构如下:
-
auth-parent-new: 顶级父工程
- auth-common: 公共依赖,放一些工具类、通用实体类等 - auth-framework: 完成认证授权的核心业务
依赖框架及版本:
名称 | 版本号 |
---|---|
Springboot | 2.7.8 |
Spring-Security | 5 |
Maven | 3 |
JDK | 1.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 认证过程描述
具体认证过程如下(以用户名密码登录为例):
- 用户登录:用户在登录页面输入用户名和密码并提交表单。
- 认证请求:Web容器拦截到表单提交请求后,Spring Security会将其转发至认证处理过滤器链(AuthenticationProcessingFilter)。
- 身份验证:认证处理过滤器链根据配置的身份验证方式,在内存、关系型数据库或LDAP等数据源中验证用户的身份。
- 认证结果处理:如果身份验证成功,则Spring Security会创建一个经过授权的安全上下文(SecurityContext)并将其绑定到当前线程。如果身份验证失败,则会返回相应的错误信息。
- 认证成功处理:如果身份验证成功,Spring Security会根据配置的设置,执行一些额外的操作,例如生成令牌、跳转到指定页面等。
- 授权检查:所有后续请求都将通过授权检查过滤器链(FilterSecurityInterceptor)以确保用户被授权访问相应资源。
- 认证注销:用户在退出时,Spring Security会清除与该用户相关的所有安全上下文。
以上是Spring Security的基本认证流程,其中具体实现可以根据项目需求进行定制化配置。
3.3 核心概念
- Authentication(认证):验证用户身份的过程,通常包含用户名和密码。
- Authorization(授权):确定用户是否有权限访问某个资源或执行某个操作。
- Principal(主体):代表当前被认证的用户,包含用户的信息和凭证。
- Granted Authority(授权信息):用于表示用户拥有哪些操作或资源的访问权限。
3.4 核心组件
- SecurityContext(安全上下文):存储当前用户的认证信息和授权信息。
- AuthenticationManager(认证管理器):处理Authentication对象的认证过程。
- UserDetailsService(用户详情服务):用于加载用户信息,通常从数据库中读取用户信息。
- UserDetails(用户详情):包含用户的用户名、密码、授权信息等详细信息。
- AccessDecisionManager(访问决策管理器):决定当前用户是否有权限访问某个资源或执行某个操作。
- FilterChainProxy(过滤器链代理):在请求到达应用程序之前,对请求进行预处理,并将请求传递给相应的安全过滤器。
3.5 认证流程图
图中描述的已经很清晰了,认真看完应该对流程和各个组件的关系就心里有数了。
3.6 扩展点
Spring Security 中的一些扩展点包括:
- AuthenticationProvider:用于自定义身份验证逻辑。
- AbstractAuthenticationToken: 允许用户实现不同的登录方式,AuthenticationProvider根据具体AbstractAuthenticationToken实现类选择Provider的具体实现类
- AccessDecisionVoter:用于根据授权策略来决定是否允许访问特定资源。
- FilterInvocationSecurityMetadataSource:用于为每个请求提供相应的安全元数据,这些元数据描述了该请求所需的安全配置。
- SecurityContextRepository:用于管理用户的安全上下文信息,例如认证和授权状态。
- WebSecurityConfigurerAdapter:用于配置 Spring Security 的安全策略,例如指定哪些 URL 路径需要受保护,如何处理登录和注销等操作。
小结
本文完成了springsecurity基础环境的搭建和简单示例以及原理的简单介绍,后文会继续介绍进一步的应用。
就本文涉及的内容有任何疑问或者建议欢迎留言评论~
创作不易,欢迎一键三连~~~~