Spring security OAuth2 - 身份验证后使会话无效
Spring security OAuth2 - invalidate session after authentication
我们正在使用 spring 安全 OAuth2 保护 REST 服务。应用程序可以调用 /oauth/authorize
、/oauth/token
或 /rest-api
端点。 token 和 rest-api 端点是无状态的,不需要会话。
我们可以在用户通过身份验证后使会话无效吗?如果是这样,最好的方法是什么。我们希望用户在调用 /oauth/authorize
时始终登录。目前,只要存在会话,对 /oauth/authorize
的调用就会跳过身份验证。
据我了解,您正在尝试在执行某些操作后以编程方式注销。也许您应该查看 SecurityContextLogoutHandler 并了解它是如何工作的。那里有一种注销方法。我认为将其称为建议将解决您的问题。
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
SecurityContextHolder.clearContext();
}
首先:在您的配置中为 oauth
声明带有令牌存储的 bean
@Bean
@Primary
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
对于控制器方法,我们做了以下 class
@Controller
public class TokenController {
@RequestMapping(value = "/oauth/token/revoke", method = RequestMethod.POST)
public @ResponseBody void create(@RequestParam("token") String value) {
this.revokeToken(value);
}
@Autowired
TokenStore tokenStore;
public boolean revokeToken(String tokenValue) {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
if (accessToken == null) {
return false;
}
if (accessToken.getRefreshToken() != null) {
tokenStore.removeRefreshToken(accessToken.getRefreshToken());
}
tokenStore.removeAccessToken(accessToken);
return true;
}
}
如果您不想使用这种方法,您可以获取当前用户的令牌自动装配 Principal
:
OAuth2Authentication authorization = (OAuth2Authentication) principal;
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authorization.getDetails();
String token = details.getTokenValue();
甚至自动装配 OAuth2Authentication
:
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
String token = details.getTokenValue();
理解问题有点老了,希望下面的内容对寻找问题正确答案的人有所帮助
OP 询问的不是令牌失效,而是如何在用户身份验证成功通过并且有效的 access_token 或 authorization_code 之后立即使 Spring OAuth2 服务器上的 httpSession 失效access_token) 返回给客户。
这个用例仍然没有开箱即用的解决方案。但是可以找到 spring-security-oauth 最活跃的贡献者 Dave Syer 的解决方法 here on GitHub
只需从那里复制代码:
@Service
@Aspect
public class SessionInvalidationOauth2GrantAspect {
private static final String FORWARD_OAUTH_CONFIRM_ACCESS = "forward:/oauth/confirm_access";
private static final Logger logger = Logger.getLogger(SessionInvalidationOauth2GrantAspect.class);
@AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && @annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
public void authorizationAdvice(JoinPoint joinpoint, ModelAndView result) throws Throwable {
// If we're not going to the confirm_access page, it means approval has been skipped due to existing access
// token or something else and they'll be being sent back to app. Time to end session.
if (!FORWARD_OAUTH_CONFIRM_ACCESS.equals(result.getViewName())) {
invalidateSession();
}
}
@AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && @annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
public void authorizationAdvice(JoinPoint joinpoint, View result) throws Throwable {
// Anything returning a view and not a ModelView is going to be redirecting outside of the app (I think).
// This happens after the authorize approve / deny page with the POST to /oauth/authorize. This is the time
// to kill the session since they'll be being sent back to the requesting app.
invalidateSession();
}
@AfterThrowing(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && @annotation(org.springframework.web.bind.annotation.RequestMapping)", throwing = "error")
public void authorizationErrorAdvice(JoinPoint joinpoint) throws Throwable {
invalidateSession();
}
private void invalidateSession() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
HttpSession session = request.getSession(false);
if (session != null) {
logger.warn(String.format("As part of OAuth application grant processing, invalidating session for request %s", request.getRequestURI()));
session.invalidate();
SecurityContextHolder.clearContext();
}
}
}
添加pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
另一个解决方案 可能是将会话超时设置为某个非常小的值。实现这一点的最简单方法是将以下内容添加到 application.yml 配置中:
server:
session:
timeout: 1
但这不是理想的解决方案,因为供应商的最小值可能是 1(为无限会话保留零)并且它以分钟而不是秒为单位
我可以提供这样的选择(根据@de_xtr推荐):
import static org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes;
@Slf4j
@Component
@Aspect
public class InvalidateSessionAspect {
private final LogoutHandler logoutHandler;
public InvalidateSessionAspect() {
logoutHandler = new SecurityContextLogoutHandler();
}
@Pointcut("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
public void postAccessTokenPointcut() {
}
@AfterReturning(value = "postAccessTokenPointcut()", returning = "entity")
public void invalidateSession(JoinPoint jp, Object entity) {
log.debug("[d] Trying to invalidate the session...");
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) currentRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
logoutHandler.logout(request, null, null);
log.debug("[d] Session has been invalidated");
}
}
以及没有任何方面的选项:
@Slf4j
class LogoutHandlerInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object h, ModelAndView view) {
HttpSession session = req.getSession(false);
if (session != null) {
log.debug("[d] Trying to invalidate the session...");
session.invalidate();
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
SecurityContextHolder.clearContext();
log.debug("[d] Session has been invalidated");
}
}
}
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
//...
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.addInterceptor(new LogoutHandlerInterceptor())
// ...
;
}
}
我们正在使用 spring 安全 OAuth2 保护 REST 服务。应用程序可以调用 /oauth/authorize
、/oauth/token
或 /rest-api
端点。 token 和 rest-api 端点是无状态的,不需要会话。
我们可以在用户通过身份验证后使会话无效吗?如果是这样,最好的方法是什么。我们希望用户在调用 /oauth/authorize
时始终登录。目前,只要存在会话,对 /oauth/authorize
的调用就会跳过身份验证。
据我了解,您正在尝试在执行某些操作后以编程方式注销。也许您应该查看 SecurityContextLogoutHandler 并了解它是如何工作的。那里有一种注销方法。我认为将其称为建议将解决您的问题。
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
SecurityContextHolder.clearContext();
}
首先:在您的配置中为 oauth
声明带有令牌存储的 bean@Bean
@Primary
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
对于控制器方法,我们做了以下 class
@Controller
public class TokenController {
@RequestMapping(value = "/oauth/token/revoke", method = RequestMethod.POST)
public @ResponseBody void create(@RequestParam("token") String value) {
this.revokeToken(value);
}
@Autowired
TokenStore tokenStore;
public boolean revokeToken(String tokenValue) {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
if (accessToken == null) {
return false;
}
if (accessToken.getRefreshToken() != null) {
tokenStore.removeRefreshToken(accessToken.getRefreshToken());
}
tokenStore.removeAccessToken(accessToken);
return true;
}
}
如果您不想使用这种方法,您可以获取当前用户的令牌自动装配 Principal
:
OAuth2Authentication authorization = (OAuth2Authentication) principal;
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authorization.getDetails();
String token = details.getTokenValue();
甚至自动装配 OAuth2Authentication
:
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
String token = details.getTokenValue();
理解问题有点老了,希望下面的内容对寻找问题正确答案的人有所帮助
OP 询问的不是令牌失效,而是如何在用户身份验证成功通过并且有效的 access_token 或 authorization_code 之后立即使 Spring OAuth2 服务器上的 httpSession 失效access_token) 返回给客户。
这个用例仍然没有开箱即用的解决方案。但是可以找到 spring-security-oauth 最活跃的贡献者 Dave Syer 的解决方法 here on GitHub
只需从那里复制代码:
@Service
@Aspect
public class SessionInvalidationOauth2GrantAspect {
private static final String FORWARD_OAUTH_CONFIRM_ACCESS = "forward:/oauth/confirm_access";
private static final Logger logger = Logger.getLogger(SessionInvalidationOauth2GrantAspect.class);
@AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && @annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
public void authorizationAdvice(JoinPoint joinpoint, ModelAndView result) throws Throwable {
// If we're not going to the confirm_access page, it means approval has been skipped due to existing access
// token or something else and they'll be being sent back to app. Time to end session.
if (!FORWARD_OAUTH_CONFIRM_ACCESS.equals(result.getViewName())) {
invalidateSession();
}
}
@AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && @annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
public void authorizationAdvice(JoinPoint joinpoint, View result) throws Throwable {
// Anything returning a view and not a ModelView is going to be redirecting outside of the app (I think).
// This happens after the authorize approve / deny page with the POST to /oauth/authorize. This is the time
// to kill the session since they'll be being sent back to the requesting app.
invalidateSession();
}
@AfterThrowing(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && @annotation(org.springframework.web.bind.annotation.RequestMapping)", throwing = "error")
public void authorizationErrorAdvice(JoinPoint joinpoint) throws Throwable {
invalidateSession();
}
private void invalidateSession() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
HttpSession session = request.getSession(false);
if (session != null) {
logger.warn(String.format("As part of OAuth application grant processing, invalidating session for request %s", request.getRequestURI()));
session.invalidate();
SecurityContextHolder.clearContext();
}
}
}
添加pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
另一个解决方案 可能是将会话超时设置为某个非常小的值。实现这一点的最简单方法是将以下内容添加到 application.yml 配置中:
server:
session:
timeout: 1
但这不是理想的解决方案,因为供应商的最小值可能是 1(为无限会话保留零)并且它以分钟而不是秒为单位
我可以提供这样的选择(根据@de_xtr推荐):
import static org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes;
@Slf4j
@Component
@Aspect
public class InvalidateSessionAspect {
private final LogoutHandler logoutHandler;
public InvalidateSessionAspect() {
logoutHandler = new SecurityContextLogoutHandler();
}
@Pointcut("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
public void postAccessTokenPointcut() {
}
@AfterReturning(value = "postAccessTokenPointcut()", returning = "entity")
public void invalidateSession(JoinPoint jp, Object entity) {
log.debug("[d] Trying to invalidate the session...");
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) currentRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
logoutHandler.logout(request, null, null);
log.debug("[d] Session has been invalidated");
}
}
以及没有任何方面的选项:
@Slf4j
class LogoutHandlerInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object h, ModelAndView view) {
HttpSession session = req.getSession(false);
if (session != null) {
log.debug("[d] Trying to invalidate the session...");
session.invalidate();
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
SecurityContextHolder.clearContext();
log.debug("[d] Session has been invalidated");
}
}
}
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
//...
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.addInterceptor(new LogoutHandlerInterceptor())
// ...
;
}
}