文章目录
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
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);
}
}