™技术博客

框架 | 5.oauth2短信验证码登录实现

2021年8月11日

1. spring security oauth2 登录过程详解


1. 定义手机号登录令牌

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
/**
* 手机号登录令牌
* @author: chenrq
* @date: 2021年08月06日 00时08分
*/
public class MobileAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

private final Object principal;

public MobileAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}

@JsonCreator
public MobileAuthenticationToken(@JsonProperty("principal") Object principal,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}

@Override
public Object getPrincipal() {
return this.principal;
}

@Override
public Object getCredentials() {
return null;
}

@Override
@SneakyThrows
public void setAuthenticated(boolean isAuthenticated) {
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();
}

}

2. 手机号登录校验逻辑

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
/**
* 手机登录校验逻辑 验证码登录、社交登录
* @author: chenrq
* @date: 2021年08月06日 00时08分
*/
@Slf4j
public class MobileAuthenticationProvider implements AuthenticationProvider {

private MessageSourceAccessor messages = AiopsSecurityMessageSourceUtil.getAccessor();

private UserDetailsChecker detailsChecker = new AiopsPreAuthenticationChecks();

@Getter
@Setter
private AiopsUserDetailsService userDetailsService;

@Override
@SneakyThrows
public Authentication authenticate(Authentication authentication) {
MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;

String principal = mobileAuthenticationToken.getPrincipal().toString();
UserDetails userDetails = userDetailsService.loadUserBySocial(principal);
if (userDetails == null) {
log.debug("Authentication failed: no credentials provided");

throw new BadCredentialsException(messages
.getMessage("AbstractUserDetailsAuthenticationProvider.noopBindAccount", "Noop Bind Account"));
}

// 检查账号状态
detailsChecker.check(userDetails);

MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(userDetails,
userDetails.getAuthorities());
authenticationToken.setDetails(mobileAuthenticationToken.getDetails());
return authenticationToken;
}

@Override
public boolean supports(Class<?> authentication) {
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}

}

3. 登录过程filter处理

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
/**
* 手机号登录验证filter
* @author: chenrq
* @date: 2021年08月06日 00时07分
*/
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

private static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

@Getter
@Setter
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;

@Getter
@Setter
private boolean postOnly = true;

@Getter
@Setter
private AuthenticationEntryPoint authenticationEntryPoint;

public MobileAuthenticationFilter() {
super(new AntPathRequestMatcher(SecurityConstants.MOBILE_TOKEN_URL, "POST"));
}

@Override
@SneakyThrows
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}

String mobile = obtainMobile(request);

if (mobile == null) {
mobile = "";
}

mobile = mobile.trim();

MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(mobile);

setDetails(request, mobileAuthenticationToken);

Authentication authResult = null;
try {
authResult = this.getAuthenticationManager().authenticate(mobileAuthenticationToken);

logger.debug("Authentication success: " + authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);

}
catch (Exception failed) {
SecurityContextHolder.clearContext();
logger.debug("Authentication request failed: " + failed);

try {
authenticationEntryPoint.commence(request, response,
new UsernameNotFoundException(failed.getMessage(), failed));
}
catch (Exception e) {
logger.error("authenticationEntryPoint handle error:{}", failed);
}
}

return authResult;
}

private String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}

private void setDetails(HttpServletRequest request, MobileAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

}

4. 生产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
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
/**
* 手机号登录成功,返回oauth token
* @author: chenrq
* @date: 2021年08月05日 23时59分
*/
@Slf4j
public class MobileLoginSuccessHandler implements AuthenticationSuccessHandler {

private static final String BASIC_ = "Basic ";

@Autowired
private ObjectMapper objectMapper;

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private ClientDetailsService clientDetailsService;

@Lazy
@Autowired
private AuthorizationServerTokenServices defaultAuthorizationServerTokenServices;

/**
* Called when a user has been successfully authenticated. 调用spring security oauth API
* 生成 oAuth2AccessToken
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);

if (header == null || !header.startsWith(BASIC_)) {
throw new UnapprovedClientAuthenticationException("请求头中client信息为空");
}

try {
String[] tokens = AuthUtils.extractAndDecodeHeader(header);
assert tokens.length == 2;
String clientId = tokens[0];

ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

// 校验secret
if (!passwordEncoder.matches(tokens[1], clientDetails.getClientSecret())) {
throw new InvalidClientException("Given client ID does not match authenticated client");

}

TokenRequest tokenRequest = new TokenRequest(MapUtil.newHashMap(), clientId, clientDetails.getScope(),
"mobile");

// 校验scope
new DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails);
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken oAuth2AccessToken = defaultAuthorizationServerTokenServices
.createAccessToken(oAuth2Authentication);
log.info("获取token 成功:{}", oAuth2AccessToken.getValue());

response.setCharacterEncoding(CharsetUtil.UTF_8);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter printWriter = response.getWriter();
printWriter.append(objectMapper.writeValueAsString(oAuth2AccessToken));
}
catch (IOException e) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}

}

}

5. 配置以上自定义

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
/**
* 手机号登录配置入口
* @author: chenrq
* @date: 2021年08月06日 00时08分
*/
@Getter
@Setter
public class MobileSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

@Autowired
private ObjectMapper objectMapper;

@Autowired
private AuthenticationSuccessHandler mobileLoginSuccessHandler;

@Autowired
private AiopsUserDetailsService userDetailsService;

@Override
public void configure(HttpSecurity http) {
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
mobileAuthenticationFilter.setAuthenticationSuccessHandler(mobileLoginSuccessHandler);
mobileAuthenticationFilter.setAuthenticationEntryPoint(new AiopsCommenceAuthExceptionEntryPoint(objectMapper));

MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider();
mobileAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(mobileAuthenticationProvider).addFilterAfter(mobileAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}

}

6. 在spring security 配置 上边定一个的那个聚合配置

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

@Override
@SneakyThrows
protected void configure(HttpSecurity http) {
http.formLogin().loginPage("/token/login").loginProcessingUrl("/token/form")
.successHandler(tenantSavedRequestAwareAuthenticationSuccessHandler())
.failureHandler(authenticationFailureHandler()).and().logout()
.logoutSuccessHandler(logoutSuccessHandler()).deleteCookies("JSESSIONID").invalidateHttpSession(true)
.and().authorizeRequests().antMatchers("/token/**", "/actuator/**", "/mobile/**").permitAll()
.anyRequest().authenticated().and().csrf().disable().apply(mobileSecurityConfigurer());
}

7. 使用

1
2
curl -H "Authorization:Basic cGlnOnBpZw==" -d "grant_type=mobile&scope=server&mobile=17034642119&code=" http://localhost:9999/auth/mobile/token

扫描二维码,分享此文章