6.1认证流程
弄清楚认证原理之后我们来看下具体认证时数据源的获取:默认情况下,AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过UserDetailsService完成数据源的校验。他们之间调用关系如下:
AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式,认证时底层数据源需要调用 UserDetailService 来实现。
6.2配置全局 AuthenticationManager
6.2.1默认的全局 AuthenticationManager
springboot 对 security 进行自动配置时,自动在工厂中使用AuthenticationManagerBuilder 创建一个全局AuthenticationManager,配置的是原有的全局AuthenticationManager
//springboot对security默认配置中,在工厂中默认创建AuthenticationManager
@Autowired //将容器中自动创建的AuthenticationManagerBuilder注入进来
//这里AuthenticationManagerBuilder是spring工厂中给我们创建好了的默认AuthenticationManager,拿到后就可以进行修改
public void initialize(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("springboot默认配置: " + builder);
}
输出结果:
springboot默认配置:org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@60297f36
总结:默认自动配置创建的全局AuthenticationManager
-
默认查找当前项目中是否存在自定义的UserDetailService 实例,如果存在实例,原有的UserDetailService自动配置的内存将会失效,自动将当前项目UserDetailService 实例设置为数据源。
-
在工厂中使用时直接在代码中注入即可。
6.2.2自定义全局 AuthenticationManager
覆盖原有的全局AuthenticationManager,也就是构建出一个空的AuthenticationManager也没有指定的UserDetailService。
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override//重写的父类WebSecurityConfigurerAdapter中的configure方法,使用的也是父类的AuthenticationManagerBuilder
public void configure(AuthenticationManagerBuilder builder) {
//builder ....
}
}
总结自定义全局 AuthenticationManager
-
一旦通过 configure 方法自定义AuthenticationManager的实现就会将工厂中自动配置的AuthenticationManager进行覆盖,也就是构建出一个空的AuthenticationManager也没有指定的UserDetailService。
-
需要在实现中指定认证数据源对象UserDetailService实例;
-
这种方式创建的AuthenticationManager对象是工厂内部本地的一个 AuthenticationManager对象,没有暴露出来,不允许在其他自定义组件中进行注入使用。
在工厂中暴露自定义AuthenticationManager 实例
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
//1.自定义AuthenticationManager 推荐使用这种方式,而不是直接使用默认自动配置的,但这种方式并没有在工厂中暴露出来
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager: " + builder);
builder.userDetailsService(userDetailsService());
}
//作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
@Override
@Bean //记得加上此注解,将AuthenticationManager 对象注入工厂
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
6.3测试代码总结
package com.study.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
/**
* @ClassName WebSecurityConfigurer
* @Description TODO
* @Date 2022/7/21 22:34
* @Version 1.0
*/
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
//1.默认AuthenticationManager:springboot对security默认配置中,在工厂中默认创建AuthenticationManager
// @Autowired //代码从官网复制而来
// public void initialize(AuthenticationManagerBuilder builder) throws Exception {
// System.out.println("springboot默认配置:" + builder);
// builder.userDetailsService(userDetailsService());
// }
/**
* 未定义UserDetailsService前:启动服务,重新访问hello路径,发现原先的用户名、密码组合失效,需要使用master、123456才能登录成功
* 输出结果:
* springboot默认配置:org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder@68ace111
*/
//在工厂中自定义userDetailsService
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("master").password("{noop}123456").roles("admin").build());//noop表示明文
return userDetailsService;
}
/**
* 定义UserDetailsService后启动服务测试结果:
* 上面这段(1.默认AuthenticationManager:)默认可以不写,写的时候会报错:
* org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'webSecurityConfigurer':
* Requested bean is currently in creation: Is there an unresolvable circular reference?
* 这段代码显得多余了
*/
//2.自定义AuthenticationManager 推荐使用这种自定义方式,但并没有在工厂中进行暴露
@Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
System.out.println("自定义AuthenticationManager:" + builder);
builder.userDetailsService(userDetailsService());
}
/**
* 启动服务,重新访问hello路径,发现原先的用户名、密码组合失效,需要使用master、123456才能登录成功
* 输出结果:
* 自定义AuthenticationManager:org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$2@24fabd0f
*/
//作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
@Override
@Bean//加上Bean注解才会有AuthenticationManager具体实例
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/index").permitAll() //放行/index请求
.mvcMatchers("/login.html").permitAll() //放行/login.html请求
.anyRequest().authenticated() //其它请求需要登录认证后才能访问
.and()
.formLogin() //默认form表单页面登录
.loginPage("/login.html") //使用自定义登录页面登录页面登录
.loginProcessingUrl("/doLogin") //使用自定义登录页面时需要重新指定url,对应login.html中的action路径
.usernameParameter("uname") //重新指定用户名名称
.passwordParameter("pwd") //重新指定密码名称
//.successForwardUrl("/index") //认证成功后跳转路径
//forward 跳转路径 始终在认证成功之后跳转到指定请求 地址栏不变
//.defaultSuccessUrl("/hello") //默认认证成功后跳转路径
//.defaultSuccessUrl("/hello",true) //第二个参数设置为true时总是跳转,效果同successForwardUrl一致,默认false
//redirect 重定向 注意:如果之前有请求过的路径,会优先跳转之前的请求路径 地址栏改变
.successHandler(new MyAuthenticationSuccessHandler())//认证成功时处理,前后端分离解决方案
//.failureForwardUrl("/login.html")//认证失败之后,forward跳转
//.failureUrl("/login.html") //默认认证失败之后,redirect跳转
.failureHandler(new MyAuthenticationFailureHandler())//认证失败时处理,前后端解决方案
.and()
.logout()
//.logoutUrl("/logout")//指定注销登录URL,默认请求方式必须为GET
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/aaa", "GET"),//通过以get方式访问/aaa也可以实现登出
new AntPathRequestMatcher("/bbb", "POST")//通过以delete方式访问/bbb也可以实现登出
))
.invalidateHttpSession(true)//默认开启会话失效
.clearAuthentication(true)//默认清除认证标志
//.logoutSuccessUrl("/login.html")//注销登录成功后跳转的页面
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.and()
.csrf().disable();//此处先关闭CSRF跨站保护
}
}
6.4自定义内存数据源
通过上面分析可以重点在于UserDetailsService,查看UserDetailsService接口如下:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
里面只有一个要实现的方法loadUserByUsername,返回类型为UserDetails,查看UserDetails定义如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetails的实现类User
User定义
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 560L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
public Collection<GrantedAuthority> getAuthorities() {
return this.authorities;
}
public String getPassword() {
return this.password;
}
public String getUsername() {
return this.username;
}
public boolean isEnabled() {
return this.enabled;
}
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
public void eraseCredentials() {
this.password = null;
}
private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {
Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new AuthorityComparator());
Iterator var2 = authorities.iterator();
while(var2.hasNext()) {
GrantedAuthority grantedAuthority = (GrantedAuthority)var2.next();
Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
sortedAuthorities.add(grantedAuthority);
}
return sortedAuthorities;
}
public boolean equals(Object obj) {
return obj instanceof User ? this.username.equals(((User)obj).username) : false;
}
public int hashCode() {
return this.username.hashCode();
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getName()).append(" [");
sb.append("Username=").append(this.username).append(", ");
sb.append("Password=[PROTECTED], ");
sb.append("Enabled=").append(this.enabled).append(", ");
sb.append("AccountNonExpired=").append(this.accountNonExpired).append(", ");
sb.append("credentialsNonExpired=").append(this.credentialsNonExpired).append(", ");
sb.append("AccountNonLocked=").append(this.accountNonLocked).append(", ");
sb.append("Granted Authorities=").append(this.authorities).append("]");
return sb.toString();
}
public static UserBuilder withUsername(String username) {
return builder().username(username);
}
public static UserBuilder builder() {
return new UserBuilder();
}
/** @deprecated */
@Deprecated
public static UserBuilder withDefaultPasswordEncoder() {
logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications.");
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserBuilder var10000 = builder();
Objects.requireNonNull(encoder);
return var10000.passwordEncoder(encoder::encode);
}
public static UserBuilder withUserDetails(UserDetails userDetails) {
return withUsername(userDetails.getUsername()).password(userDetails.getPassword()).accountExpired(!userDetails.isAccountNonExpired()).accountLocked(!userDetails.isAccountNonLocked()).authorities(userDetails.getAuthorities()).credentialsExpired(!userDetails.isCredentialsNonExpired()).disabled(!userDetails.isEnabled());
}
public static final class UserBuilder {
private String username;
private String password;
private List<GrantedAuthority> authorities;
private boolean accountExpired;
private boolean accountLocked;
private boolean credentialsExpired;
private boolean disabled;
private Function<String, String> passwordEncoder;
private UserBuilder() {
this.passwordEncoder = (password) -> {
return password;
};
}
public UserBuilder username(String username) {
Assert.notNull(username, "username cannot be null");
this.username = username;
return this;
}
public UserBuilder password(String password) {
Assert.notNull(password, "password cannot be null");
this.password = password;
return this;
}
public UserBuilder passwordEncoder(Function<String, String> encoder) {
Assert.notNull(encoder, "encoder cannot be null");
this.passwordEncoder = encoder;
return this;
}
public UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList(roles.length);
String[] var3 = roles;
int var4 = roles.length;
for(int var5 = 0; var5 < var4; ++var5) {
String role = var3[var5];
Assert.isTrue(!role.startsWith("ROLE_"), () -> {
return role + " cannot start with ROLE_ (it is automatically added)";
});
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return this.authorities((Collection)authorities);
}
public UserBuilder authorities(GrantedAuthority... authorities) {
return this.authorities((Collection)Arrays.asList(authorities));
}
public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = new ArrayList(authorities);
return this;
}
public UserBuilder authorities(String... authorities) {
return this.authorities((Collection)AuthorityUtils.createAuthorityList(authorities));
}
public UserBuilder accountExpired(boolean accountExpired) {
this.accountExpired = accountExpired;
return this;
}
public UserBuilder accountLocked(boolean accountLocked) {
this.accountLocked = accountLocked;
return this;
}
public UserBuilder credentialsExpired(boolean credentialsExpired) {
this.credentialsExpired = credentialsExpired;
return this;
}
public UserBuilder disabled(boolean disabled) {
this.disabled = disabled;
return this;
}
public UserDetails build() {
String encodedPassword = (String)this.passwordEncoder.apply(this.password);
return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities);
}
}
private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {
private static final long serialVersionUID = 560L;
private AuthorityComparator() {
}
public int compare(GrantedAuthority g1, GrantedAuthority g2) {
if (g2.getAuthority() == null) {
return -1;
} else {
return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority());
}
}
}
}
主要关注User的几个成员变量:
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
举例:
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager
= new InMemoryUserDetailsManager();
UserDetails u1 = User.withUsername("zhangs")
.password("{noop}111").roles("USER").build();
inMemoryUserDetailsManager.createUser(u1);
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService());
}
}