前言
Spring Cloud 整合 Spring Security Oauth2 请看我上一篇文章
在当今的数字化时代,随着微服务架构的流行和前后端分离技术的广泛应用,如何实现安全、高效的用户认证与授权成为了开发者们面临的重要挑战。Spring Cloud与Spring Security OAuth2作为Java领域中的佼佼者,为我们提供了强大的解决方案。本文将详细探讨Spring Cloud与Spring Security OAuth2的整合过程,以及如何在前后端分离的架构中实现用户认证与授权,旨在帮助读者更好地理解并掌握这一关键技术。
Spring Cloud作为一套微服务解决方案,提供了丰富的功能组件,用于构建分布式系统。其中,安全认证与授权是微服务架构中不可或缺的一环。Spring Security OAuth2则是Spring Security的一个扩展模块,它支持OAuth 2.0协议,实现了客户端与服务器之间的安全认证与授权。通过整合Spring Cloud与Spring Security OAuth2,我们可以轻松构建出安全、可靠的分布式系统。
在前后端分离的架构中,前端负责与用户进行交互,展示数据和接收用户输入;后端则负责处理业务逻辑和数据存储。这种架构使得前后端可以独立开发、部署和扩展,提高了系统的灵活性和可维护性。然而,这也带来了新的问题:如何在前后端之间实现安全的用户认证与授权?
通过整合Spring Cloud与Spring Security OAuth2,我们可以实现以下目标:
- 提供一个统一的认证授权中心,用于管理用户的身份信息和权限;
- 实现前后端之间的安全通信,确保数据的完整性和机密性;
- 简化用户认证与授权的流程,提高用户体验;
- 支持多种认证方式,如用户名密码、社交登录等;
- 提供细粒度的权限控制,满足不同业务场景的需求。
本文将详细介绍如何整合Spring Cloud与Spring Security OAuth2,并在前后端分离的架构中实现用户认证与授权。我们将从环境搭建、配置认证授权中心、实现前后端通信等方面入手,逐步引导读者完成整个整合过程。希望通过本文的介绍,读者能够掌握这一关键技术,并在实际项目中加以应用。
步骤
下面我们按照第三方授权登录流程讲解
页面改造
- 登录页面改造
我们这里采用了两次登录方式,先登录我们自己的系统,成功后再次请求oauth2的登录请求。
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid && !store.state.fullscreenLoading) {
store.commit('fullscreenLoading', true)
const res = <resData>await oauth2Token({}, data)
if (res.ok) {
setSession('user_info', data)
setSession('access_token', res.data.token_type.value.concat(" ").concat(res.data.access_token))
setSession('refresh_token', res.data.refresh_token)
// 如果路由携带回调地址参数则认为是客户端授权,需要请求oauth2的登录请求,登录成功后重定向到路由上的回调地址
if (route.query.redirect_url) {
data.redirect_url = route.query.redirect_url
const res = <resData>await login(data)
if (res.ok) {
window.location.href = decodeURI(route.query.redirect_url)
}
} else {
await router.replace('tenant').catch()
}
}
}
})
}
- 创建授权页面
授权页面需要显示授权的客户和用户的基本细腻,所以需要提供一个获取客户信息和用户信息的接口。
效果如下
请求授权地址
http://127.0.0.1:8011/single-auth/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://www.baidu.com
- 如果未登录、未授权的情况下会被
AuthenticationEntryPoint
拦截,我们需要在此监听中判断是否是授权地址请求,如果是则重定向到登录页面并携带授权地址。
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final String OAUTH2_AUTHORIZE = "/oauth2/authorize";
@Value("${oauth2.login-uri}")
private String loginUri;
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, AuthenticationException e) {
// 设置响应内容类型为 JSON,编码为 UTF-8
httpServletResponse.setContentType(Constant.APPLICATION_JSON_UTF8_VALUE);
// 设置响应状态码为 401 Unauthorized
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try (PrintWriter out = httpServletResponse.getWriter()) {
// 判断请求的 URI 是否为 "/oauth2/authorize"
if (OAUTH2_AUTHORIZE.equals(httpServletRequest.getRequestURI())) {
// 如果是,则重定向到用户登录页面
String requestURL = "http://127.0.0.1:8011/single-auth/oauth2/authorize" +
SymbolConstant.QUESTION_MARK +
httpServletRequest.getQueryString();
httpServletResponse.sendRedirect(loginUri.concat("?redirect_url=").concat(URLEncoder.encode(requestURL, Constant.UTF8)));
} else {
// 如果不是,则将错误信息转换为 JSON 字符串并写入响应输出流
out.write(JSONUtil.toJsonStr(RespJson.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, I18nUtils.getMessage(e.getMessage()))));
}
} catch (IOException ex) {
throw new MyException(ErrorCode.ERROR);
}
}
}
- 如果已登录、未授权则会重定向到我们自定义的授权页面。
这里我们需要自定义授权页面地址,自定义授权成功回调,自定义授权失败回调。
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 定义授权服务配置器
OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
configurer.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.accessTokenRequestConverter(new OAuth2PasswordAuthenticationConverter())
.accessTokenRequestConverter(new OAuth2EmailAuthenticationConverter(userInfoFeignService))
.accessTokenResponseHandler(new MyAuthenticationSuccessHandler(userService))
.errorResponseHandler(new MyAuthenticationFailureHandler()))
// 自定义授权
.authorizationEndpoint(authorizationEndpoint -> {
// 自定义授权地址
authorizationEndpoint.consentPage(consent);
// 自定义授权成功监听
authorizationEndpoint.authorizationResponseHandler(new MyAuthenticationSuccessHandler(userService));
// 自定义授权失败监听
authorizationEndpoint.errorResponseHandler(new MyAuthenticationFailureHandler());
})
// Enable OpenID Connect 1.0, 启用 OIDC 1.0
.oidc(Customizer.withDefaults());
// 获取授权服务器相关的请求端点
RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();
http
// 拦截对授权服务器相关端点的请求
.requestMatcher(endpointsMatcher)
// 拦载到的请求需要认证
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())
// 忽略掉相关端点的 CSRF(跨站请求): 对授权端点的访问可以是跨站的
.csrf().disable()
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// 应用授权服务器的配置
.apply(configurer);
DefaultSecurityFilterChain securityFilterChain = http.build();
// 注入自定义授权模式实现
http.authenticationProvider(
new OAuth2PasswordAuthenticationProvider(
http.getSharedObject(OAuth2AuthorizationService.class),
http.getSharedObject(JwtGenerator.class),
new OAuth2RefreshTokenGenerator(),
http.getSharedObject(AuthenticationManager.class)
));
return securityFilterChain;
}
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final IUserService userService;
public MyAuthenticationSuccessHandler(IUserService userService) {
this.userService = userService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) {
if (AuthorizationGrantType.PASSWORD.getValue().equals(httpServletRequest.getParameter(Constant.GRANT_TYPE))) {
// 登录日志
userService.saveLoginLog(httpServletRequest);
}
// 设置响应内容类型为 JSON,编码为 UTF-8
httpServletResponse.setContentType(Constant.APPLICATION_JSON_UTF8_VALUE);
// 设置响应状态码为 200 OK
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
// 创建一个 HashMap 用于存储认证信息
HashMap<String, Object> map = new HashMap<>();
try (PrintWriter out = httpServletResponse.getWriter()) {
if (authentication instanceof OAuth2AuthorizationCodeRequestAuthenticationToken) {
//使用客户端授权登录
OAuth2AuthorizationCodeRequestAuthenticationToken auth2AuthorizationCodeRequestAuthenticationToken = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
// 前端授权请求标识(为了解决跨域问题)
String accredit = httpServletRequest.getParameter("accredit");
// 回调地址
String url = auth2AuthorizationCodeRequestAuthenticationToken.getRedirectUri() + "?code=" + auth2AuthorizationCodeRequestAuthenticationToken.getAuthorizationCode().getTokenValue();
// 如果是前端请求授权则返回成功信息,移交给前端跳转到授权成功页面,否则直接重定向到成功页面
if (accredit != null) {
out.write(JSONUtil.toJsonStr(RespJson.success(url)));
} else {
httpServletResponse.sendRedirect(auth2AuthorizationCodeRequestAuthenticationToken.getRedirectUri() + "?code=" + auth2AuthorizationCodeRequestAuthenticationToken.getAuthorizationCode().getTokenValue());
}
}
} catch (IOException e) {
// 捕获 IO 异常,并抛出自定义异常
throw new MyException(ErrorCode.ERROR);
}
}
}
- 如果已登录、已授权则会重定向到客户自己配置的重定向地址并且带code。
总结
完成上诉步骤我们就可以进行第三方授权登录了。复盘一下
- 自定义登录页面
- 自定义授权页面
- 请求授权地址
AuthenticationEntryPoint
监听重定向改造 - 登录成功监听改造(返回成功信息)
- 授权成功监听改造(如果是前端请求则返回成功信息,由前端重定向到成功回调地址)
- 前端登录改造(先请求我们自己的登录接口,成功后再请求oauth2提供的登录接口)
