简介
Spring Security为应用程序提供了方便的登陆和鉴权的 API ,默认启用各项安全配置,能够简化大量的应用程序安全需求开发。不过由于这些默认启用的各项安全配置,也会让第一次集成Spring Security的开发人员有一定的上手难度。
使用环境:Java 11 和 Spring Boot 2.3.5.RELEASE
集成Spring Security
在Spring Boot中集成Spring Security与集成其他的框架类似,在pom.xml
里引入相关的starter就可以:
<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>
由于大部分的使用场景还是web,所以这里也引进Spring MVC做示例。
现在建立一个Controller
作为简单的示例:
@RestController("foods")
public class FoodController {
private List<String> foods = List.of("Bread","Sandwich");
@GetMapping
public List<String> getFood(){
return foods;
}
@PostMapping
public void addFood(String food){
foods.add(food);
}
@DeleteMapping
public void deleteFood(String food){
boolean removed = foods.remove(food);
if (!removed) {
throw new IllegalArgumentException("no such food : " + food);
}
}
}
这时启动项目,会看到在控制台会有类似输出:
2020-11-26 23:27:23.376 INFO 5264 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: f091d11a-2524-40bf-b29d-a6579f6b7a5a
这里输出的是随机密码,并且访问http://localhost:8080/foods
时,会进入一个页面要求输入账号和密码,默认账号是user
,密码就是控制台输出的这串随机密码,填上后就能看到输出了:
["Bread","Sandwich"]
可以看到集成Spring Security后,默认就有了登陆功能。但是大多数项目不会只有一个用户,也不会使用默认的简洁的页面,所以下面介绍如何从数据库中加载用户,如何替换默认登陆页面。
Authentication
Authentication(Who are you) 就是证明你拥有这个账户,也就是通常所说的登陆流程。
配置模板
Spring Security可以帮助简化登陆流程,先需要配置对应的Spring Security配置,配置模板通常如下:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
- 为类加上
@Configuration
和@EnableWebSecurity
注解,表示为Spring Security的配置类 - 继承
WebSecurityConfigurerAdapter
,覆盖需要重写的方法
配置数据库和相关实体
通常的表单登陆,都是从数据库中查询对应用户名和密码,匹配正确就登陆成功。这里建立一个简单的示例表和数据:
create table t_user
(
id bigint auto_increment
primary key,
username varchar(255) not null,
password varchar(255) not null,
permission varchar(255) null
);
-- 密码为BCrypt哈希后的结果,密码是admin
INSERT INTO spring_security_demos.t_user (id, username, password, permission) VALUES (1, 'admin', '$2y$12$dsYcIyVxO5aOtfybzsuUr.Gc8gK8DdcqaRPr.pg6UcYODnhYET1RC', 'FOODS_VIEW,FOODS_EDIT');
再引入spring data jdbc
和mysql
的驱动来连接数据库:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
再创建对应的repository和entity,用于数据库操作和映射数据库表:
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByUsername(String username);
}
@Table("t_user")
public class User {
@Id
private Long id;
private String username;
private String password;
private String permission;
// GETTER SETTER
}
实现Spring Security的登陆相关接口
Spring Security有几个接口来定义登陆的相关操作,这里介绍常用的UserDetails
和UserDetailsService
接口。
UserDetailsService
接口只有一个loadUserByUsername
方法,表示从数据库或某些地方通过 username 加载用户信息,然后包装成UserDetails
返回。
现在先来简单实现UserDetails
接口:
public class UserDetail implements UserDetails {
private final String username;
private final String password;
private Set<SimpleGrantedAuthority> authorities = Collections.emptySet();
public UserDetail(String username, String password) {
this.username = username;
this.password = password;
}
public UserDetail(String username, String password, Set<SimpleGrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
再来实现UserDetailsService
接口:
public class JdbcUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
public JdbcUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("user can't found by username : " + username));
return new UserDetail(user.getUsername(), user.getPassword());
}
}
密码Hash算法
Spring Security在加载用户之后,会对提交的密码进行Hash后,再对比UserDetails
中的密码。可以在配置中指定密码的Hash算法,用法在下面的配置中演示。
配置登陆接口
实现了登陆相关接口后,需要配置到上面说到的配置模板中,使这些实现能接入到Spring Security。配置如下:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
@Bean
@Override
protected UserDetailsService userDetailsService() {
return new JdbcUserDetailsService(userRepository);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
到这里,主要登陆相关的配置就完成了。
这时启动程序后,在浏览器中输入:http://localhost:8080/foods
,这时会看到Spring Security默认的登录页,输入admin/admin
登陆后,就可以看到接口的响应了:
["Bread","Sandwich"]
自定义登陆页面
想要自定义登陆页面,可以在SecurityConfig
配置中覆盖方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and().httpBasic()
.and().formLogin()
.loginPage("/login.html")
.defaultSuccessUrl("/foods",true)
.loginProcessingUrl("/login").permitAll()
.and().csrf().disable();
}
loginPage
方法接收登陆页地址defaultSuccessUrl
方法接收登陆成功之后的跳转地址loginProcessingUrl
方法指定处理验证登陆的方法permitAll()
表示这些登陆相关方法不需要登陆就能调用,如果漏了这个,会导致循环重定向
Authorization
Authorization(Access Control)就是鉴权,判断某个用户是否有权限进行某些操作。
可以看到UserDetail
中预留的authorities集合就是该用户拥有权限的集合,数据库中的permission
列就是权限的集合,用逗号隔开。实际的项目中,一般是通过用户赋予的角色,再关联查出所有的权限KEY。
JdbcUserDetailsService
的loadUserByUsername
稍微修改一下,把权限加载进UserDetail
中:
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("user can't found by username : " + username));
Set<SimpleGrantedAuthority> permissions = Arrays.stream(user.getPermission().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
return new UserDetail(user.getUsername(), user.getPassword(), permissions);
}
配置权限验证注解
再上面提到的SecurityConfig
类中,加上@EnableGlobalMethodSecurity
注解,让Spring Security开启注解权限验证:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
开启之后,再在Controller的对应方法上,加上对应的注解来为方法增加权限验证,以上面的FoodController
为例:
@GetMapping
@PreAuthorize("hasAuthority('FOODS_VIEW')")
public List<String> getFood(){
return foods;
}
@PostMapping
@PreAuthorize("hasAuthority('FOODS_EDIT')")
public void addFood(String food){
foods.add(food);
}
@DeleteMapping
@PreAuthorize("hasAuthority('FOODS_EDIT')")
public void deleteFood(String food){
boolean removed = foods.remove(food);
if (!removed) {
throw new IllegalArgumentException("no such food : " + food);
}
}
此时可以重复登陆,并调整t_user
表的permission
列查看效果。比如,如果permission
列中没有FOODS_VIEW
权限,那么访问该接口的时候,接口会返回一个403错误,表示没有权限访问该接口。
总结
本文主要总结了Spring Security整合Spring Boot的基本用法,示例工程在Github上。
评论
发表评论