Spring Security实现认证的主要步骤如下:
配置用户存储:您可以选择将用户信息存储在内存中、数据库中或其他外部身份验证源中。通过配置UserDetailsService或AuthenticationProvider,Spring Security可以获取用户的凭据和权限信息。
用户认证:当用户尝试登录时,Spring Security会验证用户提供的凭据(例如用户名和密码)。它使用AuthenticationManager来处理认证过程。AuthenticationManager会调用配置的AuthenticationProvider来验证用户凭据的有效性。
身份验证过滤器:Spring Security使用身份验证过滤器来拦截登录请求并进行身份验证。通常使用UsernamePasswordAuthenticationFilter来处理基于用户名和密码的认证请求。该过滤器会验证用户提供的凭据,并将认证结果封装成一个Authentication对象。
认证管理器:AuthenticationManager是Spring Security的核心接口之一,用于管理和执行身份验证过程。它负责调用配置的AuthenticationProvider来验证用户凭据的有效性。
认证成功处理器:当认证成功时,可以配置一个认证成功处理器来处理成功的认证请求。该处理器可以执行一些自定义逻辑,例如生成和返回访问令牌或重定向到特定页面。
认证失败处理器:当认证失败时,可以配置一个认证失败处理器来处理失败的认证请求。该处理器可以返回错误消息或重定向到登录页面等。
WebSecurityConfigurerAdapter是Spring Security提供的一个方便的基类,用于自定义安全配置。通过继承WebSecurityConfigurerAdapter类,并重写其中的方法,可以实现对Spring Security的自定义配置。 通过对configure方法的重新,我们可以做很多配置。
下面是configure方法中常用配置的含义和对应的代码示例(Java):
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public").permitAll()
.anyRequest().authenticated();
}
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.defaultSuccessUrl("/home")
.failureUrl("/login?error=true");
}
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true");
}
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public").permitAll()
.antMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated();
}
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public").permitAll();
}
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated();
}
@Component
public class CustomFilter implements GenericFilterBean{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 过滤器逻辑,在此可以做接口的token校验
chain.doFilter(request, response);
}
}
//把自定义的过滤器加入到过滤器链中
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomFilter customFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout();
}
}
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
org.springframework.security.access.AccessDeniedException accessDeniedException)
throws IOException, ServletException {
// 在这里自定义处理访问被拒绝的情况
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
}
}
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// 在这里自定义处理未经身份验证的情况
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AccessDeniedHandler accessDeniedHandler;
private final AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
public SecurityConfig(CustomAccessDeniedHandler accessDeniedHandler,
CustomAuthenticationEntryPoint authenticationEntryPoint) {
this.accessDeniedHandler = accessDeniedHandler;
this.authenticationEntryPoint = authenticationEntryPoint;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler) // 设置自定义的accessDeniedHandler
.authenticationEntryPoint(authenticationEntryPoint) // 设置自定义的authenticationEntryPoint
.and()
// 其他的配置...
}
}
这些只是一些常用的配置示例,实际使用中可能需要根据需求进行更复杂的配置和自定义。
下面是使用UsernamePasswordAuthenticationFilter进行登录认证,并将账号密码存储在数据库中的Java代码示例:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("SELECT username, password, enabled FROM users WHERE username = ?")
.authoritiesByUsernameQuery("SELECT username, authority FROM authorities WHERE username = ?")
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UsernamePasswordAuthenticationFilter authenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setFilterProcessesUrl("/login");
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
}
在上面的代码中,我们首先配置了一个DataSource,用于连接数据库。然后在SecurityConfig类中,通过configure方法配置了HttpSecurity,定义了登录页面、注销页面,以及哪些请求需要进行身份验证。接着,我们通过configure方法配置了AuthenticationManagerBuilder,使用jdbcAuthentication()方法来指定使用数据库进行身份验证,并提供了查询用户信息和权限的SQL语句。我们还使用了passwordEncoder()方法来指定密码的加密方式,这里使用了BCryptPasswordEncoder。最后,我们通过@Bean注解定义了一个UsernamePasswordAuthenticationFilter,并将其添加到了过滤器链中,用于拦截登录请求并进行身份验证。
这只是简单的代码示例,主要是为了理解原理,我们真正使用的时候还是要自定义一个登录过滤器和token校验过滤器。下一章我们会举一个前后端分离,自定义登录认证的代码示例。
UsernamePasswordAuthenticationFilter是Spring Security框架中的一个过滤器,用于处理基于用户名和密码的身份验证。它是Spring Security核心过滤器链中的一部分,负责拦截用户的登录请求,并将用户名和密码交给AuthenticationManager进行身份验证处理。如果身份验证成功,该过滤器会创建一个包含用户信息和权限的认证对象,并将其交给SecurityContextHolder进行管理。如果身份验证失败,则会返回错误信息给用户。通过该过滤器,可以实现基于表单的登录认证
*/
public class UsernamePasswordAuthenticationFilter extends org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
//此过滤器拦截的接口
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
//无参构造器,使用默认的AuthenticationManager实现
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
//有参构造器,使用方可自定义AuthenticationManager,通过实现AuthenticationManager接口
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
可以看到此过滤器拦截自带的/login接口
UsernamePasswordAuthenticationFilter继承自AbstractAuthenticationProcessingFilter,
UsernamePasswordAuthenticationFilter没有重写doFilter()方法,我们看AbstractAuthenticationProcessingFilter的doFilter()方法。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//attemptAuthentication是抽象方法,可被子类重写
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
//成功后会
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//保存用户信息等
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
认证逻辑主要在attemptAuthentication,attemptAuthentication是抽象方法,被子类UsernamePasswordAuthenticationFilter覆写,我们看下覆写逻辑。
//将request中的参数提取出来保存到 Authentication 中, 返回给认证器认证, 就这么一步, 如果你是前后端分离, 就有可能需要重写该方法
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//请求中获取用户名
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
//从请求中获取密码
String password = obtainPassword(request);
password = (password != null) ? password : "";
//构建个UsernamePasswordAuthenticationToken令牌,继承自Authentication
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//把令牌传入AuthenticationManager,进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
主要逻辑是this.getAuthenticationManager().authenticate(authRequest);可以看到是把UsernamePasswordAuthenticationToken从传入到AuthenticationManager。AuthenticationManager的接口的主要实现类是ProviderManager,核心认证逻辑就在ProviderManager类中。
我们看下ProviderManager类的authenticate()方法,代码过长,主要看下主要逻辑。
//认证器集合
private List<org.springframework.security.authentication.AuthenticationProvider> providers = Collections.emptyList();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
//支持多种认证,遍历所有AuthenticationProvider
for (org.springframework.security.authentication.AuthenticationProvider provider : getProviders()) {
//匹配当前的Authentication
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//执行匹配到的AuthenticationProvider逻辑
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
我们根据supports()方法,匹配到正确的AuthenticationProvider是DaoAuthenticationProvider。DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider。
核心逻辑:
//校验用户密码并返回用户信息
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//返回本地用户信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
//请求密码和本地密码匹配
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
DaoAuthenticationProvider中覆写的方法。
//从缓存或者数据库查询用户信息
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//从缓存或者数据库查询用户信息
//默认spring security保存在内存中, 如果你需要改从数据库中拿到用户, 就需要重写UserDetailsService
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
} catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
} catch (InternalAuthenticationServiceException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
//密码验证方法
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
//比较密码是否相同
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
可以看到,这段逻辑是:比较用户输入的账号密码和数据库或缓存中的用户密码是否匹配。如果匹配成功,认证通过,存储用户信息到上下文中,发送监听事件。认证失败,报错返回前端。
Spring Security认证流程总结如下:
这是一个简化的Spring Security认证流程,具体的流程可能会根据配置和需求有所不同。但总体来说,Spring Security提供了一个灵活且可定制的认证框架,可以满足各种身份验证需求。
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role;
// 构造函数、getter和setter方法省略
}
public interface UserRepository {
//查询数据库用户信息,接口实现类自定义编写
User findByUsername(String username);
}
import org.springframework.beans.factory.annotation.Autowired;
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.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
返回对象是org.springframework.security.core.userdetails.User,这是springSecurity内部对象,如果不满足我们可以自定义返回对象,自定义返回对象要继承springSecurity的UserDetails。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsServiceImpl userDetailsService;
@Autowired
public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // 需要ADMIN角色才能访问
.antMatchers("/user/**").hasAnyRole("ADMIN", "USER") // 需要ADMIN或USER角色才能访问
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
//密码加密方法
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
可以根据需求修改configure()方法。
@RestController
public class UserController {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
@Autowired
public UserController(UserRepository userRepository, PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public String login(@RequestBody User user) {
User storedUser = userRepository.findByUsername(user.getUsername());
if (storedUser != null && passwordEncoder.matches(user.getPassword(), storedUser.getPassword())) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
return "登录成功";
} else {
return "用户名或密码错误";
}
}
}
@Component
public class CustomFilter implements GenericFilterBean{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 过滤器逻辑,在此可以做接口的token校验
chain.doFilter(request, response);
}
}
//把自定义的过滤器加入到过滤器链中
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomFilter customFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout();
}
}
UsernamePasswordAuthenticationToken 是一个基于用户名和密码的身份验证令牌。
此令牌作为参数传入AuthenticationManager。在AuthenticationManager中,和2步骤中我们自定义的 loadUserByUsername()方法返回的用户信息做匹配,匹配成功认证成功,把认证信息设置到SecurityContextHolder中,失败返回错误信息。
其他接口请求后端接口的时候,会通过我们的自定义过滤器CustomFilter 校验token。
以上只是简单的代码示例,主要是帮助我们理解认证流程。真正的项目代码要更完善,细节点与功能点要更多,也会用到其他框架的东西,比如JWT。
Spring Security认证流程总结如下: