Spring Security OAuth2 AngularJS |注销流程

Posted

技术标签:

【中文标题】Spring Security OAuth2 AngularJS |注销流程【英文标题】:Spring Security OAuth2 AngularJS | Logout Flow 【发布时间】:2017-06-10 13:10:34 【问题描述】:

参考oauth2spring-guides 项目中的注销流程,一旦用户第一次使用用户/密码进行身份验证,下次注销后不再询问凭据。

如何确保每次注销后都询问用户名/密码。

这就是我想要实现的:-

OAuth2 服务器使用“authorization_code”授权类型发出 JWT 令牌 自动批准。这有 html/angularjs 形式来收集 用户名/密码。

UI/Webfront - 使用 @EnableSSO。它的所有端点都经过身份验证 即它没有任何未经授权的登录页面/用户界面/链接该用户 单击以转到 /uaa 服务器。所以点击http://localhost:8080 立即将您重定向到http://localhost:9999/uaa 并呈现 用于收集用户名/密码的自定义表单。

资源服务器 - 使用 @EnableResourceServer。简单明了的 REST api。

使用上述方法,我无法锻炼注销流程。到 UI 应用程序的 HTTP POST /logout 会清除 UI 应用程序中的会话/身份验证,但用户会自动再次登录(因为我选择了所有范围的自动批准),而不会再次询问用户名密码。

查看日志和网络调用,看起来所有“oauth dance”都成功地再次发生,而无需再次要求用户输入用户名/密码,并且似乎身份验证服务器记住了为客户端发出的最后一个身份验证令牌(使用org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices?)。

我如何告诉身份验证服务器在每次请求代码/令牌时询问用户名/密码 - 无状态。

或者在我的给定场景中实现注销的最佳方法是什么。

(要重新创建接近我的要求,请从 UiApplication 中删除 permitAll() 部分,并在上述启动项目的身份验证服务器中配置 autoApproval。)

github issue

【问题讨论】:

我能想到的一个快速解决方法是在身份验证服务器上将 server.session.timeout 设置为较低的值(可能是 30 秒),并将“/me”端点移动到资源服务器。 还有其他方法可以解决这个问题,而不是减少会话超时? 【参考方案1】:

我也遇到了你描述的错误,我看到了问题的解决方案 Spring Boot OAuth2 Single Sign Off。我并不是说这是唯一的全球真相解决方案。

但在场景中,

身份验证服务器具有登录表单,您已通过该表单进行身份验证 浏览器仍保持与身份验证服务器的会话 在您完成注销过程后(撤销令牌、删除 cookie...) 并尝试重新登录 身份验证服务器不发送登录表单并自动登录

您需要从身份验证服务器的会话中删除身份验证信息,如this 答案所述。

sn-ps下面是我如何配置解决方案

客户端(在您的情况下为 UI 应用程序)应用程序的 WebSecurityConfig

...
@Value("$auth-server/ssoLogout")
private String logoutUrl;
@Autowired
private CustomLogoutHandler logoutHandler;
...
    @Override
    public void configure(HttpSecurity http) throws Exception 
        // @formatter:off
        http.antMatcher("/**")
            .authorizeRequests()
            .antMatchers("/", "/login").permitAll()
            .anyRequest().authenticated()
        .and()
            .logout()
                .logoutSuccessUrl(logoutUrl)
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .addLogoutHandler(logoutHandler)
        .and()      
            .csrf()
                .csrfTokenRepository(csrfTokenRepository())
        .and()
            .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
        // @formatter:on
    

客户端应用程序的自定义注销处理程序

@Component
public class CustomLogoutHandler implements LogoutHandler 

    private static Logger logger = Logger.getLogger(CustomLogoutHandler.class);

    @Value("$auth-server/invalidateTokens")
    private String logoutUrl;

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) 

        logger.debug("Excution CustomLogoutHandler for " + authentication.getName());
        Object details = authentication.getDetails();
        if (details.getClass().isAssignableFrom(OAuth2AuthenticationDetails.class)) 

            String accessToken = ((OAuth2AuthenticationDetails) details).getTokenValue();
            RestTemplate restTemplate = new RestTemplate();

            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
            params.add("access_token", accessToken);

            HttpHeaders headers = new HttpHeaders();
            headers.add("Authorization", "bearer " + accessToken);

            HttpEntity<Object> entity = new HttpEntity<>(params, headers);

            HttpMessageConverter<?> formHttpMessageConverter = new FormHttpMessageConverter();
            HttpMessageConverter<?> stringHttpMessageConverternew = new StringHttpMessageConverter();
            restTemplate.setMessageConverters(Arrays.asList(new HttpMessageConverter[]  formHttpMessageConverter, stringHttpMessageConverternew ));
            try 
                ResponseEntity<String> serverResponse = restTemplate.exchange(logoutUrl, HttpMethod.POST, entity, String.class);
                logger.debug("Server Response : ==> " + serverResponse);
             catch (HttpClientErrorException e) 
                logger.error("HttpClientErrorException invalidating token with SSO authorization server. response.status code:  " + e.getStatusCode() + ", server URL: " + logoutUrl);
            
        
        authentication.setAuthenticated(false);
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        new SecurityContextLogoutHandler().logout(request, response, auth);

    


我使用了JDBC tokenStore,所以我需要撤销token。在认证服务器端,我添加了一个控制器来处理注销过程

