Spring Security-25-用户名密码登录重构

上文中,了解了 Spring Security OAuth 的一个大致的流程,本文将我们之前写的用户名密码登录的方式重新修改一下,让他登录之后也返回一个token, 后续请求都通过token来请求

实现思路

回顾一下之前的流程图:
image

我们在用户名密码等登陆认证完成之后,会生成一个已认证的 Authentication 对象, 看一下图中的 Authentication 这里,也就是说我们再搞到一个 OAuth2Request 对象,就可以用他后面的逻辑去处理生成了,大致流程图如下
image

这里就是在登录成功之后,在登录成功的处理器里面,从请求中获取到basic client的信息,然后去获取 ClientDetails 的信息, 虚线框住的部分是需要我们自己去实现的

具体实现

我们最终是希望拿到一个 OAuth2Request 对象, 如图,得先通过 ClientDetailsService 获取 ClientDetails ,所以第一步得先从请求中把 ClientId 拿出来

之前我们的请求头中有 Authorization:Basic dGVzdEFwcGlkOnRlc3RTZWNyZXQ= 我们只要把这个拿出来做解析就可以拿到 clientId

BasicAuthenticationFilter 中有一段解析的代码, 如下:
image

具体的解码方法代码如下:
image

把这两段代码直接复制到我们的成功处理器里面,然后做一些修改即可,如下:

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
@Slf4j
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

@Autowired
private ObjectMapper objectMapper;

@Autowired
private ClientDetailsService clientDetailsService;

@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;

@Autowired
private SecurityProperties securityProperties;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("用户登录成功...");

String header = request.getHeader("Authorization");

if (header == null || !header.startsWith("Basic ")) {
throw new UnapprovedClientAuthenticationException("请在header中传入client信息");
}


String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;

String clientId = tokens[0];
String clientSecret = tokens[1];

// 拿到client的信息之后,去构建clientDetails
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
// 验证
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("clientId错误");
} else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
throw new UnapprovedClientAuthenticationException("clientSecret不匹配");
}

// 创建 TokenRequest对象
TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom");

OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

// 返回
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(accessToken));
}

private String[] extractAndDecodeHeader(String header, HttpServletRequest request)
throws IOException {

byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
}
catch (IllegalArgumentException e) {
throw new BadCredentialsException(
"Failed to decode basic authentication token");
}

String token = new String(decoded, "UTF-8");

int delim = token.indexOf(":");

if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
}

}

这里主要是先获取 client 的信息,然后一步一步往下创建对象就可以了

资源服务器的配置

现在项目里的一些登录路径什么的都没有做配置,所以在资源服务器的配置类里还需要做一些配置

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
@Configuration
@EnableResourceServer
public class MyResourcesServerConfig extends ResourceServerConfigurerAdapter {

@Autowired
private SecurityProperties securityProperties;

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private DataSource dataSource;

@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;

@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;

@Autowired
private SpringSocialConfigurer securitySocialConfigurer;

@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler;

@Override
public void configure(HttpSecurity http) throws Exception {
// web网页登录的配置
http.formLogin()
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM)
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler);

http
.apply(validateCodeSecurityConfig)
.and()
.apply(smsAuthenticationSecurityConfig)
.and()
.apply(securitySocialConfigurer)
.and()
.authorizeRequests()
// 匹配的是登录页的话放行
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*",
securityProperties.getBrowser().getLoginPage(),
SecurityConstants.DEFAULT_SIGN_UP_URL,
securityProperties.getBrowser().getSignUpPage(),
SecurityConstants.GET_SOCIAL_USER_URL,
securityProperties.getBrowser().getSession().getSessionInvalidUrl(),
"/user/register"
)
.permitAll()
// 授权请求. anyRequest 就表示所有的请求都需要权限认证
.anyRequest().authenticated()
.and()
.csrf().disable()
;
}
}

直接先把浏览器的配置拷过来,然后去掉一些浏览器特有的配置,后期再改

测试

启动项目,访问用户名密码登录的接口 /authentication/form ,效果如下:
image

可以看到,这里已经成功返回token了

短信验证码登录

如果之前的验证码是放到 redis 中的话,短信登录现在也是可以正常使用了的,如果是使用的 session 的方式,那还需要做一些修改,因为在App的请求中是没有cookie的,也就没有JSESSIONID, 也就获取不到session, 具体修改这里就不说了

image

本文代码传送门