Spring全家桶-Spring Security之多用户管理
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
为什么需要多用户?
Spring Security 默认中的用户是单一的用户,系统自带或者通过配置进行设置默认的用户名和密码,但是身为一个系统,总不会只有一个人去使用并且这个用户还是固定在系统的中🤔。并且角色也不是只有一个角色。如果我们需要修改,还需要修改配置文件,并且还要重启应用。这样用起来很不方便,那我们就需要多用户,并且还需要通过不同的用户的角色进行管理配置。
一、通过内存管理多用户
Spring Security 为我们提供了相应的接口进行操作,我们只需要实现一个自定义的UserDetailsService 接口即可。
通过InMemoryUserDetailsManager进行多用户管理
搭建环境
`我们没有通过其他的相关jar包的依赖,因此也不用导入新的jar包`
pom.xml
<dependencies>
<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>
</dependencies>
创建配置类WebSecurityConfig
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/books/**").hasAnyRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("ADMIN","USER")
.antMatchers("/").permitAll()
.and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable();
}
}
创建BookController
@RestController
@RequestMapping("/books/")
public class BookController {
@GetMapping("index")
public String index(){
return "index";
}
@GetMapping("list")
public String list(){
return "list";
}
}
创建IndexController
@RestController
public class IndexController {
@GetMapping("/")
public String index(){
return "index";
}
}
创建UserController
@RestController
@RequestMapping("/user/")
public class UserController {
@GetMapping("index")
public String index(){
return "index";
}
@GetMapping("list")
public String list(){
return "index";
}
}
在启动类中声明一个bean
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("admin").password("admin").roles("ADMIN").build());
inMemoryUserDetailsManager.createUser(User.withUsername("user").password("user").roles("USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("user1").password("user1").roles("USER").build());
return inMemoryUserDetailsManager;
}
登陆页和之前的一样即可
启动的时候,我们访问http://localhost:8080 访问,将显示主页。因为主页我们设置的权限是开放的权限,不需要登陆。
当我们访问user或者books的时候就需要登陆了。 登陆一下试试。 报错了,什么鬼?报如下错误:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:254) ~[spring-security-crypto-5.6.2.jar:5.6.2]
at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:202) ~[spring-security-crypto-5.6.2.jar:5.6.2]
at org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:76) ~[spring-security-core-5.6.2.jar:5.6.2]
at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:147) ~[spring-security-core-5.6.2.jar:5.6.2]
这个是DelegatingPasswordEncoder 抛出来的异常。
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
private UnmappedIdPasswordEncoder() {
}
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
String id = DelegatingPasswordEncoder.this.extractId(prefixEncodedPassword);
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
}
官方spring说,需要进行密码进行加密,因为没有设置密码加密的策略。因此我们需要修改用户创建的地方,将密码设置的时候,指定密码加密策略。
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("ADMIN").build());
inMemoryUserDetailsManager.createUser(User.withUsername("user").password(new BCryptPasswordEncoder().encode("user")).roles("USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("user1").password(new BCryptPasswordEncoder().encode("user1")).roles("USER").build());
return inMemoryUserDetailsManager;
}
同时在WebSecurityConfig 中添加如下信息:
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
我们重启项目再登陆试试: 证明是登陆成功,并且是根据不同的权限进行访问。 我们用user没有权限的时候,应该为报403的错误。 现在内存维护多用户就到这了
二、通过数据库管理多用户
通过JdbcUserDetailsManager进行多用户管理
JdbcUserDetailsManager帮助我们以JDBC的方式对接数据库和Spring Security, 它设定了一个默认的数据库模型
搭建环境
因为我们需要使用jdbc和数据库(选用mysql),因此我们多引入两个包:1.spring-boot-starter-jdbc 2.mysql-connector-java ,完整的POM:
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
创建数据库脚本
create database `spring-security-learn`;
use `spring-security-learn`;
create table users(
username varchar(50) not null primary key,
`password` varchar(500) not null,
enabled boolean not null
);
create table authorities (
username varchar(50) not null,
authority varchar(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
以上是Spring Security 提供的user的ddl语句,在/org/springframework/security/core/userdetails/jdbc/users.ddl 中,不同的数据库进行相应的调整即可
修改application.yml
因为我们使用的jdbc的操作,因此我们需要在配置文件中添加数据库相应的链接信息和用户信息。完整的配置如下:
server:
port: 8080
spring:
datasource:
password: 自己的数据库密码
username: 自己的数据库用户名
url: jdbc:mysql:///spring-security-learn?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc:mysql:///spring-security-learn 在使用localhost:3306的情况下,这个可以省略不写。
创建程序入口JdbcUserApplication
代码如下:
public class JdbcUserApplication {
@Autowired
private DataSource dataSource;
public static void main(String[] args) {
SpringApplication.run(JdbcUserApplication.class,args);
}
@Bean
public UserDetailsService userDetailsService(){
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager();
jdbcUserDetailsManager.setDataSource(dataSource);
jdbcUserDetailsManager.deleteUser("admin");
jdbcUserDetailsManager.deleteUser("user");
jdbcUserDetailsManager.deleteUser("user1");
jdbcUserDetailsManager.createUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("ADMIN").build());
jdbcUserDetailsManager.createUser(User.withUsername("user").password(new BCryptPasswordEncoder().encode("user")).roles("USER").build());
jdbcUserDetailsManager.createUser(User.withUsername("user1").password(new BCryptPasswordEncoder().encode("user1")).roles("USER").build());
return jdbcUserDetailsManager;
}
这里启动程序的时候,就会将用户写入到数据库中。应为User.ddl是将username作为主键的,如果用户已经存在的情况下进行运行的话,数据库会报重复主键 的错误。其他的代码和内存中的一致,这里就不一一贴出来了。之后运行项目,会和前面内存的现实一样。
二、代码分析
我们上面看到,两种方式都是通过UserDetailService 进行处理。 UserDetailService的Cache,InMemory,jdbc等实现方式,包括我们后面自定义的方式也是可以的,和这几种方式一样的方式即可。 内存进行管理多用户和数据库管理都是实现了UserDetailManager 的接口,这个接口提供的创建,删除,修改,用户是否存在等方法。
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
InMemoryUserDetailsManager 的实现:
public void createUser(UserDetails user) {
Assert.isTrue(!this.userExists(user.getUsername()), "user should not exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
public void deleteUser(String username) {
this.users.remove(username.toLowerCase());
}
public void updateUser(UserDetails user) {
Assert.isTrue(this.userExists(user.getUsername()), "user should exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
public boolean userExists(String username) {
return this.users.containsKey(username.toLowerCase());
}
public void changePassword(String oldPassword, String newPassword) {
Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
if (currentUser == null) {
throw new AccessDeniedException("Can't change password as no Authentication object found in context for current user.");
} else {
String username = currentUser.getName();
this.logger.debug(LogMessage.format("Changing password for user '%s'", username));
if (this.authenticationManager != null) {
this.logger.debug(LogMessage.format("Reauthenticating user '%s' for password change request.", username));
this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword));
} else {
this.logger.debug("No authentication manager set. Password won't be re-checked.");
}
MutableUserDetails user = (MutableUserDetails)this.users.get(username);
Assert.state(user != null, "Current user doesn't exist in database.");
user.setPassword(newPassword);
}
}
public UserDetails updatePassword(UserDetails user, String newPassword) {
String username = user.getUsername();
MutableUserDetails mutableUser = (MutableUserDetails)this.users.get(username.toLowerCase());
mutableUser.setPassword(newPassword);
return mutableUser;
}
内存存储用户是通过Map进行处理。
private final Map<String, MutableUserDetails> users = new HashMap();
JdbcUserDetailsManager 的实现是通过JdbcDaoSupport 进行获取JdbcTemplate 进行数据库操作。
public final void setDataSource(DataSource dataSource) {
if (this.jdbcTemplate == null || dataSource != this.jdbcTemplate.getDataSource()) {
this.jdbcTemplate = this.createJdbcTemplate(dataSource);
this.initTemplateConfig();
}
}
protected JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Nullable
public final DataSource getDataSource() {
return this.jdbcTemplate != null ? this.jdbcTemplate.getDataSource() : null;
}
public final void setJdbcTemplate(@Nullable JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.initTemplateConfig();
}
总结
Spring Security支持各种来源的用户数据, 包括内存、 数据库、 LDAP等。 它们被抽象为一个UserDetailsService 接口, 任何实现了UserDetailsService 接口的对象都可以作为认证数据源。 在这种设计模式下, Spring Security 显得尤为灵活。
|