@Controller
public class AuthenticationController 

    private static Logger logger = Logger.getLogger(AuthenticationController.class);

    @Resource(name = "tokenStore")
    private TokenStore tokenStore;

    @Resource(name = "approvalStore")
    private ApprovalStore approvalStore;

    @RequestMapping(value = "/invalidateTokens", method = RequestMethod.POST)
    public @ResponseBody Map<String, String> revokeAccessToken(HttpServletRequest request, HttpServletResponse response, @RequestParam(name = "access_token") String accessToken, Authentication authentication) 
        if (authentication instanceof OAuth2Authentication) 
            logger.info("Revoking Approvals ==> " + accessToken);
            OAuth2Authentication auth = (OAuth2Authentication) authentication;
            String clientId = auth.getOAuth2Request().getClientId();
            Authentication user = auth.getUserAuthentication();
            if (user != null) 
                Collection<Approval> approvals = new ArrayList<Approval>();
                for (String scope : auth.getOAuth2Request().getScope()) 
                    approvals.add(new Approval(user.getName(), clientId, scope, new Date(), ApprovalStatus.APPROVED));
                
                approvalStore.revokeApprovals(approvals);
            
        
        logger.info("Invalidating access token :- " + accessToken);
        OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
        if (oAuth2AccessToken != null) 
            if (tokenStore instanceof JdbcTokenStore) 
                logger.info("Invalidating Refresh Token :- " + oAuth2AccessToken.getRefreshToken().getValue());
                ((JdbcTokenStore) tokenStore).removeRefreshToken(oAuth2AccessToken.getRefreshToken());
                tokenStore.removeAccessToken(oAuth2AccessToken);
            
        
        Map<String, String> ret = new HashMap<>();
        ret.put("removed_access_token", accessToken);
        return ret;
    

    @GetMapping("/ssoLogout")
    public void exit(HttpServletRequest request, HttpServletResponse response) throws IOException 
        new SecurityContextLogoutHandler().logout(request, null, null);
        // my authorization server's login form can save with remember-me cookie 
        Cookie cookie = new Cookie("my_rememberme_cookie", null);
        cookie.setMaxAge(0);
        cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
        response.addCookie(cookie);
        response.sendRedirect(request.getHeader("referer"));
    


在授权服务器的SecurityConfig中,你可能需要允许这个url为

http
    .requestMatchers()
        .antMatchers(
        "/login"
        ,"/ssoLogout"
        ,"/oauth/authorize"
        ,"/oauth/confirm_access");

我希望这对你有一点帮助。

【讨论】:

问题是当您使用 JWT 时,authserver 上没有会话。它是无国籍的。换句话说,当 authserver 发出 JWT 令牌时,使其无效的唯一方法是使用您在它们上设置的过期时间。当您的客户端应用程序具有有效令牌时,它将继续使用它,直到到期时间到来。然后资源服务器将看到令牌不再有效,并将发送客户端在 authserver 上获取新令牌。所以我唯一能想到的关于这个用例的事情就是找到一种方法来忘记客户端应用程序中的令牌,从而引发从 authserver 获取另一个令牌。 是的,正如您所描述的,我无法使 JWT 令牌无效。我使用了 JDBC token store 但我不确定这仍然使用 JWT 吗?我的意思是授权服务器登录页面的浏览器会话。注销成功后再次尝试登录时,即使我撤销令牌并撤销批准,浏览器也不会发出Auth服务器的登录表单页面。 @JuanCarlosMendoza 对于 authserver,我们无法使用 sessionCreationPolicy(SessionCreationPolicy.STATELESS) 创建会话管理。 我的意思是如果你的 authserver 正如 Kumar Sambhav 所指出的那样使用 JWT。【参考方案2】:

当您使用 JWT 令牌时,您无法真正撤销它们。 作为一种解决方法,您可以拥有一个注销休息端点,该端点将存储用于注销调用的时间戳和用户 ID。

稍后,您可以将注销时间与 JWT 令牌颁发时间进行比较,并决定是否允许 api 调用。

【讨论】:

【参考方案3】:

我已经意识到,当您从客户端应用程序注销然后以编程方式在您的身份验证服务器上注销时重定向到控制器可以解决问题。这是我在客户端应用程序上的配置:

@Configuration
@EnableOAuth2Sso
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter 

    @Value("$auth-server/exit")
    private String logoutUrl;

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http
            .logout()
            .logoutSuccessUrl(logoutUrl)
            .and().authorizeRequests().anyRequest().authenticated();
    

这是我在 authserver 上的配置(只是一个处理 /exit 端点的控制器):

@Controller
public class LogoutController 
    public LogoutController() 
    

    @RequestMapping("/exit")
    public void exit(HttpServletRequest request, HttpServletResponse response) 
        (new SecurityContextLogoutHandler()).logout(request, null, null);

        try 
            response.sendRedirect(request.getHeader("referer"));
         catch (IOException e) 
            e.printStackTrace();
        

    

Here 是一个示例应用,展示了使用 JWT 的完整实现。检查一下,如果它对您有帮助,请告诉我们。

【讨论】:

对于 JWT 案例 (和 OP 的问题),我认为您的回答已经足够好了。在我的基于 JWT 令牌的项目的 oauth2 中,我可以注销删除用户的身份验证,但是当我尝试再次登录时,身份验证服务器没有发出登录表单。现在你的答案修复了这个解决方案。现在我正在使用 JDBC tokenStore 并将其配置为我的答案。谢谢你。赏金是你的。

以上是关于Spring Security OAuth2 AngularJS |注销流程的主要内容,如果未能解决你的问题,请参考以下文章

Spring-Security OAuth2 设置 - 无法找到 oauth2 命名空间处理程序

Spring Security OAuth2 v5:NoSuchBeanDefinitionException:'org.springframework.security.oauth2.jwt.Jwt

针对授权标头的Spring Security OAuth2 CORS问题

OAuth2.0学习(4-1)Spring Security OAuth2.0 - 代码分析

Spring Security---Oauth2详解

如何使用spring-security,oauth2调整实体的正确登录?