基于Spring Security实现自定义认证-以短信登录

基于Spring Security实现自定义认证-以短信登录为例

《Spring Security实战》、慕课网《Spring Security技术栈开发企业级认证与授权》笔记

实现基于Spring Security的认证有两种方式

  1. 增加一个过滤器继承OncePerRequestFilter,将这个Filter放到HttpSecurity的合适的位置。(继承OncePerRequestFilter的目的是确保一次请求只通过一次该过滤器)
  2. 基于Spring Security的自定义认证

方法1的那种过滤器的方式大家应该很熟悉,就不展开记录了,下面详细说一下基于Spring Security的自定义认证。

先以UsernamePassword认证为例,先捋一下认证流程。

基本概念

  • Authentication:Spring Security验证的封装类,包括权限、确定身份正确的凭据、身份详细信息、是否被验证。常见的实现类有RememberMeAuthenticationTokenUsernamePasswordAuthenticationToken
  • AuthenticationProvider:Spring Security的一个验证过程,一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager管理。Authentication在AuthenticationProvider中流动。用大白话说就是不同的AuthenticationProvider提供不同的token。
  • AuthenticationManager:处理Authentication的请求,整个系统只有一个。ProviderManager是AuthenticationManager的实现类

UsernamePassword认证流程

  1. 进入UsernamePasswordAuthenticationFilter类:attemptAuthentication()方法中将前端传过来的usernamepassword封装到UsernamePasswordAuthenticationToken中并标记为未认证。最后调用this.getAuthenticationManager().authenticate(authRequest)交给AuthenticationManager处理。
  2. ProviderManager根据传入的token类从众多的AuthenticationProvider中找出合适的AuthenticationProvider来处理改认证。
  3. 在具体的XXXAuthenticationProvider中认证用户返回带有认证通过和详细信息的token。

所以我们基于Spring Security自定义一个认证要新建一个XXXAuthenticationToken和XXXAuthenticationProvider,最后将自己的逻辑加入到HttpSecurity的过滤器链中。

下边以短信登录为例实践上边的知识点。

短信登录逻辑:

  1. 前端输入手机号,然后获取短信验证码,最后带着手机号和验证码一起登录
  2. 服务端要监听这个登录地址,校验验证码是否正确
  3. 正确使用Spring Security自定义认证颁发一个token给前端。

下面贴代码:

SmsValidateCodeFilter

短信验证码过滤器,验证短信登录验证码是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
java复制代码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写就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码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类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码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

将自己写的这些类配置到过滤器链上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码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

项目的安全配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
java复制代码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)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%