Spring Security详解和应用

1.认证

Spring Security的认证与授权是相互独立的
在这里插入图片描述

总而言之,在控制器取用户信息时,可以从SecurityContextHolder中取出数据,同时通过session的控制可以来控制用户与服务器的连接数量,可以以此来控制用户的登录数量。
不过,强制下线的操作不能同步,因为web不是用socket进行连接的,只有等被下线的用户向服务器发起请求时,服务器才能向其发出下线通知。

在这里插入图片描述
在学习Spring Security的时候,我们要搞清楚这三个问题

2. 为什么引入Spring Security之后,没有任何配置,所有请求却要认证?

在这里插入图片描述
对http的所有请求开启表单认证
这就是为什么在没有任何配置的情况下,请求会被拦截的原因!

在这里插入图片描述
我们可以点进去看DefaultWebSecurityCondition这个注解,发现如果当我们不配置WebSecurityConfigurerAadapter.class以及SecurityFilterChain.class这两个类时,就会启用默认配置,因此这是一个重点,如果我们想修改默认配置时,就要通过子类来修改。

3.登录界面从何而来?

在这里插入图片描述
发现请求未认证,请求被拦截并抛出异常,并将客户端重定向到login界面。
在这里插入图片描述
在这里插入图片描述
可以看到的是,其的登录界面的代码是在此处编写的,拼接成html。

4.为什么使用默认user和控制台密码可以登录?登录时的验证数据源在哪?

FormLoginConfigurer
在这里插入图片描述
根据debug可以追踪到DaoAuthenticationProvider类中,有个getUserDetailsService()的方法
在这里插入图片描述
看到UserDetailService的接口我们可知,这是一个顶层父接口,接口中的loadUserByUsername方法是用来认证用户名的方法,默认实现使用是内存实现,如果想要修改数据库实现我们只需要自定义UserDetailService实现,最终返回UserDetails实例即可
在这里插入图片描述
在这里插入图片描述
而SecurityProperties的类中就有关于User的配置

5. 登录请求的流程

5.1检测请求

当发起login请求时,此时由UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter处理,简单来说就是从请求request中获取用户名和密码进行认证操作。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
    
    
 
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		
		//当不是登录请求 POST:/login时则直接跳过
		if (!requiresAuthentication(request, response)) {
    
    
			chain.doFilter(request, response);
 
			return;
		}
		//如果是登录请求 POST:/login则进行验证处理
		if (logger.isDebugEnabled()) {
    
    
			logger.debug("Request is to process authentication");
		}
 
		Authentication authResult;
 
		try {
    
    
			//从请求中获取用户名密码进行校验
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
    
    
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
    
    
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			//校验失败则跳转到登录页面
			unsuccessfulAuthentication(request, response, failed);
 
			return;
		}
		catch (AuthenticationException failed) {
    
    
			// Authentication failed
			//校验失败则跳转到登录页面
			unsuccessfulAuthentication(request, response, failed);
 
			return;
		}
 
		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
    
    
			chain.doFilter(request, response);
		}
		//校验成功则跳转到成功之后页面
		successfulAuthentication(request, response, chain, authResult);
	}

在doFilter方法中,首先会调用requiresAuthentication方法判断是不是登录请求,如果不是则直接跳过这个Filter,如果是则进行身份验证

在这里插入图片描述

5.2 身份验证

如果请求是登录操作,则接下来进行身份验证相关的操作
调用authResult = attemptAuthentication(request, response)方法进行验证,在attemptAuthentication中会从请求中获取用户名和密码构建UsernamePasswordAuthenticationToken对象,然后调用接口AuthenticationManager的authenticate进行验证返回Authentication

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
    
    
		if (postOnly && !request.getMethod().equals("POST")) {
    
    
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
        //获取用户名密码
		String username = obtainUsername(request);
		String password = obtainPassword(request);
 
		if (username == null) {
    
    
			username = "";
		}
 
		if (password == null) {
    
    
			password = "";
		}
 
		username = username.trim();
 
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
 
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
        //调用AuthenticationManager的实现类进行校验
		return this.getAuthenticationManager().authenticate(authRequest);
	}

在接口AuthenticationManager的实现类ProviderManager调用authenticate方法进行校验操作。在authenticate方法中提供了一个List,开发者可以提供不同的校验方式(用户名密码、手机号密码、邮箱密码等)只要其中有一个AutenticationProvider调用authenticate方法校验通过即可,当校验不通过时会抛出AuthenticationException ,当所有的AuthenticationProvider校验不通过时,直接抛出异常由ExceptionTranslationFilter捕捉处理,跳转到登录页面。

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    
    
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		boolean debug = logger.isDebugEnabled();
        //可以提供多个验证器,只要其中有一个校验通过接口
		for (AuthenticationProvider provider : getProviders()) {
    
    
			if (!provider.supports(toTest)) {
    
    
				continue;
			}
 
			if (debug) {
    
    
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}
 
			try {
    
    
                //进行校验,校验不通过则直接抛出AuthenticationException 
				result = provider.authenticate(authentication);
 
				if (result != null) {
    
    
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
    
    
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
    
    
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
    
    
				lastException = e;
			}
		}
 
		if (result == null && parent != null) {
    
    
			// Allow the parent to try.
			try {
    
    
				result = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
    
    
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
    
    
				lastException = e;
			}
		}
 
		if (result != null) {
    
    
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
    
    
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
 
			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}
        
		// Parent was null, or didn't authenticate (or throw an exception).
        //最终校验不通过则抛出异常,由ExceptionTranslationFilter捕捉处理
		if (lastException == null) {
    
    
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] {
    
     toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}
 
		prepareException(lastException, authentication);
 
		throw lastException;
	}

5.3 检验用户名密码

在接口AuthenticationProvider的实现类AbstractUserDetailsAuthenticationProvider中调用authenticate进行用户名密码等的校验

(1)首先从缓存userCahce中获取,如果获取到从子类DaoAuthenticationProvider的retirveUser方法中获取

(2)如果获取不到则直接抛出异常

(3)如果匹配成功调用createSuccessAuthentication方法创建Authentication返回。

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    
    
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
 
		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
 
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
 
		if (user == null) {
    
    
			cacheWasUsed = false;
 
			try {
    
    
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
    
    
				logger.debug("User '" + username + "' not found");
 
				if (hideUserNotFoundExceptions) {
    
    
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
    
    
					throw notFound;
				}
			}
 
			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}
 
		try {
    
    
			preAuthenticationChecks.check(user);
            //判断用户名和密码等信息是否完全匹配
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
    
    
			if (cacheWasUsed) {
    
    
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
    
    
				throw exception;
			}
		}
 
		postAuthenticationChecks.check(user);
 
		if (!cacheWasUsed) {
    
    
			this.userCache.putUserInCache(user);
		}
 
		Object principalToReturn = user;
 
		if (forcePrincipalAsString) {
    
    
			principalToReturn = user.getUsername();
		}
 
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

5.4 重写UserDetailService

在子类DaoAuthenticationProvider中调用接口的UserDetailsService的loadUserByUsername方法根据用户名来查找用户信息(在正式项目中实现此接口来完成从数据库等中查找),如果查找不到还是直接抛出异常,由上层去处理。

protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    
    
		UserDetails loadedUser;
 
		try {
    
    
            //调用默认实现InMemoryUserDetailsManager查找用户名密码
			loadedUser = this.getUserDetailsService().loadUserByUsername(username);
		}
		catch (UsernameNotFoundException notFound) {
    
    
			if (authentication.getCredentials() != null) {
    
    
				String presentedPassword = authentication.getCredentials().toString();
				passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
						presentedPassword, null);
			}
			throw notFound;
		}
		catch (Exception repositoryProblem) {
    
    
			throw new InternalAuthenticationServiceException(
					repositoryProblem.getMessage(), repositoryProblem);
		}
 
		if (loadedUser == null) {
    
    
			throw new InternalAuthenticationServiceException(
					"UserDetailsService returned null, which is an interface contract violation");
		}
		return loadedUser;
	}

可以看到的是,这个方法是根据UserDetailService中的loadUserByUsername来获取用户名和密码来进行认证的,如果密码有加密,记得在config中进行bean的注入

     * 密码加密
     *
     * @return {
    
    @link PasswordEncoder} 加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return new BCryptPasswordEncoder();
    }

当找到用户信息后,还需要根据用户信息和password字段进行匹配,在additionalAuthenticationChecks中完成完整的用户名和密码认证。

protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    
    
		Object salt = null;
 
		if (this.saltSource != null) {
    
    
			salt = this.saltSource.getSalt(userDetails);
		}
 
		if (authentication.getCredentials() == null) {
    
    
			logger.debug("Authentication failed: no credentials provided");
 
			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
 
		String presentedPassword = authentication.getCredentials().toString();
 
		if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
				presentedPassword, salt)) {
    
    
			logger.debug("Authentication failed: password does not match stored value");
 
			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}

在UserDetailService的实现类InMemoryUserDetailsManager中根据用户名获取用户信息。

    public UserDetails loadUserByUsername(String username) {
    
    
        if (StringUtils.isBlank(username)) {
    
    
            throw new BizException("用户名不能为空!");
        }
        // 查询账号是否存在
        UserAuth userAuth = userAuthDao.selectOne(new LambdaQueryWrapper<UserAuth>()
                .select(UserAuth::getId, UserAuth::getUserInfoId, UserAuth::getUsername, UserAuth::getPassword, UserAuth::getLoginType)
                .eq(UserAuth::getUsername, username));
        if (Objects.isNull(userAuth)) {
    
    
            throw new BizException("用户名不存在!");
        }
        // 封装登录信息
        return convertUserDetail(userAuth, request);
    }

这里做部分展示,反正要记得将username和password属性封装到UserDetails中

