某项目后台管理,既管理后台账号,又管理前台账号,前台账号信息存储front_account, 后台账号信息存储back_account。这里通过spring security 既管理后台账号的认证,也管理前台账号的认证。
既然是两张表,而且为了适应不同环境,那么首先我们知道,单个项目内既要实现前台登录,又要实现后台登录,那么前后台的登录路径肯定不同,基于路径的区分,我们需要关心两点:
1.security是否对路径做拦截。
2.重写loadUserByUsername(String username)方法
路径拦截在UsernamePasswordAuthenticationFilter中可以看到new AntPathRequestMatcher("/login", “POST”),这段代码就是过滤,所以重写它
loadUserByUsername authenticate#retrieveUser 里面有调用到loadUserByUsername,所以重写它
基本套路为:
config 继承WebSecurityConfigurerAdapter,重写configure(HttpSecurity http)
Filter 继承AbstractAuthenticationProcessingFilter,参考UsernamePasswordAuthenticationFilter 实现路径过滤和attemptAuthentication(身份验证)
token 继承AbstractAuthenticationToken, 参考UsernamePasswordAuthenticationToken
provider 实现AuthenticationProvider,参考DaoAuthenticationProvider 重写authenticate和retrieveUser方法
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
...//TODO 主要是下面两个
//后台过滤
http.addFilterAt(backAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//前台过滤
http.addFilterAt(frontAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
BackAuthenticationFilter backAuthenticationFilter() throws Exception {
BackAuthenticationFilter filter = new BackAuthenticationFilter();
filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Token token = tokenService.saveToken(loginUser);
ResponseUtil.responseJson(response, HttpStatus.OK.value(), token);
}
});
filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String msg = null;
if (exception instanceof BadCredentialsException) {
msg = "密码错误";
} else {
msg = exception.getMessage();
}
ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), Result.error(HttpStatus.UNAUTHORIZED.value(), msg));
}
});
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
public class BackAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public BackAuthenticationFilter() {
//路径过滤
super(new AntPathRequestMatcher("/back/login", "POST"));
}
// ~ Methods
// ========================================================================================================
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
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();
AbstractAuthenticationToken authRequest = new BackAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
protected void setDetails(HttpServletRequest request,
AbstractAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return usernameParameter;
}
public final String getPasswordParameter() {
return passwordParameter;
}
}
public class BackAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private String credentials;
public BackAuthenticationToken(Object principal, String credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
// ~ Methods
// ========================================================================================================
public String getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
public class BackAuthenticationProvider implements AuthenticationProvider {
@Autowired
@Qualifier("bCryptPasswordEncoder")
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private BackUserDetailsServiceImpl userDetailsService;
/**
* The plaintext password used to perform
* PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean hideUserNotFoundExceptions = true;
/**
* The password used to perform
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is not
* in a valid format.
*/
private volatile String userNotFoundEncodedPassword;
public UserCache getUserCache() {
return userCache;
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
String password = (String) authentication.getCredentials();
if (StringUtils.isEmpty(password)) {
throw new BadCredentialsException("密码不能为空");
}
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(BackAuthenticationToken) authentication);
} catch (UsernameNotFoundException notFound) {
log.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");
}
if (null == user) {
throw new BadCredentialsException("用户不存在");
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("用户名或密码不正确");
}
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
UsernamePasswordAuthenticationToken result =
new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
private UserDetails retrieveUser(String username, BackAuthenticationToken authentication) throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.userDetailsService.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);
}
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
}
private void mitigateAgainstTimingAttack(BackAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
@Override
public boolean supports(Class<?> authentication) {
return (BackAuthenticationToken.class.isAssignableFrom(authentication));
}
}