Spring Security7、使用动态用户进行登录
在前面的文章中,我们登录的账号和密码都是写在配置文件 SecurityConfig 中的。
这样的写法只适用于用户固定、密码不进行修改的情况,如果常常有新用户注册进来,并且用户还需要可以自己修改密码等这一系列的操作,那登录用户的信息我们就得存放在数据库,然后登录时动态读取。
除了登录用户的账号密码需要动态读取,用户拥有权限也应该动态从数据库中读取。
本篇文章我们主要说的登录的账号密码动态读取的问题,权限后面文章再说。
一、移除固定账号
既然是动态加载的账号和密码,那么我们之前写的固定账号和密码就得删掉,然互再使用其他的方式进行动态加载账号密码。我们现在把 SecurityConfig 配置文件的下面这段代码删除掉。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("admin", "guest")
.and()
.withUser("user")
.password(new BCryptPasswordEncoder().encode("000000"))
.roles("guest");
}
二、实现加载账号的接口
在进行登录时,我们一般会使用用户输入账号来查询到对应的用户信息,然后使用用户上传的明文密码进行加密后和查询出来的用户的密文密码进行对比,以验证用户输出的账号密码是否正确。
那么Security也应该有方法进行类似的操作,在Spring Security中,有一个叫做 UserDetailsService
的接口,里面就有一个需要实现的方法 loadUserByUsername
。
就和它的名字一样,它就可以用来实现我们自定义加载账号的功能。
我们现在实现这个接口:
(1)在这之前,我们创建一个用户实体类,用于模拟数据库生成的数据。
【注意要实现UserDetails, CredentialsContainer接口,不然这个用户不能作为登录使用,因为 UserDetailsService
接口中的 loadUserByUsername
方法的返回值只能是 UserDetails
】
import lombok.Data;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Date;
/**
* 用户信息
*
* @author lixin
*/
@Data
public class SysUser implements UserDetails, CredentialsContainer {
/*** 记录id(数据库自增) */
private Long id;
/*** 创建时间(创建设置) */
private Date gmtCreate;
/*** 修改时间(创建时可以不填入,每次修改必须设置) */
private Date gmtModified;
/*** 逻辑删除(true表示被逻辑删除) */
private Boolean isDelete;
/*** 操作人(每次操作更新操作人) */
private String operator;
/*** 排序字段 */
private BigDecimal sort;
/*** 乐观锁字段,每次更新记录+1 */
private Long version;
/*** 用户名 */
private String username;
/*** 密码 */
private String password;
/*** 用户状态 */
private Integer status;
/*** 备注 */
private String remarks;
/*** 权限集合 */
private Collection<GrantedAuthority> authorities;
@Override
public void eraseCredentials() {
this.password = null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.status == 0;
}
@Override
public boolean isAccountNonLocked() {
return this.status != 1;
}
@Override
public boolean isCredentialsNonExpired() {
return this.status != 2;
}
@Override
public boolean isEnabled() {
return this.status != -1;
}
}
(2)然后实现 UserDetailsService
接口:
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.lang.Console;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 自定查询UserDetails
*
* @author lixin
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Console.log("根据用户名查询UserDetails,{}", username);
// 在模拟的数据中,根据传入的用户名筛选出对应的用户信息
Optional<UserDetails> first = userDetailsList().stream()
.filter(userDetails -> Objects.equals(userDetails.getUsername(), username))
.findFirst();
if (first.isPresent()) {
return first.get();
}
// 如果模拟数据库不存在输入的用户名的,就直接抛出异常,表示登录失败
// 这里抛出的异常会被 JsonFailureHandler 这个登录失败的处理程序拦截到
throw new BadCredentialsException("[" + username + "]用户不存在");
}
/**
* 正常情况下,我们应该是去数据库中查询数据,但是为了方便测试,这里就使用模拟出来的数据
* 当然,除了到数据库中去查询,这里我们也可以去文件中、内存中、甚至到网络上去爬取的方式来获取到用户信息,只要能提供数据来源就行了。
*
* 模拟从数据库查询出来的用户信息列表,生成的账号密码为:
* suer0 pwd_0
* suer1 pwd_1
* suer2 pwd_2
* ....
* suer9 pwd_9
*/
private List<UserDetails> userDetailsList() {
List<UserDetails> userDetails = new ArrayList<>(10);
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
SysUser user;
for (int i = 0; i < 10; i++) {
user = new SysUser();
user.setId(Convert.toLong(i));
user.setGmtCreate(DateTime.now());
user.setOperator("管理员");
user.setIsDelete(false);
user.setSort(BigDecimal.valueOf(0));
user.setStatus(0);
user.setRemarks("测试用户" + i);
user.setUsername("user" + i);
user.setPassword(passwordEncoder.encode("pwd_" + i));
userDetails.add(user);
}
// 打印输出userDetails的信息
userDetails.forEach(Console::log);
return userDetails;
}
}
三、配置自定义查找用户的实现类
我们又再次来到SecurityConfig配置文件,把我们自定义的实现类设置到 AuthenticationManagerBuilder
上,这样在进行登录时,就会使用我们自定义的方法去进行查询用户的信息,然后进行密码的校验。
import com.miaopasi.securitydemo.config.security.handler.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Security配置类,会覆盖yml配置文件的内容
*
* @author lixin
*/
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JsonSuccessHandler successHandler;
private final JsonFailureHandler failureHandler;
private final JsonAccessDeniedHandler accessDeniedHandler;
private final JsonAuthenticationEntryPoint authenticationEntryPoint;
private final JsonLogoutSuccessHandler logoutSuccessHandler;
private final UserDetailsServiceImpl userDetailsService;
@Autowired
public SecurityConfig(JsonSuccessHandler successHandler, JsonFailureHandler failureHandler, JsonAccessDeniedHandler accessDeniedHandler, JsonAuthenticationEntryPoint authenticationEntryPoint, JsonLogoutSuccessHandler logoutSuccessHandler, UserDetailsServiceImpl userDetailsService) {
this.successHandler = successHandler;
this.failureHandler = failureHandler;
this.accessDeniedHandler = accessDeniedHandler;
this.authenticationEntryPoint = authenticationEntryPoint;
this.logoutSuccessHandler = logoutSuccessHandler;
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/code", "/login", "/logout", "/doLogin").permitAll()
.antMatchers("/admin/**", "/guest/**").hasRole("admin")
.antMatchers("/guest/**").hasRole("guest")
.anyRequest().authenticated()
.and().formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/doLogin")
.successHandler(successHandler)
.failureHandler(failureHandler)
.and().logout().logoutUrl("/doLogout")
.logoutSuccessHandler(logoutSuccessHandler)
.and().exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
.and().cors()
.and().csrf().disable();
}
/*** 注入密码的加密方式 */
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 注册userDetailsService,设置密码加密方式
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
四、测试
由于我们在模拟数据时,模拟的账号密码如下:
编号 | 账号 | 密码 |
---|---|---|
1 | user0 | pwd_0 |
2 | user1 | pwd_1 |
3 | user2 | pwd_2 |
4 | user3 | pwd_3 |
5 | user4 | pwd_4 |
6 | user5 | pwd_5 |
7 | user6 | pwd_6 |
8 | user7 | pwd_7 |
9 | user8 | pwd_8 |
10 | user9 | pwd_9 |
现在我随便输入一个账号user6,密码pwd_6进行登录,发现正常返回JSON字符串:
{
"msg": "登录成功",
"code": 0,
"data": {
"authenticated": true,
"authorities": [],
"principal": {
"isDelete": false,
"sort": 0,
"gmtCreate": 1594660220211,
"operator": "管理员",
"id": 6,
"remarks": "测试用户6",
"username": "user6",
"status": 0
},
"details": {
"sessionId": "E61B1F9B042D61F85A1A1E4B843C4A91",
"remoteAddress": "127.0.0.1"
}
}
}
但是如果我输入错误的密码pwd_666,则返回JSON字符串:
{
"msg": "登录失败",
"code": 1000,
"data": "账号密码输入有误"
}
如果我们输入一个没有模拟过的账号试一试,比如说:root,返回JSON字符串如下:
{
"msg": "登录失败",
"code": 1000,
"data": "[root]用户不存在"
}
五、简单说一下
本篇文章主要是在说明如何使用动态用户进行登录,其中在 UserDetailsServiceImpl 中使用 userDetailsList 来模拟数据,在 loadUserByUsername 方法中根据账号获取模拟数据中的用户信息。
在我们实际的开发项目中,我们更多的是调用 根据用户账号查询到用户信息
的方法到数据库中查询到对应的用户信息,如果查询不到直接抛出用户名不存在的异常。
如果想要把本篇文章的代码应用到你的项目:
(1)你应该有一个自己的用户实体类,比如模拟数据中的 SysUser。
(2)实现实体 SysUser 对应的CRUD的操作,然后提供一个根据账号查询用户信息的方法。我们这里假设你写的业务逻辑处理类的名字叫 SysUserService,根据账号查询用户信息方法的名字叫 findByUserName。
(3)我们在 UserDetailsServiceImpl 这个类中注入 SysUserService 。
(4)然后在 UserDetailsServiceImpl 的 loadUserByUsername 方法中,使用你 SysUserService 中定义的 findByUserName 的方法,到数据库中查询到对应的用户信息。
(5)基本上这样你就可以动态的在数据库中获取到用户信息了。
spring security系列文章请 点击这里 查看。
这是代码 码云地址 。
注意注意!!!项目是使用分支的方式来提交每次测试的代码的,请根据章节来我切换分支。