Spring Security OAuth2 AngularJS |注销流程

时间:2017-01-25 11:15:06

标签: spring-security spring-security-oauth2

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

如何确保每次退出后都会询问用户名/密码。

这是我想要实现的: -

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

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

  • 资源服务器 - 使用@EnableResourceServer。普通&简单的REST api。

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

查看日志和网络电话,看起来所有" oauth跳舞"在没有再次要求用户输入用户名/密码的情况下再次成功发生并且似乎auth服务器记住为客户端发出的最后一个auth令牌(使用org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices?)。

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

或者在给定方案中实施注销的最佳方法是什么。

(要稍微重新创建我的要求,请从UiApplication中删除$(document).ready(function() { $("label").click(function(){ $(this).css("background-color: black;"); }); }); 部分,并在上述启动项目的auth服务器中配置permitAll()。)

github issue

3 个答案:

答案 0 :(得分:2)

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

但在场景中,

  • 身份验证服务器具有登录表单并且您已通过身份验证
  • 浏览器仍然使用身份验证服务器维护会话
  • 完成注销过程后(撤销令牌,删除Cookie ...) 并尝试重新登录
  • 身份验证服务器不发送登录表单并自动登录

您需要从身份验证服务器的会话中删除身份验证信息,并将其描述为this

以下摘录是我如何配置解决方案

客户端(在您的案例中为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,所以我需要撤销令牌。在身份验证服务器端,我添加了一个控制器来处理注销过程

@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中,您可能需要允许此网址为

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

我希望这对你有所帮助。

答案 1 :(得分:2)

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

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

答案 2 :(得分:1)

我已经意识到当您从客户端应用程序注销然后以编程方式注销您的authserver时重定向到控制器就可以了。这是我在客户端应用程序上的配置:

@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的完整实现。检查一下,告诉我们它是否对您有所帮助。