5.5 配置加密策略

    /**
     * 身份认证接口
     *
     * @param auth 身份
     * @throws Exception 异常处理
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth
                // 从数据库读取的用户进行身份认证
                .userDetailsService(userDetailsService)
                //加密方法(不加密)
                .passwordEncoder(new BCryptPasswordEncoder());
    }

在security的配置类中,要配置passwordEncoder的实例,否则会出现报错:There is no PasswordEncoder mapped for the id “null”

6. 自定义认证

在这里插入图片描述

Spring Security默认的是全局认证,我们要做的是:如何直接访问公共资源、然后认证、授权访问受限资源

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Autowired
    private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
      //将SpringSecurity的拦截请求全部放行
        http.headers().frameOptions().disable();    //Spring Security4默认是将'X-Frame-Options' 设置为 'DENY'
        http.authorizeRequests()
                .antMatchers("/index")	//匹配该路径的请求都放行
                .permitAll()
              .anyRequest().authenticated()	//其余所有请求都需要认证
                //.successFormwardUrl("/hello)	//跳转路径,地址栏不变
                //.defalutSuccessUrl("/hello")	//认证成功后redirect跳转,继续进行上一次请求
                .successHandler(authenticationSuccessHandler)	//适用于前后端分离,响应json
                .and()
                .csrf().disable();
    }
}

Spring Security使用的是线程绑定,session会维持三十分钟:
当前端传认证信息,认证信息就会保存在securitycontextholder 然后数据存在session中,并返回sessionid给前端;之后清空SecurityContextHolder,以后每次请求都是先把存在context的数据再更新到session中即可。
在这里插入图片描述
而且默认的身份信息只能由threadlocal获取,它的子线程将无法获取身份信息
在这里插入图片描述
在这里插入图片描述

我们要关注的是AuthenticationManager的实现,观察其子类除了ProviderManager,其他类都是静态内部类所以我们主要关注ProviderManager。

但是调用的时候逻辑有点复杂,第一次调用时,首先调用AuthenticationManager中的ProviderManager,result是空的,
在这里插入图片描述
在这里插入图片描述
因为第一次调用时显然authentication为空(即result为空),所以要调用父类(也是ProviderManager)的authenticate方法。相信大多数人看到这里就有点懵了,这是在自己调用自己吗?
其实不同,父类的的是DaoAuthenticationProvider,认证成功后返回authentication对象,其实真正认证的是AuthenticationProvider

在这里插入图片描述
在这里插入图片描述

  • 至于为什么所有孩子的ProviderManager为什么要求只有唯一的父ProviderManager,是因为父亲是用来备用和管理的:例如当你的每个微服务都对应一个ProviderManager来进行认证,如果认证失败,可以回到父ProviderManager进行一个自己的统一认证规则,方便管理。简单来说就是全局和局部的Manager
    在这里插入图片描述

到这里我们就明白了,回调的全局ProviderManager中,默认的Authentication是DaoAuthenticationProvider,接下来我们就要看DaoAuthenticationProvider中的authenticate方法怎么做认证的
在这里插入图片描述
首先我们看到retrieveuser这个方法,这个方法是个抽象方法,所以AbstractUserDetailsAuthenticationProvider和调用子类DaoAuthenticationProvide的这个方法,在这里插入图片描述

在这里插入图片描述
最终还是调用了loadUserByUsername()这个方法,真的要扩展数据源的处理,只要重写UserDetailsService中的loadUserByUsername()方法重写即可
在这里插入图片描述
在这里插入图片描述

7. 自定义数据源

7.1 全局配置AuthenticationManager方法

有两种方法,一种是用默认的工厂创建的builer,我们只需要写个UserdetailsService的bean修改对应的方法即可。另外一种就是重写configure方法

区别:自定义的会覆盖掉默认的全局的方法
默认:

  • 默认的会检测是否有UserDetailsService实例,并将其设置为数据源
  • 使用时可在工厂中直接在代码中注入

自定义:

  • 一旦实现,就会覆盖工厂中自动配置的AuthenticationManager
  • 实现后,需要指定认证的数据源,并且不允许在其他的组件中使用,除非覆盖authenticationManagerBean方法,将AuthenticationManager在工厂中暴露

7.2 自定义的数据源

package pers.fjl.server.dto;

import lombok.Builder;
import lombok.Data;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import static com.sun.el.parser.ELParserConstants.FALSE;

/**
 * 用户信息
 *
 */
@Data
@Builder
public class UserDetailDTO implements UserDetails {
    
    

    /**
     * 用户账号id
     */
    private Long uid;

    /**
     * 邮箱号
     */
    private String email;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 用户角色
     */
    private List<String> roleList;

    /**
     * 用户昵称
     */
    private String nickname;

    /**
     * 用户头像
     */
    private String avatar;

    /**
     * 点赞文章集合
     */
    private Set<Object> articleLikeSet;

    /**
     * 点赞评论集合
     */
    private Set<Object> commentLikeSet;

    /**
     * 用户登录ip
     */
    private String lastIp;

    /**
     * ip来源
     */
    private String ipSource;

    /**
     * 是否禁用
     */
    private boolean dataStatus;

    /**
     * 浏览器
     */
    private String browser;

    /**
     * 操作系统
     */
    private String os;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return this.roleList.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
    }

    @Override
    public String getPassword() {
    
    
        return this.password;
    }

    @Override
    public String getUsername() {
    
    
        return this.username;
    }

    /**
     * 账号是否过期
     * @return boolean
     */
    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    
    
        return this.dataStatus;
    }

    /**
     * 认证是否过期
     * @return boolean
     */
    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isEnabled() {
    
    
        return true;
    }

}

