基于Spring Security实现自定义认证-以短信登录为例
《Spring Security实战》、慕课网《Spring Security技术栈开发企业级认证与授权》笔记
实现基于Spring Security的认证有两种方式
- 增加一个过滤器继承OncePerRequestFilter,将这个Filter放到HttpSecurity的合适的位置。(继承OncePerRequestFilter的目的是确保一次请求只通过一次该过滤器)
- 基于Spring Security的自定义认证
方法1的那种过滤器的方式大家应该很熟悉,就不展开记录了,下面详细说一下基于Spring Security的自定义认证。
先以UsernamePassword认证为例,先捋一下认证流程。
基本概念
Authentication:Spring Security验证的封装类,包括权限、确定身份正确的凭据、身份详细信息、是否被验证。常见的实现类有
RememberMeAuthenticationToken
、UsernamePasswordAuthenticationToken
AuthenticationProvider:Spring Security的一个验证过程,一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager管理。Authentication在AuthenticationProvider中流动。用大白话说就是不同的AuthenticationProvider提供不同的token。
AuthenticationManager:处理Authentication的请求,整个系统只有一个。ProviderManager是AuthenticationManager的实现类
UsernamePassword认证流程
- 进入
UsernamePasswordAuthenticationFilter
类:attemptAuthentication()
方法中将前端传过来的username
、password
封装到UsernamePasswordAuthenticationToken
中并标记为未认证。最后调用this.getAuthenticationManager().authenticate(authRequest)
交给AuthenticationManager处理。 - ProviderManager根据传入的token类从众多的AuthenticationProvider中找出合适的AuthenticationProvider来处理改认证。
- 在具体的XXXAuthenticationProvider中认证用户返回带有认证通过和详细信息的token。
所以我们基于Spring Security自定义一个认证要新建一个XXXAuthenticationToken和XXXAuthenticationProvider,最后将自己的逻辑加入到HttpSecurity的过滤器链中。
下边以短信登录为例实践上边的知识点。
短信登录逻辑:
- 前端输入手机号,然后获取短信验证码,最后带着手机号和验证码一起登录
- 服务端要监听这个登录地址,校验验证码是否正确
- 正确使用Spring Security自定义认证颁发一个token给前端。
下面贴代码:
SmsValidateCodeFilter
短信验证码过滤器,验证短信登录验证码是否正确
package com.zchi.customizeAuthentication.security.smsCode;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description 短信验证码过滤器,验证短信登录验证码是否正确
* @Author 张弛
* @Datee 2021/7/4
* @Version 1.0
**/
@Setter
public class SmsValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AntPathMatcher pathMatcher = new AntPathMatcher();
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
String url = "/authentication/smsLogin";
if (pathMatcher.match(url, httpServletRequest.getRequestURI())) {
action = true;
}
if (action) {
try {
validate(new ServletWebRequest(httpServletRequest));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// todo 获取验证码,现在先写死,之后改成从session或者redis中获取
String codeInSession = "f123";
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
// todo 验证码是否过期
if(!StringUtils.equals(codeInSession, codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
// todo 使用完这个验证码就删除
}
public void setFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
}
SmsCodeAuthenticationToken
直接照着UsernamePasswordAuthenticationToken写就行
package com.zchi.customizeAuthentication.security.smsCode;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import javax.security.auth.Subject;
import java.util.Collection;
/**
* @Description 直接照着UsernamePasswordAuthenticationToken写就行
* @Author 张弛
* @Datee 2021/7/3
* @Version 1.0
**/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.principal;
}
@Override
public Object getPrincipal() {
return null;
}
@Override
public boolean implies(Subject subject) {
return false;
}
}
SmsCodeAuthenticationProvider
Authentication的提供者,根据之前SmsCodeAuthenticationFilter中存入token中的信息获取当前用户信息。声明支持的token类型
package com.zchi.customizeAuthentication.security.smsCode;
import lombok.Data;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* @Description Authentication的提供者,根据之前SmsCodeAuthenticationFilter中存入token中的信息获取当前用户信息。声明支持的token类型
* @Author 张弛
* @Datee 2021/7/3
* @Version 1.0
* @see SmsCodeAuthenticationFilter,SmsCodeAuthenticationToken
**/
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) token.getPrincipal());
if(user == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
// 需要把未认证中的一些信息copy到已认证的token中
authenticationResult.setDetails(token);
return authenticationResult;
}
// 该provider支持的token
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
SmsCodeAuthenticationSecurityConfig
将自己写的这些类配置到过滤器链上
package com.zchi.customizeAuthentication.security.smsCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* @Description 将自己写的这些类配置到过滤器链上
* @Author 张弛
* @Datee 2021/7/3
* @Version 1.0
**/
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private SmsCodeAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private SmsCodeAuthenctiationFailureHandler authenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity httpSecurity) {
SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
SmsValidateCodeFilter smsValidateCodeFilter = new SmsValidateCodeFilter();
smsValidateCodeFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
httpSecurity.addFilterBefore(smsValidateCodeFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
}
}
SecurityConfig
项目的安全配置类
package com.zchi.customizeAuthentication.security;
import com.zchi.customizeAuthentication.security.smsCode.SmsCodeAuthenticationSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* @Description 项目的安全配置
* @Author 张弛
* @Datee 2021/7/3
* @Version 1.0
**/
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfigs;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/authentication/require",
"/login",
"/code/*",
"/error",
"/authentication/smsLogin"
)
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
// 将我们自己的短信登录配置到项目的过滤器链上
.apply(smsCodeAuthenticationSecurityConfigs);
}
@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> {
return new UserDetails() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
};
};
}
}
参考:
《Spring Security实战》
《Spring Security技术栈开发企业级认证与授权》
代码连接securityDemo: 《基于Spring Security实现自定义认证-以短信登录为例》代码 (gitee.com)