述
上文中,了解了 Spring Security OAuth 的一个大致的流程,本文将我们之前写的用户名密码登录的方式重新修改一下,让他登录之后也返回一个token, 后续请求都通过token来请求
实现思路
回顾一下之前的流程图:
我们在用户名密码等登陆认证完成之后,会生成一个已认证的 Authentication
对象, 看一下图中的 Authentication
这里,也就是说我们再搞到一个 OAuth2Request
对象,就可以用他后面的逻辑去处理生成了,大致流程图如下
这里就是在登录成功之后,在登录成功的处理器里面,从请求中获取到basic client的信息,然后去获取 ClientDetails
的信息, 虚线框住的部分是需要我们自己去实现的
具体实现
我们最终是希望拿到一个 OAuth2Request
对象, 如图,得先通过 ClientDetailsService
获取 ClientDetails
,所以第一步得先从请求中把 ClientId
拿出来
之前我们的请求头中有 Authorization:Basic dGVzdEFwcGlkOnRlc3RTZWNyZXQ=
我们只要把这个拿出来做解析就可以拿到 clientId
了
BasicAuthenticationFilter
中有一段解析的代码, 如下:
具体的解码方法代码如下:
把这两段代码直接复制到我们的成功处理器里面,然后做一些修改即可,如下: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
,效果如下:
可以看到,这里已经成功返回token了
短信验证码登录
如果之前的验证码是放到 redis 中的话,短信登录现在也是可以正常使用了的,如果是使用的 session 的方式,那还需要做一些修改,因为在App的请求中是没有cookie的,也就没有JSESSIONID, 也就获取不到session, 具体修改这里就不说了