重写userService

  /**
     * 返回接口所需的UserDetails实体
     *
     * @param username 用户名
     * @return UserDetails
     */
    @Override
    public UserDetails loadUserByUsername(String username) {
    
    
        if (StringUtils.isBlank(username)) {
    
    
            throw new BizException(MessageConstant.USERNAME_IS_NULL);
        }
        // 查询账号是否存在
        User user = userDao.selectOne(new LambdaQueryWrapper<User>()
                .eq(User::getUsername, username));
        if (Objects.isNull(user)) {
    
    
            throw new BizException(MessageConstant.USER_NOT_EXIST);
        }
        // 封装登录信息
        return convertUserDetail(user, request);
    }

7.3 前后端分离实现

我们要知道的是,在进行login请求时,Spring Security默认只能接受表单认证,无法接收json请求,我们可以进行修改。
UsernamePasswordAuthenticationFilter这个filter中是对该认证进行鉴定的,因此我们可以自定义一个filter
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter

package pers.fjl.server.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * 自定义前后端分离认证Security的Filter(否则login默认只能接受表单请求,无法接受json)
 */
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    
    
        //1.判断是否是POST请求
        if (!request.getMethod().equals("POST")) {
    
    
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //2.判断是否是json格式请求类型
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)){
    
    
            //3.从json数据中获取用户输入用户名和密码进行验证
            try {
    
    
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                this.setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }

        return super.attemptAuthentication(request, response);
    }
}

自定义的LoginFilter

还要记得在配置中注入

package pers.fjl.server.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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import pers.fjl.server.handler.AuthenticationFailHandlerImpl;
import pers.fjl.server.handler.AuthenticationSuccessHandlerImpl;
import pers.fjl.server.handler.LogoutSuccessHandlerImpl;

import javax.annotation.Resource;

/**
 * <p>
 * SpringSecurity配置
 * </p>
 *
 * @author fangjiale
 * @since 2021-01-27
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
    @Resource
    private LogoutSuccessHandlerImpl logoutSuccessHandler;
    @Resource
    private AuthenticationFailHandlerImpl authenticationFailHandler;

    // 自定义filter交给工厂管理
    @Bean
    public LoginFilter loginFilter() throws Exception {
    
    
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/login");
        loginFilter.setUsernameParameter("username");
        loginFilter.setPasswordParameter("password");   //指定json接收的参数的key
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        loginFilter.setAuthenticationFailureHandler(authenticationFailHandler);
        return loginFilter;
    }

    /**
     * 自定义的loginFilter要使用,所以要将这个manager暴露出来给其使用
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
      //将SpringSecurity的拦截请求全部放行
        http.headers().frameOptions().disable();    //Spring Security4默认是将'X-Frame-Options' 设置为 'DENY'
        http
                .formLogin()
//                .loginProcessingUrl("/login")
//                .successHandler(authenticationSuccessHandler)
//                .failureHandler(authenticationFailHandler)
                .and()
                .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)    //默认会话失效
                .clearAuthentication(true)     //清除认证标记
                .logoutSuccessHandler(logoutSuccessHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/**")
//                .anyRequest()
                .permitAll()
                //解决跨域
                .and()
                .cors()
                // 关闭csrf防护
                .and()
                .csrf()
                .disable().exceptionHandling();
//                .and()
//                .sessionManagement()
//                .maximumSessions(20)
//                .sessionRegistry(sessionRegistry());
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 身份认证接口(自定义的AuthenticationManager)
     *
     * @param auth 身份
     * @throws Exception 异常处理
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth
                // 从数据库读取的用户进行身份认证
                .userDetailsService(userDetailsService)
                //加密方法(不加密)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

package pers.fjl.server.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * 自定义前后端分离认证Security的Filter(否则login默认只能接受表单请求,无法接受json)
 */
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    
    
        //1.判断是否是POST请求
        if (!request.getMethod().equals("POST")) {
    
    
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        System.out.println("不是json" + request.getContentType());
        //2.判断是否是json格式请求类型
        if (request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) {
    
    
            System.out.println("是json");
            //3.从json数据中获取用户输入用户名和密码进行验证
            try {
    
    
                Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }

        return super.attemptAuthentication(request, response);
    }
}

猜你喜欢

转载自blog.csdn.net/Dlihctcefrep/article/details/122707335