1、spring security的基本工作流程
首先 我们需要了解spring security的基本工作流程
-
当登录请求进来时,会在UsernamePasswordAuthenticationFilter 里构建一个没有权限的 Authentication , -
然后把Authentication 交给 AuthenticationManager 进行身份验证管理 -
而AuthenticationManager 本身不做验证 ,会交给 AuthenticationProvider 进行验证 -
AuthenticationProvider 会调用 UserDetailsService 对用户信息进行校验
- 我们可以自定义自己的类来 实现UserDetailsService 接口
- 就可以根据自己的业务需要进行验证用户信息 , 可以从数据库进行用户账号密码校验
- 也可以 在redis 中用手机号和code 进行校验
-
UserDetailsService 校验成功后 会返回 UserDetails类,里面存放着用户祥细信息
- 一般我们会重新写一个自定义的类来继承 UserDetails ,方便数据转换。
-
验证成功后 会重新构造 Authentication 把 UserDetails 传进去,并把认证 改为 true super.setAuthenticated(true) -
验证成功后来到 AuthenticationSuccessHandler 验证成功处理器 ,在里面可以返回数据给前端
2、spring security 的 短信登录流程
对基本的流程熟悉后,我们就可以仿照密码登录 来自己定义短信验证方法
3、代码
根据上图 我们要重写 SmsAuthenticationFilter、SmsAuthenticationProvider、UserDetailsService、UserDetails,来模拟用户密码登录
在写一些配置类来启用我们的短信业务流程 SmsSecurityConfigurerAdapter MySecurityConfig extends WebSecurityConfigurerAdapter
还有自定义成功方法 public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler
自定义 用户认证失败异常 MyAuthenticationEntryPointImpl implements AuthenticationEntryPoint
SmsAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
private String phoneParameter = "phone";
private String smsCodeParameter = "code";
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String phone = this.obtainPhone(request);
phone = phone != null ? phone : "";
phone = phone.trim();
String smsCode = this.obtainSmsCode(request);
smsCode = smsCode != null ? smsCode : "";
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(phone, smsCode);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainSmsCode(HttpServletRequest request) {
return request.getParameter(this.smsCodeParameter);
}
@Nullable
protected String obtainPhone(HttpServletRequest request) {
return request.getParameter(this.phoneParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setPhoneParameter(String phoneParameter) {
Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");
this.phoneParameter = phoneParameter;
}
public void setSmsCodeParameter(String smsCodeParameter) {
Assert.hasText(smsCodeParameter, "SmsCode parameter must not be empty or null");
this.smsCodeParameter = smsCodeParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.phoneParameter;
}
public final String getPasswordParameter() {
return this.smsCodeParameter;
}
}
SmsAuthenticationProvider
验证手机号和短信是否匹配 核心业务
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
private static final String REDIS_LONGIN_PRE = "login:";
private SmsDetailsServiceImpl smsUserDetailsService;
private StringRedisTemplate stringRedisTemplate;
public SmsAuthenticationProvider (SmsDetailsServiceImpl userDetailsServiceImpl,StringRedisTemplate stringRedisTemplate) {
this.smsUserDetailsService = userDetailsServiceImpl;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
Object principal = authentication.getPrincipal();
String phone = "";
if (principal instanceof String) {
phone = (String) principal;
}
String inputCode = (String) authentication.getCredentials();
String redisCode =stringRedisTemplate.opsForValue().get(REDIS_LONGIN_PRE+phone);
if (StringUtils.isEmpty(redisCode)) {
throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
}
if (!inputCode.equals(redisCode)) {
throw new BadCredentialsException("输入的验证码不正确,请重新输入");
}
stringRedisTemplate.delete(REDIS_LONGIN_PRE+phone);
LoginUser userDetails = (LoginUser) smsUserDetailsService.loadUserByUsername(phone);
if (userDetails == null) {
throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,inputCode, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
UserDetailsService
查询用户信息
@Service
public class SmsDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
System.out.println("正在使用SmsDetailsServiceImpl。。。。。");
QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
System.out.println("手机号为:" + phone);
wrapper.eq("mobile",phone);
UserEntity userEntity = userMapper.selectOne(wrapper);
if(Objects.isNull(userEntity)){
throw new RuntimeException("用户不存在");
}
LoginUser user = new LoginUser(userEntity, Arrays.asList("test","admin"));
return user;
}
}
SmsCodeAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
public SmsCodeAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object 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;
}
}
UserDetails
自定义UserDetails类 方便数据获取
@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
private UserEntity userEntity;
public LoginUser(UserEntity userEntity) {
this.userEntity = userEntity;
}
private List<String> permissions;
@JSONField(serialize = false)
List<SimpleGrantedAuthority> authorities;
public LoginUser(UserEntity userEntity, List<String> permissions) {
this.userEntity = userEntity;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null)
return authorities;
authorities = permissions.stream().map(item -> {
return new SimpleGrantedAuthority(item);
}).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
SmsSecurityConfigurerAdapter
单独配置短信验证
@Component
public class SmsSecurityConfigurerAdapter extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private SmsDetailsServiceImpl userDetailsService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
SmsAuthenticationProvider smsCodeAuthenticationProvider = new SmsAuthenticationProvider(userDetailsService,redisTemplate);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
MySecurityConfig
把 短信验证配置添加到总配置
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
MyAuthenticationEntryPointImpl myAuthenticationEntryPoint;
@Autowired
MyAccessDeniedHandlerImpl myAccessDeniedHandler;
@Autowired
SmsSecurityConfigurerAdapter smsSecurityConfigurerAdapter;
@Autowired
UsernamePassSecurityConfAdapter usernamePassSecurityConfAdapter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login","/sms/login","/sms/login/test","/sms/sendcode").anonymous()
.antMatchers("/").hasAnyAuthority("admin")
.anyRequest().authenticated()
.and().apply(smsSecurityConfigurerAdapter);
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint);
http.cors();
}
}
MyAuthenticationSuccessHandler
自定义认证成功方法 主要用来生成token 后在返回给前端
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
static StringRedisTemplate stringRedisTemplate;
private static final String REDIS_LONGIN_PRE = "login:";
private static final String REDIS_LONGIN_TOKEN = "login:token:";
@Autowired
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
MyAuthenticationSuccessHandler.stringRedisTemplate = stringRedisTemplate;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
LoginUser principal = (LoginUser) authentication.getPrincipal();
String token = new JWTEasyUtil().createToken(principal.getUserEntity().getId());
this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_TOKEN+principal.getUserEntity().getId(),token,1, TimeUnit.HOURS);
this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_PRE+principal.getUserEntity().getId(), JSONObject.toJSONString(principal));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(token));
}
}
MyAuthenticationEntryPointImpl
自定义 用户认证失败异常
@Component
public class MyAuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
response.getWriter().println("<h1>"+authException.getMessage()+"</h1>");
}
}
JwtAuthenticationTokenFilter
此过滤器最先执行,用来校验 前端发送请求的token
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
StringRedisTemplate redisTemplate;
private static final String REDIS_LONGIN_PRE = "login:";
private static final String REDIS_LONGIN_TOKEN = "login:token:";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
filterChain.doFilter(request, response);
return;
}
String userId = null;
try {
Claims parseToken = new JWTEasyUtil().parseToken(token);
userId = parseToken.getSubject();
} catch (Exception e) {
e.printStackTrace();
}
String redisToken = redisTemplate.opsForValue().get(REDIS_LONGIN_TOKEN + userId);
if(StringUtils.isEmpty(redisToken) || (!redisToken.equals(token))){
throw new AccountExpiredException("token过期,请重新登录");
}
String userJson = redisTemplate.opsForValue().get(REDIS_LONGIN_PRE + userId);
if(Objects.isNull(userJson)){
throw new AccountExpiredException("请重新登录");
}
LoginUser loginUser = JSONObject.parseObject(userJson, LoginUser.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
controller()
@RestController
public class LoginController {
@Autowired
LoginService loginService;
@GetMapping("/sms/sendcode")
public BaseResult sendCode(@RequestParam("phone") String phone) {
return loginService.sendCode(phone);
}
@GetMapping("/loginout")
public BaseResult loginOut() {
return loginService.logout();
}
}
public interface LoginService extends IService<UserEntity> {
BaseResult logout();
BaseResult sendCode(String phone);
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
System.out.println("用户名:"+username);
wrapper.eq("username",username);
UserEntity userEntity = userMapper.selectOne(wrapper);
if(Objects.isNull(userEntity)){
throw new RuntimeException("用户不存在");
}
LoginUser user = new LoginUser(userEntity,Arrays.asList("test","admin"));
return user;
}
}
4、源码分析
- 验证登录请求一进来,首先进入这个。 我们要 创建
SmsAuthenticationFilter ,用来根据手机号来查找用户信息, 此类 仿写UsernamePasswordAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
private String phoneParameter = "phone";
private String smsCodeParameter = "code";
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String phone = this.obtainPhone(request);
phone = phone != null ? phone : "";
phone = phone.trim();
String smsCode = this.obtainSmsCode(request);
smsCode = smsCode != null ? smsCode : "";
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(phone, smsCode);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainSmsCode(HttpServletRequest request) {
return request.getParameter(this.smsCodeParameter);
}
@Nullable
protected String obtainPhone(HttpServletRequest request) {
return request.getParameter(this.phoneParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setPhoneParameter(String phoneParameter) {
Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");
this.phoneParameter = phoneParameter;
}
public void setSmsCodeParameter(String smsCodeParameter) {
Assert.hasText(smsCodeParameter, "SmsCode parameter must not be empty or null");
this.smsCodeParameter = smsCodeParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.phoneParameter;
}
public final String getPasswordParameter() {
return this.smsCodeParameter;
}
}
- 根据
SmsAuthenticationFilter 里的return this.getAuthenticationManager().authenticate(authRequest); 来到了ProviderManager 执行authenticate(Authentication authentication) 方法。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = 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 {
result = provider.authenticate(authentication);
- 当 getProviders()遍历到 支持SmsCodeAuthenticationToken的
AuthenticationProvider 后,会执行上面Authentication authenticate(Authentication authentication) 方法里的 result = provider.authenticate(authentication);
? 也就来到了 SmsAuthenticationProvider 里,执行里面的 public Authentication authenticate(Authentication authentication) throws AuthenticationException { 方法 ,在方法里进行短信验证码校验。
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
private static final String REDIS_LONGIN_PRE = "login:";
private SmsDetailsServiceImpl smsUserDetailsService;
private StringRedisTemplate stringRedisTemplate;
public SmsAuthenticationProvider (SmsDetailsServiceImpl userDetailsServiceImpl,StringRedisTemplate stringRedisTemplate) {
this.smsUserDetailsService = userDetailsServiceImpl;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
Object principal = authentication.getPrincipal();
String phone = "";
if (principal instanceof String) {
phone = (String) principal;
}
String inputCode = (String) authentication.getCredentials();
String redisCode =stringRedisTemplate.opsForValue().get(REDIS_LONGIN_PRE+phone);
if (StringUtils.isEmpty(redisCode)) {
throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
}
if (!inputCode.equals(redisCode)) {
throw new BadCredentialsException("输入的验证码不正确,请重新输入");
}
stringRedisTemplate.delete(REDIS_LONGIN_PRE+phone);
LoginUser userDetails = (LoginUser) smsUserDetailsService.loadUserByUsername(phone);
if (userDetails == null) {
throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,inputCode, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
-
经过 SmsAuthenticationProvider 里的Authentication authenticate(Authentication authentication) 方法后,会返回到 ProviderManager 里继续执行sessionStrategy.onAuthentication(authResult, request, response) 方法。 然后来到AbstractAuthenticationProcessingFilter 类里,执行successHandler.onAuthenticationSuccess(request, response, authResult) 此时就来到了自定义认证成功方法MyAuthenticationSuccessHandler 里,执行里面的onAuthenticationSuccess 方法。
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
static StringRedisTemplate stringRedisTemplate;
private static final String REDIS_LONGIN_PRE = "login:";
private static final String REDIS_LONGIN_TOKEN = "login:token:";
@Autowired
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
MyAuthenticationSuccessHandler.stringRedisTemplate = stringRedisTemplate;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
LoginUser principal = (LoginUser) authentication.getPrincipal();
String token = new JWTEasyUtil().createToken(principal.getUserEntity().getId());
this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_TOKEN+principal.getUserEntity().getId(),token,1, TimeUnit.HOURS);
this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_PRE+principal.getUserEntity().getId(), JSONObject.toJSONString(principal));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(token));
}
}
5、测试
发送验证码
验证码登录
6、项目源码
地址 https://github.com/Anan-X/spring_security_demo.git
|