Spring Security 的基本使用
引言:使用 Spring Security 可以保护我们的Spring 程序, 避免某些恶意的请求。以下是有关Spring Security 的一些基本使用。
- 添加对应的 Maven 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.1</version>
</dependency>
-
添加类对应的 Maven 依赖之后, 运行此程序, 对于所有的URL请求都将会重定向到Spring Security 默认的登录界面。默认的用户名为 “user”, 密码打印在控制台上, 类似于以下这样:
在这次的输出中, “51fdbce9-0f29-4445-87ec-0bba83408c8f”就是这次的默认登录密码。
-
通过定义一个配置类, 继承
WebSecurityConfigurerAdapter
类, 重写父类方法void configure(AuthenticationManagerBuilder auth)
可以实现我们自定义的认证方式。
方式一: 基于内存的用户认证方式。通过将用户名、用户密码以及对应的用户权限等信息放在程序运行时的对象中,实现用户的认证。值得注意的是, 当设置密码时, Spring 将会检测密码的加密方式, 所以设置的密码格式应当为{加密方式}加密后的密码
的方式。例如, 以下的例子是一个基于内存对象的认证, 用户名为“Jack”, 用户角色为“USER_ROLE”,密码为 “123456”, 加密方式为bcrypt
加密后的密码为$2a$10$o/SQpYXDmLUrGf0IpB/.kOm1y9HSWzNDCxQMXrUTAfDxegNIxPapK
现在, 当跳转到登录页面时,输入用户名为 “Jack”, 密码为 “123456” 即可登录。
/*
基于内存中的用户认证对象实现对于用户的验证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("Jack")
.password("{bcrypt}$2a$10$o/SQpYXDmLUrGf0IpB/.kOm1y9HSWzNDCxQMXrUTAfDxegNIxPapK")
.authorities("USER_ROLE");
}
除了“bcrypt”加密方式外, Spring 还提供了 pbkdf2
、MD5
、SHA-512
等多种加密方式, 具体细节可以查看 org.springframework.security.crypto.factory.PasswordEncoderFactories
以及 org.springframework.security.crypto.password.DelegatingPasswordEncoder
方式二:基于JDBC 的用户认证。首先设置数据源, 这个在 application.yml
配置文件中即可配置, 在使用时注入即可。这样的话, Spring 将会自动帮助我们去查找数据表来验证用户。前提是配置的数据源中具有对应的数据表:users
、authorities
、group_authorities
等数据表, 因为这是默认的JDBC的SQL进行查找。
Spring默认的用户信息查找SQL:
如果不想这么做, 那么我们也可以自己配置对应的SQL来完成这一操作。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("SELECT user_name, user_password, is_enabled " +
"FROM user_role WHERE user_name=?")
.authoritiesByUsernameQuery("SELECT user_name, user_role " +
"FROM user_role WHERE user_name=?");
}
不用担心列名是否与Spring 的是否一致, 我们只需要保证对应的列:user_name、user_password、enabled的顺序是正确的即可。因为Spring 对于我们自己编写的SQL语句的用户认证的查找是按照列的索引位置来配置的, 因此保证对应查找的列的位置是对应用户名、用户密码和enabled即可。
同样的, 如果我们的密码没有满足对应的密码格式{加密方式}加密后的密码
,那么我们在登录时将会抛出找不到加密算法的异常, 因此我们必须在最后添加对应的加密方式。这里的加密方式是bcrypt
, 因此存储的密码必须是经过bcrypt
加密后的密码。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("SELECT user_name, user_password, is_enabled " +
"FROM user_role WHERE user_name=?")
.authoritiesByUsernameQuery("SELECT user_name, user_role " +
"FROM user_role WHERE user_name=?")
.passwordEncoder(new BCryptPasswordEncoder());
}
方式三:自定义用户认证,所对应的实体类应当实现 UserDetails
接口, 同时, 需要创建一个服务类, 实现 UserDetailsService
接口, 从而提供验证用户的功能。
修改 UserRole
类, 实现 UserDetails
接口:
import lombok.AccessLevel;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
@Data
@Entity
@RequiredArgsConstructor(access = AccessLevel.PUBLIC)
@Table(name = "user_role")
public class UserRole implements UserDetails {
@Id
@Column(name = "user_id")
private Long userId;
@Basic
@Column(name = "user_name")
private String userName;
@Basic
@Column(name = "user_password")
private String userPassword;
@Basic
@Column(name = "user_role")
private String userRole;
@Basic
@Column(name = "is_enabled")
private Boolean enabled;
// 用户角色信息集合, 这个不是对应表的列, 因此添加 @Transient 注解
@Transient
private Collection<SimpleGrantedAuthority> grantedAuthorityCollection;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.grantedAuthorityCollection;
}
@Override
public String getPassword() {
return this.userPassword;
}
@Override
public String getUsername() {
return this.userName;
}
@Override
public boolean isAccountNonExpired() {
return true; // 默认为 true
}
@Override
public boolean isAccountNonLocked() {
return true; // 默认为 true
}
@Override
public boolean isCredentialsNonExpired() {
return true; // 默认为 true
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
实现 UserRole 的Repository 接口:
import com.example.springsecurity.Entity.UserRole;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
/**
* 通过用户名来查找对应的用户对象
* @param userName : 查找的用户名
* @return : 查找到的用户角色对象
*/
UserRole findUserRoleByUserName(String userName);
}
创建 UserRoleDetailService 服务类, 实现 UserDetailsService 接口。
import com.example.springsecurity.Entity.UserRole;
import com.example.springsecurity.Repository.UserRoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
public class UserRoleDetailService implements UserDetailsService {
private final UserRoleRepository roleRepository;
@Autowired
public UserRoleDetailService(UserRoleRepository roleRepository) {
this.roleRepository = roleRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// 定义一个集合, 用于存储用户角色(即权限)的集合。该集合是为了存储用户角色的类型
Collection<SimpleGrantedAuthority> collection = new ArrayList<>();
// 遍历所有的用户, 将他们的角色信息添加到集合中
for (UserRole role: roleRepository.findAll())
collection.add(new SimpleGrantedAuthority(role.getUserRole()));
// 使用流过滤掉相同的元素
collection = collection.stream()
.distinct()
.collect(Collectors.toList());
// 通过用户名查找对应的用户信息
UserRole userRole = roleRepository.findUserRoleByUserName(username);
// 将存在的用户角色集合注入到用户信息对象中
userRole.setGrantedAuthorityCollection(collection);
return userRole;
}
}
现在, 使用 UserDetailsService 服务对象来完成我们自定义的用户验证。同样地, 也需要添加对应的加密方式, 否则Spring 将会解析对应密码, 获取指定的加密方式(这将导致无法找到加密算法,从而抛出异常), 最好的方法还是在方法内定义加密方式, 这里我们设置为 bcrypt
加密。
所以最后的 configure()
方法为下面所示:
/*
使用用户自定义的验证方式进行验证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userRoleDetailService)
.passwordEncoder(new BCryptPasswordEncoder());
}
再次打开 http://127.0.0.1:8080/, 进入登录界面, 输入对应信息, 即可完成验证。
- 保护 Web 请求, 有注册才有登录, 完全重定向所有的请求不是一个很好的选择。所以必须设置对应访问路径的请求验证。
可以通过对某个路径设置访问权限来实现这个目标, 比如, 我们希望将 “/better’ 和 ”/wonderful“ 访问路径设置为只有角色为”ROLE_ADMIN“ 的角色才能访问, 而对于”/home“路径对于所有的用户都可以访问, 那么可以这么做:
注意: Spring 对于用户角色的定义, 是以 “ROLE_”为前缀, 后面再接具体的角色名。比如:ROLE_ADMIN 则表示这是一个 ADMIN 的角色。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/better", "/wonderful")
.hasRole("ROLE_ADMIN")
.antMatchers("/home").permitAll()
}
除了 hasRole()
和 permitAll()
之外, Spring 还提供了许多的路径配置方法:
同时, access()
方法通给定的 SpEL (Spring Expression Language Spring扩展语言),判断 SpEL结果 来进行访问验证。
我们希望对于对于除了某些指定的URL外, 对于其它URL的访问都需要经过认证后才能访问, 那么我们可以在最后的认证后添加 .anyRequest().authenticated()
来实现, 具体代码如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/wonderful")
.access("hasRole('ROLE_ADMIN')")
/* '/wonderful' 的请求需要用户具有 “ROLE_ADMIN”的角色*/
.antMatchers("/home", "/login")
.access("permitAll()")
/* 对于 “/home”, "/login" 允许所有的请求访问。
“/login”在这里被允许任何请求访问是必须的, 因为所有的需要认证的
请求都会重定向到 “/login”, 如果 “/login” 也是需要被认证的,
即不是允许所有请求的访问, 那么“/login” 的请求将不断重定向
到“/login”, 直至浏览器崩溃。*/
/*
‘/login’ 是登录的界面的URL, 它是可以修改的,
因此当 “/login” 修改为其它的URL时, 请务必将它设置为是允许所有请求访问的。
*/
.anyRequest()
.authenticated();
}
这里需要注意的地方便是对于 “/login”的访问, 它应当是允许所有的请求访问的, 如果不是的, 那么将会导致浏览器的崩溃。
要处理登录, Spring 为我们提供了默认的对于Post
的/login
URL。因此我们在编写HTML的登录表单时, 需要将处理路径, 即 form
的 action
属性设置为/login
。用户名输入框的 name
属性请设置为 username
, 密码输入框的 name
属性请设置为 password
, 因为这是Spring 的默认参数名, 如果不想折腾的话, 还是按照它原有的设计是一个更好的选择。
<form th:action="@{/login}" method="post">
<!-- Email input -->
<div class="form-outline mb-4">
<input type="text" id="username" name="username" class="form-control"/>
<label class="form-label" for="username">user name</label>
</div>
<!-- Password input -->
<div class="form-outline mb-4">
<input type="password" id="password" name="password" class="form-control"/>
<label class="form-label" for="password">Password</label>
</div>
<div class="col">
<!-- Simple link -->
<a th:href="@{/register}">Register</a>
</div>
<!-- Submit button -->
<button type="submit" class="btn btn-primary btn-block">Sign in</button>
</form>
如果想替换默认的登录界面, 可以通过设置 HttpSecurity 的 formLogin().loginPage(String)
方法即可, 方法的参数名为处理登录的页面。通过 defaultSuccessUrl()
可以设置认证成功后跳转到的URL。示例如下:
.and()
.formLogin()
.loginPage("/login")
/*
用户认证登录界面的 URL,
请注意这个 URL 应当是允许任何请求访问的
*/
.loginProcessingUrl("/login")
/*
登录信息提交的处理URL, 由于Spring为我们提供了默认的
“/login”处理URL, 因此设置为 “/login”
*/
.defaultSuccessUrl("/better", true)
/*
认证成功后的默认跳转界面, 如果不设置第二个参数, 或者设置为false,
那么在认证成功后将会访问之前被重定向的页面。
设置第二个参数为true将强制在认证成功后跳转到指定的页面, 在这里是 “/better”
*/
.failureUrl("/error")
/*
认证失败时的默认重定向URL。
*/
使用 HttpSecurity
的 logout()
方法完成退出登录状态动作。Spring 为我们提供了默认的退出登录URL “/logout”, 它应当是允许所有的i请求访问的。
.and()
.logout() // 退出登录
.logoutSuccessUrl("/home") // 退出登录后的页面 URL
.permitAll(); // 允许任何请求访问
- 了解发送请求的用户。对于已经认证过的用户, Spring 提供了四种方式获取认证的用户信息。
方式一:注入Principal对象到控制器方法中
/**
* 注入Principal对象到控制器方法中, 获取已认证的用户信息
* @param principal : 注入的 Principal 对象
* @return : 得到的用户信息
*/
@GetMapping(path = "/checkUserByPrincipal")
public @ResponseBody
String checkUserByPrincipal(Principal principal) {
UserRole role = roleRepository.findUserRoleByUsername(principal.getName());
assert role != null; // 用户应当是已经登录的, 所以它应当是已经存在的。
return role.toString();
}
方式二:注入 Authentication 对象到控制器方法中
/**
* 注入 Authentication 对象到控制器方法中, 获取已认证的用户信息
* @param authentication : 注入的 Authentication 对象
* @return : 获取到的用户信息
*/
@GetMapping(path = "/checkUserAuth")
public @ResponseBody
String checkUser(Authentication authentication) {
UserRole userRole = (UserRole) authentication.getPrincipal();
return userRole.getUserRole();
}
方式三:添加 @AuthenticationPrincipal
注解到用户信息对象上, 表明这个用户信息对象是认证的 Principal 对象。
/**
* 添加 @AuthenticationPrincipal 注解,
* 表明这个 UserRole 对象是一个已经认证的 Principal 对象。
* @param userRole : 认证过的 principal 对象
* @return : 认证的 UserRole 对象
*/
@GetMapping(path = "/checkUserByAuthAnnotation")
public @ResponseBody
String checkUserByAuthAnnotation(@AuthenticationPrincipal UserRole userRole) {
return userRole.toString();
}
方式四:通过上下文来获取已认证的用户信息。
/**
* 通过上下文来获取已认证的用户信息
* @return : 根据上下问获取到的用户信息
*/
@GetMapping(path = "/checkUserByContext")
public @ResponseBody
String checkUserByContext() {
// 获取上下文的认证信息
Authentication authentication = SecurityContextHolder
.getContext().getAuthentication();
UserRole userRole = (UserRole) authentication.getPrincipal();
assert userRole != null;
return userRole.toString();
}
推荐使用后三种方式。
示例地址:https://github.com/LiuXianghai-coder/Spring-Study/tree/master/spring-security