一、简单叙述
上一节【SpringSecurity登陆原理(源码级讲解)】已经讲过了完整的登录流程,所以此处不再重复讲解,只是接着上一节继续讲解。登陆完后会给我们返回信息,这没毛病,但是问题来了,他跳转了页面,假设我们不想跳转页面,只想返回固定格式的JSON呢?这就需要我们自定义成功处理器了。失败的情况也雷同。
二、源码分析
(一)、接着上一节。AbstractAuthenticationProcessingFilter.doFilter()
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
// 过滤器doFilter方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
/*
* 判断当前filter是否可以处理当前请求,若不行,则交给下一个filter去处理。
*/
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
// 很关键!!!调用了子类(UsernamePasswordAuthenticationFilter)的方法
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
// 最终认证成功后,会处理一些与session相关的方法(比如将认证信息存到session等操作)。
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);
}
/*
* 最终认证成功后的相关回调方法,主要将当前的认证信息放到SecurityContextHolder中
* 并调用成功处理器做相应的操作。
*/
successfulAuthentication(request, response, chain, authResult);
}
}
PS:不难发现,我们成功/失败后会各自调用一个方法来处理。
(二)、成功处理器
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
// 将当前的认证信息放到SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
// 调用成功处理器,可以自己实现AuthenticationSuccessHandler接口重写方法写自己的逻辑
successHandler.onAuthenticationSuccess(request, response, authResult);
}
最后一句话调用了AuthenticationSuccessHandler
接口的onAuthenticationSuccess
方法。这下简单了,我们可以自己写一个类去实现此接口。并配置为接口的处理器即可。
(三)、失败处理器
同理,调用了AuthenticationFailureHandler
接口的onAuthenticationFailure
方法。老规矩,我们依然是自己写一个类去实现此接口,并配置为此接口的处理器即可。
三、总结
这。。。好尴尬,没啥说的。因为很简单,只要摸清楚上一章节的东西,这章节一点压力都没有,看一眼就懂了。一步步分析我给出的源码即可。实在有问题留言给我,看到必回!
四、Demo
1、成功处理器
package com.chentongwei.security.browser.authentication;
import com.alibaba.fastjson.JSON;
import com.chentongwei.security.core.entity.SimpleResponse;
import com.chentongwei.security.core.enums.LoginType;
import com.chentongwei.security.core.properties.SecurityProperties;
import org.apache.commons.lang.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 为什么不实现接口,而是继承SavedRequestAwareAuthenticationSuccessHandler类的方式?
* 因为SavedRequestAwareAuthenticationSuccessHandler这个类记住了你上一次的请求路径,比如:
* 你请求user.html。然后被拦截到了登录页,这时候你输入完用户名密码点击登录,会自动跳转到user.html,而不是主页面。
*
* 若是前后分离项目则实现接口即可,因为我弄的是通用的权限组件,所以选择了继承
*
* @author [email protected] 2018-03-26 14:09
*/
@Component("ctwAuthenticationSuccessHandler")
public class CtwAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
logger.info("登录成功!");
if (ObjectUtils.equals(securityProperties.getBrowser().getLoginType(), LoginType.JSON)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new SimpleResponse(HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase(), authentication)));
} else {
// 会帮我们跳转到上一次请求的页面上
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
PS:我是根据配置loginType是否是JSON而定返回形式,JSON反回JSON,REDIRECT跳转页面。
2、失败处理器
package com.chentongwei.security.browser.authentication;
import com.alibaba.fastjson.JSON;
import com.chentongwei.security.core.entity.SimpleResponse;
import com.chentongwei.security.core.enums.LoginType;
import com.chentongwei.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* 自定义失败处理器
*
* @author [email protected] 2018-03-26 14:02
*/
@Component("ctwAuthenticationFailureHandler")
public class CtwAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("登录失败!");
if (Objects.equals(securityProperties.getBrowser().getLoginType(), LoginType.JSON)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new SimpleResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage(), null)));
} else {
response.setContentType("text/html;charset=UTF-8");
super.onAuthenticationFailure(request, response, exception);
}
}
}
PS:我是根据配置loginType是否是JSON而定返回形式,JSON反回JSON,REDIRECT跳转页面。
3、配置类
package com.chentongwei.security.core.authentication;
import com.chentongwei.security.core.constant.SecurityConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
/**
* 封装处理请求成功/失败的操作
*
* @author [email protected] 2018-03-26 10:35
*/
public class AbstractChannelSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler ctwAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler ctwAuthenticationFailureHandler;
protected void authenticationConfig(HttpSecurity http) throws Exception {
// 表单登录
http.formLogin()
// 默认表单登录页
.loginPage(SecurityConstant.DEFAULT_UNAUTHENTICATION_URL)
// 登录接口
.loginProcessingUrl(SecurityConstant.DEFAULT_LOGIN_PROCESSING_URL_FORM)
.successHandler(ctwAuthenticationSuccessHandler)
.failureHandler(ctwAuthenticationFailureHandler);
}
}
4、html登录页
登录
标准登录页面
表单登录
5、Service类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
* @author [email protected] 2018-03-26 13:15
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("表单登录用户名:" + username);
return buildUser(username);
}
private UserDetails buildUser(String username) {
/**
* passwordEncoder.encode这步骤应该放到注册接口去做,而这里只需要传一个从db查出来的pwd即可。
*
* passwordEncoder.encode("123456")每次打印出来都是不同的,虽然是同一个(123456)密码,
* 但是他会随机生成一个盐(salt),他会把随机生成的盐混到加密的密码里。Springsecurity验证(matches方法)的时候会将利用此盐解析出pwd,进行匹配。
* 这样的好处是:如果数据库里面有10个123456密码。但是被破解了1个,那么另外九个是安全的,因为db里存的串是不一样的。
*/
String password = passwordEncoder.encode("123456");
logger.info("数据库密码是:" + password);
// 这个User不一定必须用SpringSecurity的,可以写一个自定义实现UserDetails接口的类,然后把是否锁定等判断逻辑写进去。
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
PS:具体完整的代码请看我的码云,已经全部提交到码云了。
五、广告
-
Demo源码已上传到码云,文章会定期更新。下面链接是我对Spring-Security进行的二次封装。使之变得零配置,高扩展。如果觉得对您有帮助,希望给个star,没帮助也可以看看框架思想。
https://gitee.com/geekerdream/common-security
QQ群【Java初学者学习交流群】:458430385
-
微信公众号【Java码农社区】
今日头条号:编程界的小学生