目录
简介
一、初识Spring Security(入门案例)
(1)新建project
(2)选择依赖
(3)编写一个 HelloController
(4)启动项目,访问localhost:8080
(5)自定义用户名和密码
二、表单认证
1. 自定义表单登录页
spring%20security-toc" style="margin-left:80px;">2. 配置spring security
3. 重启项目
编辑
三、认证与授权
1. 资源准备
(1)新建两个controller
(2)资源授权的配置
(3)重启服务访问
2. 基于内存的多用户支持
(1)在内存中配置用户
(2)访问测试
3. 基于默认数据库模型的认证与授权
(1)创建数据库表
(2)引入jdbc依赖,配置数据库
spring%20securiy%E6%8E%88%E6%9D%83-toc" style="margin-left:80px;">(3)配置spring securiy授权
4. 基于自定义数据库模型的认证与授权
(1)数据库准备
(2)编写实体类User
(3)编写自定义的UserDetailsServiceImpl
(4)编写Mapper
spring%20security%20%E9%85%8D%E7%BD%AE-toc" style="margin-left:80px;">(5)spring security 配置
(6)测试
简介
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
(1)前后端分离项目登录校验流程:
(2)Spring Security 完整流程
Spring Security 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,如:
一、初识Spring Security(入门案例)
(1)新建project
(2)选择依赖
(3)编写一个 HelloController
java">@RestController
@RequestMapping("/")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello spring security!";
}
}
(4)启动项目,访问localhost:8080
即使没有任何配置,在引入Spring Security后,要访问对应的 URL 资源需要经过HTTP基本认证。
启动后,控制台会打印一个初始密码,如图:
访问localhost:8080,填写登录信息(这就是HTTP基本认证),填写用户名 user
(5)自定义用户名和密码
在配置文件 application.yml 中配置用户名和密码,重启项目之后使用该用户名和密码即可登录。
java">spring:
security:
user:
name: zy
password: abc
入门案例总结:
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
步骤:
(1)提交用户名和密码之后,会将用户名和密码传给UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter是Spring Security中一个非常重要的过滤器,它负责处理基于表单的身份验证,即当用户提交包含用户名和密码的表单时,该过滤器会从该请求中提取用户名和密码并进行身份验证。
(2)UsernamePasswordAuthenticationFilter调用authenticate() 方法进行认证
执行该方法时,它会获取请求中的用户名和密码参数,然后调用 AuthenticationManager 对象的 authenticate() 方法来进行身份验证。如果身份验证成功,则创建一个 Authentication 对象并将其传递给 SecurityContextHolder 中。如果身份验证失败,则会抛出 AuthenticationException。
(3)AuthenticationManager 会继续调用DaoAuthenticationProvider的authenticate() 进行验证
DaoAuthenticationProvider是一个AuthenticationProvider的实现类,用于处理用户名和密码验证的过程。当AuthenticationManager调用authenticate()方法时,实际上是委托给了DaoAuthenticationProvider来处理认证请求。
(4)DaoAuthenticationProvider也会调用 loadUserByUsername() 方法查询用户
在DaoAuthenticationProvider的authenticate()方法中,会先调用UserDetailsService的loadUserByUsername()方法获取用户信息,(这里根据实际情况可能去内存中查找,也可以去数据库查找)然后再将获取到的用户信息与用户输入的密码进行比较,最终确定用户是否通过认证。因此,在认证流程中,loadUserByUsername()方法是一个非常重要的环节。
(5)返回 UserDetail对象
loadUserByUsername() 方法主要是根据给定的用户名查询用户信息,并返回一个 UserDetails 对象。该方法的具体实现可能会涉及到访问数据库或其他存储设备,以获取用户的详细信息。在 Spring Security 中,这个方法通常由 UserDetailsService 的实现类来完成。在实现类中,通常会根据用户名查询用户信息,并将其封装为一个 UserDetails 对象,以便后续的身份验证过程中使用。UserDetails 对象包含用户的身份信息、授权信息和其他详细信息,如密码和是否启用等。
(6)通过PasswordEncoder对象对比UserDetail的密码和提交的密码是否一致
通过PasswordEncoder对象将用户输入的密码加密,然后与UserDetails中存储的加密后的密码进行比较,来验证用户的身份。如果两者一致,则认为用户身份验证通过。PasswordEncoder主要用于将密码进行加密,以提高安全性。
(7)如果正确就把UserDetails中的权限信息设置到Authentication对象中
(8)返回Authentication对象
(9)将认证成功的 Authentication
对象存储在 SecurityContextHolder
中
SecurityContextHolder 会使用 ThreadLocal 来存储认证对象,以确保每个线程都有自己的 SecurityContext 实例。在接下来的请求中,其他过滤器可以使用 SecurityContextHolder.getContext().getAuthentication() 方法获取该认证对象。
二、表单认证
1. 自定义表单登录页
login.html,放在static下
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login Page</title>
<style>
/* 样式可以自行修改 */
body {
background-color: cadetblue;
}
.login-form {
width: 350px;
margin: 150px auto;
background-color: #fff;
padding: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
h1 {
font-size: 24px;
text-align: center;
margin-bottom: 30px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
margin-bottom: 20px;
border: 2px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button {
background-color: darksalmon;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
width: 100%;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="login-form">
<h1>Login Page</h1>
<form th:action="@{/login}" method="post">
<label for="username">Username</label>
<input type="text" id="username" name="username" placeholder="Enter username">
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Enter password">
<button type="submit">Login</button>
</form>
</div>
</body>
</html>
spring%20security">2. 配置spring security
重写 configure方法,接收一个 HttpSecurity 对象,HttpSecurity提供了而很多配置相关的方法:
(1)authorizeRequests() 是 Spring Security 中的一个配置方法,用于定义哪些请求需要被授权才能被访问。该方法返回一个 ExpressionInterceptUrlRegistry 对象,用于配置针对 URL 的访问授权。
通过这个方法,我们可以使用各种方法来进行 URL 的授权配置,例如:
antMatchers()
方法用于匹配 URL,并设置需要的访问权限。hasRole()
和hasAuthority()
方法用于指定需要的角色或权限。permitAll()
方法用于指定不需要任何访问权限即可访问。
(2)formLogin()
formLogin()
是 Spring Security 中的一个配置方法,用于指定使用表单登录进行身份验证。在默认情况下,如果没有进行任何身份验证,Spring Security 会自动重定向到默认的登录页面。
通过这个方法,我们可以进行如下配置:
loginPage()
方法用于指定登录页面的 URL。loginProcessingUrl()
方法用于指定处理登录请求的 URL。usernameParameter()
和passwordParameter()
方法用于指定表单中用户名和密码的参数名称。successHandler()
和failureHandler()
方法用于指定登录成功和失败后的处理逻辑。permitAll()
方法用于指定登录页面的访问权限。
(3)csrf()
是 Spring Security 中的一个配置方法,用于配置跨站请求伪造(Cross-Site Request Forgery,CSRF)防护功能。CSRF 攻击是一种恶意攻击方式,攻击者通过某些方式获取到用户的授权信息,然后利用这些信息发送恶意请求,从而实现攻击目的。
在 Spring Security 中,CSRF 防护功能默认是开启的,如果想要关闭它,可以使用 csrf().disable()
方法进行禁用。
java">@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable();
}
}
3. 重启项目
访问 localhost:8080/hello,会跳转到:http://localhost:8080/login.html
三、认证与授权
1. 资源准备
(1)新建两个controller
一个是只有管理员才能访问的,一个是普通用户访问的。
java">@RestController
@RequestMapping("/admin/api")
public class AdminController {
@GetMapping("/hello")
public String helloAdmin() {
return "hello Admin!";
}
}
java">@RestController
@RequestMapping("/user/api")
public class UserController {
@GetMapping("/hello")
public String helloAdmin() {
return "hello User!";
}
}
(2)资源授权的配置
请求 /admin/api/** 下的资源,需要检查是否有 ADMIN 角色
请求 /user/api/** 下的资源,需要检查是否有 USER角色
java">@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable();
}
}
(3)重启服务访问
可以看到,访问 http://localhost:8080/hello 可以成功,访问http://localhost:8080/admin/api/hello会被拒绝访问(403错误码)
2. 基于内存的多用户支持
(1)在内存中配置用户
下面配置中的两个 configure(HttpSecurity http) 和configure(AuthenticationManagerBuilder auth)方法是 WebSecurityConfigurerAdapter 中的两个关键方法,它们用于配置Spring Security 的身份验证和授权。
configure(HttpSecurity http)
方法用于配置请求的授权规则,即哪些请求需要什么权限才能访问。
configure(AuthenticationManagerBuilder auth)
方法用于配置身份验证的方式,即如何验证用户身份。在这个示例中,我们使用了内存身份验证,通过调用.inMemoryAuthentication()
方法,然后使用.withUser()
方法来指定用户名和密码,使用{noop}
前缀来表示密码不进行加密,最后使用.roles()
方法来指定用户的角色。
java">@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}abcd").roles("ADMIN")
.and()
.withUser("zy").password("{noop}abc").roles("USER");
}
}
(2)访问测试
使用用户名 admin 密码 abcd ,来登录,可以显示下面内容:
3. 基于默认数据库模型的认证与授权
(1)创建数据库表
在数据库创建了两个用户,分别是:
user 对应的角色是 ROLE_USER;
admin 对应的角色是ROLE_ADMIN。
CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL
);
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
FOREIGN KEY (username) REFERENCES users(username)
);
INSERT INTO users (username, password, enabled) VALUES ('user', '12345', true);
INSERT INTO users (username, password, enabled) VALUES ('admin', '12345', true);
INSERT INTO authorities (username, authority) VALUES ('user', 'ROLE_USER');
INSERT INTO authorities (username, authority) VALUES ('admin', 'ROLE_ADMIN');
(2)引入jdbc依赖,配置数据库
引入jdbc和MySQL依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
配置数据库连接:
spring:
datasource:
url: jdbc:mysql://localhost:3306/security-db?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
spring%20securiy%E6%8E%88%E6%9D%83">(3)配置spring securiy授权
这里使用了 jdbcAuthentication() 方法来启用基于 JDBC 的用户存储,并通过 dataSource() 方法设置数据源,即连接到数据库的 DataSource。
接着,使用了 usersByUsernameQuery() 方法设置查询用户名、密码和启用状态的 SQL 语句,该语句会在用户登录时被执行,根据输入的用户名查询数据库中的用户信息,并将查询到的密码和启用状态用于认证。authoritiesByUsernameQuery() 方法用于设置查询用户角色的 SQL 语句。 最后,使用了 passwordEncoder() 方法设置密码加密方式,这里使用了 NoOpPasswordEncoder.getInstance() 方法来禁用密码加密,即直接将从数据库中查询到的密码作为明文进行比对。
java">@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("SELECT username, password, enabled FROM users WHERE username = ?")
.authoritiesByUsernameQuery("SELECT username, authority FROM authorities WHERE username = ?")
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
4. 基于自定义数据库模型的认证与授权
上面我们使用了InMemoryUserDetailsManager和JdbcUserDetailsManager两个UserDetailsService实现类。下面我们使用自定义的数据库模型,并且使用自定义的UserDetails实现类。
(1)数据库准备
java">CREATE TABLE my_users (
id INT(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT '1',
roles VARCHAR(200) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY username_UNIQUE (username)
);
插入两条数据:
(2)编写实体类User
这个实体类 User
与之前的表 my_users
是一一对应的,每个属性都对应着表中的一个字段:
id
对应着表中的id
字段,用于唯一标识每个用户username
对应着表中的username
字段,表示用户的登录名password
对应着表中的password
字段,表示用户的密码enabled
对应着表中的enabled
字段,表示用户是否启用roles
对应着表中的roles
字段,表示用户所拥有的角色
需要注意的是,这个 User
实体类中还有一个 authorities
属性,这个属性是用于保存用户的权限信息的,它不对应着表中的任何一个字段。这个属性在 UserDetailsServiceImpl
类的 loadUserByUsername
方法中会被设置为用户的权限信息,用于进行认证和授权。为了实现这个功能,User
类中还定义了一个 getAuthorities
方法,用于将 roles
字段解析成一个 List<GrantedAuthority>
类型的集合,并且在需要的时候进行懒加载。
java">@Data
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private boolean enabled;
private String roles;
private List<GrantedAuthority> authorities;
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//用于将 roles 字段解析成 List<GrantedAuthority> 类型的集合
public List<GrantedAuthority> getAuthorities() {
if (authorities == null) {
authorities = new ArrayList<>();
for (String role : roles.split(",")) {
authorities.add(new SimpleGrantedAuthority(role.trim()));
}
}
return authorities;
}
}
(3)编写自定义的UserDetailsServiceImpl
这个 UserDetailsServiceImpl
类实现了 UserDetailsService
接口,是用于加载用户信息的服务类。在 loadUserByUsername
方法中,通过 UserMapper
从数据库中查询到对应的 User
对象,然后构建出该用户对应的 GrantedAuthority
列表,最终将该 User
对象返回。
java">@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中查询用户信息
User user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 构建用户权限信息
List<GrantedAuthority> authorities = user.getAuthorities();
user.setAuthorities(authorities);
return user;
}
}
(4)编写Mapper
java">@Mapper
public interface UserMapper {
@Select("SELECT * FROM my_users WHERE username = #{username}")
User findByUsername(@Param("username") String username);
}
spring%20security%20%E9%85%8D%E7%BD%AE">(5)spring security 配置
java">@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private DataSource dataSource;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable();
}
//使用自定义的数据库模型进行认证和授权
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
(6)测试
打断点调试,可以看到在使用admin用户登录的时候可以获取到两个角色。