本章的内容较为简单,暂时先不涉及数据库的操作,只是为了演示,在Spring Security 中,如何通过其暴露的接口来自定义处理用户登录过程的逻辑。
想要了解用户登录的逻辑,我们只需要处理三个问题:
- a.用户信息获取逻辑
- b.如何校验用户信息
- c.处理用户加密解密
1.spring security 用户信息获取 UserDetailsService
用户信息的获取逻辑在Spring Security 中是被封装在一个叫UserDetailsService的接口里的,在这个接口中,只有一个方法,loadUserByUsername()。接口接收一个String参数,返回一个UserDetails对象。顾名思义,就是要传递我们的username,然后根据这个username去找到这个User的用户信息。如果找不到这个用户,就会抛出一个UsernameNotFoundException,Spring Security 捕获到这个异常,就会返回相应的错误信息。
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
接下来,我们自己写一个类,实现这个UserDetailsService
@Component
public class MyUserDetailsService implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录的用户名:" + username);
return new User("admin","123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
我们调用了这个loadUserByUsername方法,要返回的是UserDetails对象。可以看到代码中我是直接返回一个Spring Security中的User对象。这个User对象的源码我们来看看,它提供了两种构造方法
/**
* 这个构造方法三个参数:username代表用户名,password代表密码,第三个参数 authorities表示用户权限的集合。而这个方法具体实现则是调用了另一个构造方法。并且多了四个值为true的参数。接下来看另一个构造方法。
*/
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
/**
* enabled:用户是否启用,accountNonExpired:用户是否过期,
* credentialsNonExpired:用户凭证是否过期,accountNonLocked:用户是否被锁定。
*/
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (username != null && !"".equals(username) && password != null) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
2.用户信息校验 UserDetails
User对象和UserDetails又是什么关系呢? 现在一起来看看UserDetails对象的源码实现
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetails是个接口(interface),那难道说是User对象实现了UserDetails接口吗?答案是肯定的。
public class User implements UserDetails, CredentialsContainer {
// 省略代码
}
isAccountNonExpired(标识用户是否过期了,也就是用户是否被冻结,有些应用可能需要)
isAccountNonLocked(标识用户是否被锁定,一般用于用户输入密码错误次数的验证)
isCredentialsNonExpired(标识用户密码是否过期,有些网站为了安全性起见,可能会对用户密码的使用时间做验证)
isEnabled(用户是否启用,一般来说,对于应用,我们是不会删除用户数据的,如果用户注销的话,我们只需标识用户不可用就好了)
如果以上判断中,并不需要用到,那只要返回true就好了。
3.用户信息加解密 PasswordEncoder
再提一下用户对用户密码加解密的方法: PasswordEncoder。
先来写个配置类
@Configuration
@EnableWebSecurity // 启用Spring Security
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入 MyUserDetailsSerivce
@Autowired
private MyUserDetailsService userDetailsService;
// 创建PasswordEncoder Bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 设置form 登录方式,且成功跳转至hello页面
http.formLogin().successForwardUrl("/hello")
// 设置所有的请求都要经过验证
.and().authorizeRequests().anyRequest().authenticated()
// 设置自定义的UserDetailsService
.and().userDetailsService(userDetailsService);
}
}
OK, 接下来我们稍微对我们自定义的UserDetailsService实现做些变化,利用passwordEncoder来加密我们的密码。
@Component
public class MyUserDetailsService implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录的用户名:" + username);
String password = passwordEncoder.encode("123456");
logger.info("加密后的密码:" + password);
return new User("admin", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
接着,写一个简单的controller
/**
* @author developlee
* @since 2018/11/25 20:40
*/
@RestController
public class HelloController {
@PostMapping("/hello")
public String hello(){
return "Hello ! My Dear Friend !";
}
}
做个简单的验证,让我们来访问下,http:localhost:8080/hello
跳转至Spring Security为我们提供登录页面。
输入用户名密码
后台日志打印结果截图
可以看到, 每次加密过的密码都是不一样的。这里面的机制,其实就是密码中常用的加盐方式。
4.总结
这篇算是真正踏入Spring Security的入门,了解了Spring Security 中最基本也是几个类及方法,User,UserDetails,UserDetailsService,以及PasswordEncoder。
接下来的文章中,将更加深入去探讨Spring Security的登录流程,权限控制等。
以上代码均可在我的github.com中找到,感谢花时间阅读,如果文章中有错误或不足之处,烦请赐教,共同探讨。
[…] Security 使用Http Basic登录,以及Spring Security如何自定义登录逻辑。 精通Spring Boot: Spring Security自定义登录 精通Spring Boot:Spring Security使用Http Basic认证 […]