Spring Security为我们提供了一个专门的org.springframework.security.core.Authentication
接口来代表认证;它最常用的实现类有UsernamePasswordAuthenticationToken
。
一旦请求被认证后,Authentication
对象就会自动存储在由SecurityContextHolder
管理的SecurityContext
中。
认证的原理通过下面类的处理顺序来进行的:
FilterChainProxy
:Servlet过滤器(Filter
)springSecurityFilterChain
实际类型是FilterChainProxy
,它可能包含多个过滤器链(DefaultSecurityFilterChain
),每个过滤器链包含多个过滤器。特定的过滤器会将请求中的认证信息(如用户名、密码)构造成Authentication
对象交由AuthenticationManager
的authenticate
方法处理。主要的过滤器有:
UsernamePasswordAuthenticationFilter
:使用表单(用户名、密码)提交进行认证信息,构造的Authentication
对象类型为UsernamePasswordAuthenticationToken
;并调用AuthenticationManager
的authenticate
来进行认证操作。BasicAuthenticationFilter
:使用HTTP请求的基础授权头提交认证信息,同样构造的Authentication
对象的类型为UsernamePasswordAuthenticationToken
;并调用AuthenticationManager
的authenticate
来进行认证操作。ExceptionTranslationFilter
:处理过滤器链中的异常
AuthenticationException
:认证异常,返回401
状态码AccessDeniedException
:授权异常,返回403
状态码FilterSecurityInterceptor
:它是AbstractSecurityInterceptor
的子类,当认证成功后,再使用AccessDecisionManager
对Web路径资源(web URI)进行授权操作。AuthenticationManager
:AuthenticationManager
接口的实现为ProviderManager
,我们使用AuthenticationManagerBuilder
来定制构建AuthenticationManager
ProviderManager
:ProviderManager
通过它authenticate
方法将认证交给了一组顺序的AuthenticationProvider
来完成认证。
AuthenticationProvider
:AuthenticationProvider
接口包含两个方法:
supports
:是否支持认证安全过滤器缓解构造的Authentication
;authenticate
:对Authentication
进行认证,若认证通过返回Authentication
,若不通过则抛出异常。DaoAuthenticationProvider
:DaoAuthenticationProvider
是AuthenticationProvider
接口的实现,他支持认证的Authentication
类型为UsernamePasswordAuthenticationToken
。它在认证中主要用到了下面三个部分:
UserDetailsService
:从指定的位置(如数据库)获得用户信息;通过比较用户信息和Authentication
(UsernamePasswordAuthenticationToken
)中的用户名和密码信息,若认证通过则构建新的Authentication
(UsernamePasswordAuthenticationToken
),包含用户的权限信息。PasswordEncoder
:使用PasswordEncoder
将请求传来的明文密码和存储的编码后的密码进行匹配比较。AuthenticationManager
我们可以重载WebSecurityConfigurerAdapter
类的方法,使用AuthenticationManagerBuilder
来配置AuthenticationManager
。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//auth.
}
}
我们可以通过配置UserDetailsService
或AuthenticationProvider
定制认证。
UserDetailsService
本例定制一个UserDetailsService
通过Spring Data JPA从数据库中获取用户。
基本外部配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/first_db?useSSL=false
username: root
password: zzzzzz
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: update
我们用户的实体:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class SysUser implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String realName;
@Column(unique = true)
private String username;
private String password;
public SysUser(String realName, String username, String password) {
this.realName = realName;
this.username = username;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { //1
return null;
}
@Override
public String getPassword() { //2
return this.password;
}
@Override
public String getUsername() { //3
return this.username;
}
@Override
public boolean isAccountNonExpired() { //4
return true;
}
@Override
public boolean isAccountNonLocked() { //5
return true;
}
@Override
public boolean isCredentialsNonExpired() { //6
return true;
}
@Override
public boolean isEnabled() { //7
return true;
}
}
实现UserDetails
接口的用户,通过接口的方法构建Authentication
对象的用户信息。
getAuthorities
方法获得用户的权限信息,我们会在后面详细讲解;getPassword
获得用户的密码,使用存储的密码;getUsername
获得用户名,使用存储的用户名;isAccountNonExpired
是否账户未过期,设置为true
,即未过期;isAccountNonLocked
是否账户未被锁定,设置为true
,即未被锁定;isCredentialsNonExpired
是否密码未过期,设置为true
,即未过期;isEnabled
用户是否弃用,设置为true
,即弃用。用户的Repository:
public interface SysUserRepository extends JpaRepository<SysUser, Long> {
Optional<SysUser> findByUsername(String username);
}
我们自定义UserDetailsService
:
public class CusotmUserDetailsService implements UserDetailsService {
SysUserRepository sysUserRepository;
public CusotmUserDetailsService(SysUserRepository sysUserRepository) {
this.sysUserRepository = sysUserRepository; //1
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<SysUser> sysUserOptional = sysUserRepository.findByUsername(username); //2
return sysUserOptional
.orElseThrow(() -> new UsernameNotFoundException("Username not found")); //3
}
}
SysUserRepository
用来查询数据库用户信息;UserDetails
类型,我们的用户也是UserDetails
可直接返回。我们可以通过配置AuthenticationManager
来注册UserDetailsService
,它会替换`。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SysUserRepository sysUserRepository; //1
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new CusotmUserDetailsService(sysUserRepository)); //2
}
@Bean
PasswordEncoder passwordEncoder(){ //3
return new BCryptPasswordEncoder();
}
}
SysUserRepository
给CusotmUserDetailsService
的构造使用;AuthenticationManagerBuilder
的userDetailsService
方法注册自定义的UserDetailsService
;BCrypt
作为我们的密码编码加密算法给DaoAuthenticationProvider
使用;我们还可以更简单的,直接通过声明UserDetailsService
的Bean让DaoAuthenticationProvider
使用自定义UserDetailsService
。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
UserDetailsService userDetailsService(SysUserRepository sysUserRepository){
return new CusotmUserDetailsService(sysUserRepository);
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
建立一个控制器用来测试访问:
@RestController
public class IndexController {
@GetMapping("/")
public String hello(){
return "Hello Spring Security";
}
}
应用启动时,向系统添加一个用户:
@Bean
CommandLineRunner createUser(SysUserRepository sysUserRepository, PasswordEncoder passwordEncoder){
return args -> {
SysUser user = new SysUser("wangyunfei", "wyf", passwordEncoder.encode("111111"));
sysUserRepository.save(user);
};
}
Spring Security默认使用表单登陆,且为我们自动提供了一个表单,我们访问http://localhost:8080/会自动转向http://localhost:8080/login;当我们用错误的账号密码登陆时会提示“用户名或密码错误”,用正确的账号密码访问显示测试的控制器返回内容。
AuthenticationProvider
上面自定义UserDetailsService
实际上还是AuthenticationProvider
的一部分,我们也可以通过的Bean来使用自定义的AuthenticationManager
来设置AuthenticationProvider
。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SysUserRepository sysUserRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(new CusotmUserDetailsService(sysUserRepository));
authProvider.setPasswordEncoder(new BCryptPasswordEncoder());
auth.authenticationProvider(authProvider); //
}
}
若我们自定义AuthenticationProvider
,则完全使用自己的验证逻辑。
public class CustomAuthenticationProvider implements AuthenticationProvider {
SysUserRepository sysUserRepository;
PasswordEncoder passwordEncoder;
public CustomAuthenticationProvider(SysUserRepository sysUserRepository, PasswordEncoder passwordEncoder) {
this.sysUserRepository = sysUserRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String usernameFromRequest = authentication.getName(); //1
String passwordFromRequest = authentication.getCredentials().toString(); //2
Optional<SysUser> sysUserOptional = sysUserRepository.findByUsername(usernameFromRequest);
SysUser sysUser = sysUserOptional
.orElseThrow(() -> new UsernameNotFoundException("Username not found")); //3
if(passwordEncoder.matches(passwordFromRequest, sysUser.getPassword()) && //4
sysUser.isAccountNonExpired() &&
sysUser.isAccountNonLocked() &&
sysUser.isCredentialsNonExpired() &&
sysUser.isEnabled())
return new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword(), sysUser.getAuthorities()); //5
else
throw new BadCredentialsException("Bad Credentials");
}
@Override
public boolean supports(Class<?> authentication) { //6
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
Authentication
:UsernamePasswordAuthenticationToken
对象;AuthenticationProvider
能处理Authentication
类型为UsernamePasswordAuthenticationToken
的认证。使用这个AuthenticationProvider
同样可通过AuthenticationManager
来配置:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SysUserRepository sysUserRepository;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new CustomAuthenticationProvider(sysUserRepository, passwordEncoder()));
}
}
更简单的注册AuthenticationProvider
的Bean的效果也是一致的:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
AuthenticationProvider authenticationProvider(SysUserRepository sysUserRepository){
return new CustomAuthenticationProvider(sysUserRepository, passwordEncoder());
}
}
运行的结果和自定义UserDetailsService
一致,普通情况下我们使用自定义UserDetailsService
就可以了;只有涉及到复杂的认证逻辑时才需要自定义AuthenticationProvider
。
HttpSecurity
用来针对不同的HTTP请求进行Web安全配置。我们前面的认证都是通过表单登陆进行认证,我们可以通过HttpSecurity
配置在请求时将账号信息放置于头部进行登录。在现代应用中,服务端和客户端(web、app等)分离,一般都是通过类似的方式来请求服务端的接口的。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //1
.and()
.httpBasic().authenticationEntryPoint(authenticationEntryPoint()); //2
}
@Bean
AuthenticationEntryPoint authenticationEntryPoint(){ //3
BasicAuthenticationEntryPoint authenticationEntryPoint = new BasicAuthenticationEntryPoint();
authenticationEntryPoint.setRealmName("wisely");
return authenticationEntryPoint;
}
@Bean
UserDetailsService userDetailsService(SysUserRepository sysUserRepository){
return new CusotmUserDetailsService(sysUserRepository);
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
BasicAuthenticationEntryPoint
;BasicAuthenticationEntryPoint
将认证信息放置于头部,当认证不通过返回状态值为401
;Postman支持Basic Auth:
HTTP Basic实际上是在请求头的Authorization
中添加值为Basic 账号:密码的Base64编码
,wyf:111111
的Base64编码为d3lmOjExMTExMQ==
,那我们请求头为Authorization: Basic d3lmOjExMTExMQ==
即可完成认证。
我们可以通过SecurityContextHolder
中获取SecurityContext
从而获得Authentication
:
SecurityContext context = SecurityContextHolder.getContext(); //获得SecurityContext
Authentication auth = context.getAuthentication(); //获得Authentication
Object principal = auth.getPrincipal(); //获取用户信息
Object details = auth.getDetails(); //认证请求的更多信息
Spring Security给我们注册了ArgumentResolvers
,我们可以直接通过@CurrentSecurityContext
注解获得SecurityContext
,@CurrentSecurityContext
注解还支持表达式获得Authentication
、principal
和details
。
我们更简单的通过使用@AuthenticationPrincipal
注解获得用户信息。
@GetMapping("/user")
public Map<String, Object> getUserInfo(@AuthenticationPrincipal SysUser sysUser, //1
@CurrentSecurityContext SecurityContext securityContext, //2
@CurrentSecurityContext(expression = "authentication") Authentication authentication, //3
@CurrentSecurityContext(expression = "authentication.principal") Object principal,//4
@CurrentSecurityContext(expression = "authentication.details") Object details){ //5
Map<String, Object> map = new HashMap<>();
map.put("sysUser", sysUser);
map.put("authentication", authentication);
map.put("principal", principal);
map.put("details", details);
return map;
}
@AuthenticationPrincipal
注册系统用户可获得用户对象;@CurrentSecurityContext
可直接获得SecurityContext
对象;expression = "authentication"
,可获得Authentication
对象expression = "authentication.principal"
,可获得用户信息;expression = "authentication.details"
,可获得认证请求的额外信息;我们用Postman访问http://localhost:8080/user,并使用Basic Auth
认证。
那我们上一章“审计功能”获得用户的代码修改为:
@Bean
AuditorAware<String> auditorProvider(){
return () -> Optional.of(SecurityContextHolder.getContext().getAuthentication().getName());
}
在生产中我们肯定会对密码进行编码,Spring Security给我们提供了PassowrdEncoder
接口用来编码密码和匹配密码;在生产中建议使用工业标准的Bcrypt
的实现BCryptPasswordEncoder
。
public interface PasswordEncoder {
String encode(CharSequence rawPassword); //将明文密码进行编码
boolean matches(CharSequence rawPassword, String encodedPassword); //匹配明文密码和编码密码
default boolean upgradeEncoding(String encodedPassword) { //更新密码编码机制
return false;
}
}
前面我们在应用中已经注册了BCryptPasswordEncoder
的Bean:
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
我们用代码检验一下:
@Bean
CommandLineRunner passwordOperation(PasswordEncoder passwordEncoder){
return args -> {
String passwordPlain = "123456";
String passwordEncoded = passwordEncoder.encode(passwordPlain);
boolean isMatched = passwordEncoder.matches(passwordPlain, passwordEncoded);
System.out.println("明文密码为:" + passwordPlain);
System.out.println("编码密码为:" + passwordEncoded);
System.out.println("密码是否匹配:" + isMatched);
};
}
我的新书《从企业级开发到云原生微服务:Spring Boot 实战》已出版,内容涵盖了丰富Spring Boot开发的相关知识
购买地址:https://item.jd.com/12760084.html
主要包含目录有:
第一章 初识Spring Boot(快速领略Spring Boot的美丽)
第二章 开发必备工具(对常用开发工具进行介绍:包含IntelliJ IDEA、Gradle、Lombok、Docker等)
第三章 函数式编程
第四章 Spring 5.x基础(以Spring 5.2.x为基础)
第五章 深入Spring Boot(以Spring Boot 2.2.x为基础)
第六章 Spring Web MVC
第七章 数据访问(包含Spring Data JPA、Spring Data Elasticsearch和数据缓存)
第八章 安全控制(包含Spring Security和OAuth2)
第九章 响应式编程(包含Project Reactor、Spring WebFlux、Reactive NoSQL、R2DBC、Reactive Spring Security)
第十章 事件驱动(包含JMS、RabbitMQ、Kafka、Websocket、RSocket)
第11章 系统集成和批处理(包含Spring Integration和Spring Batch)
第12章 Spring Cloud与微服务
第13章 Kubernetes与微服务(包含Kubernetes、Helm、Jenkins、Istio)
多谢大家支持。