Spring Security Oauth2系列(四)

前言:

已经很久没有更新相关的文章资料了,现在分享目前的心得吧,从前面的相关系列文章相信大家对于Spring Security Oauth2系列的登录应该没有多大问题了吧。在这里我用简单的话在说一次大楷流程吧,首先从客户端开始默认/login作为sso单点登录路径。在客户端的security设置了认证权限的地方也会跳转到单点登录,它会去认证授权服务中心获取token,如果不行就跳转到/oauth/authorize去授权,当然前提是必须要认证携带Principal参数,这里不做过多的说明,由于我配置的是redis存储token所以后面流程会进入RedisTokenStore这个类,进行token的存储相关功能,然后经过TokenEnhancer(这个地方我采用自定义的增强器,因为我采用的是默认的源代码存储token,所以在后期注销sso的时候这个地方我会做一点儿事情..........)。

问题:

I'm considering to use OAuth2 for my application. The architecture I'm trying to implement is as follows:

I will have my own (and only this) Authorization Server
Some Resource Apps validating access to their resources using the Authorization Server
Some client apps (web, mobile) which will redirect the user to the Authorization Server for authentication and on success will consume the api's on the Resource Apps.
So far I have managed to implement this interaction between 3 basic apps (1 auth server, 1 resource server and 1 client). The thing I don't get working is the logout functionality. I have read of the "notoriously tricky problem" that Dave Syer describes in his tutorial, but in this case I really need the user to re-login after loging out. I have tried giving few seconds to the access token and the refresh token, but instead of being prompted to login again when the expiration arrives, I'm getting a NPE on the client app. I have also tried the solutions proposed in this post to remove the token from the token store, but it doesn't work. The single sign off is for me the desirable behaviour for this implementation. How can I achieve this using Spring Boot Oauth2. If it is not possible for some reason, which alternatives I could use to implement a centralized security using Spring Boot?

Thanks in advance.

解决办法:
一. 通过SecurityContextLogoutHandler登出

In the client app (WebSecurityConfigurerAdapter):

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .logout()
            .logoutSuccessUrl("http://your-auth-server/exit");
}

In the authorization server:

@Controller
public class LogoutController {

    @RequestMapping("oauth/exit")
    public void exit(HttpServletRequest request, HttpServletResponse response) {
        // token can be revoked here if needed
        new SecurityContextLogoutHandler().logout(request, null, null);
        try {
            //sending back to client app
            response.sendRedirect(request.getHeader("referer"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
二.通过tokenServices进行退出

In the authorization server:

@Autowired
ConsumerTokenServices tokenServices;

@GetMapping("/tokens/revoke/{tokenId:.*}")
@ResponseBody
public String revokeToken(@PathVariable String tokenId) {
    tokenServices.revokeToken(tokenId);
    return tokenId;
}

@FrameworkEndpoint
public class RevokeTokenEndpoint {

    @Autowired
    @Qualifier("consumerTokenServices")
    ConsumerTokenServices consumerTokenServices;

    @DeleteMapping("/oauth/token")
    @ResponseBody
    public String revokeToken(String access_token) {
        if (consumerTokenServices.revokeToken(access_token)){
            return "注销成功";
        }else{
            return "注销失败";
        }
    }
}

问题心得:

以上方法经过我的代码测试,由于我是微服务,可能第一种情况不太熟悉,按照第一种方式不能正常成功,当客户端第二次登录的时候会报401错误。然后第二种方式,相信大家很容易看出来关键的参数是access_token,通过我获取Principal参数以及相关request发现了token,但是我没有办法拿出来,听说可以实现filter去获取,具体怎么实现有待大家努力完成。前言部分就是为了解决获取token问题的前奏,实现的最简单的方式就是自定义token存储(默认采用源码的redis形式存储,然后自己在增强器存一份在数据库而已),这样很繁琐而已代码量大,所以我没有采用,而是采用了在token增强器的地方进行了相关的处理:

public class CustomTokenEnhancer implements TokenEnhancer {

    private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();

    private static String getApprovalKey(OAuth2Authentication authentication) {
        String userName = authentication.getUserAuthentication() == null ? "" : authentication.getUserAuthentication().getName();
        return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
    }

    private static String getApprovalKey(String clientId, String userName) {
        return clientId + (userName == null ? "" : ":" + userName);
    }

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        String principal = "";
        if (authentication.getPrincipal() instanceof UserDetails) {
            principal = ((UserDetails) authentication.getPrincipal()).getUsername();
        }
        if (accessToken instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken token = ((DefaultOAuth2AccessToken) accessToken);
            token.setValue(getNewToken());

            OAuth2RefreshToken refreshToken = token.getRefreshToken();
            if (refreshToken instanceof DefaultOAuth2RefreshToken) {
                token.setRefreshToken(new DefaultOAuth2RefreshToken(getNewToken()));
            }

            Map<String, Object> additionalInformation = new HashMap<>();
            additionalInformation.put("client_id", authentication.getOAuth2Request().getClientId());
            token.setAdditionalInformation(additionalInformation);
            TokenTemplate tokenTemplate = new TokenTemplate();
            tokenTemplate.setPrincipal(principal);
            tokenTemplate.setAccess(token.getValue());
            tokenTemplate.setAuth_to_access(this.authenticationKeyGenerator.extractKey(authentication));
            tokenTemplate.setUname_to_access(getApprovalKey(authentication));
            tokenTemplate.setClient_id_to_access(authentication.getOAuth2Request().getClientId());
            tokenTemplate.setTokenType(token.getTokenType());
            tokenTemplate.setExpiration(token.getExpiration());
            //进行token的相关数据存储
            JdbcOperateUtils.update(tokenTemplate);
            return token;
        }
        return accessToken;
    }

    private String getNewToken() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

JdbcOperateUtils.update(tokenTemplate);这个方法是最重要的,他就是负责token存储的
,首先要有一个token的模板数据类tokenTemplate

@Entity
@Table(name = "tokentemplate")
public class TokenTemplate {
    @Id
    private String principal;
    private String tokenType;
    private Date expiration;
    private String access;
    private String auth_to_access;
    private String uname_to_access;
    private String client_id_to_access;

    public String getPrincipal() {
        return principal;
    }

    public void setPrincipal(String principal) {
        this.principal = principal;
    }

    public String getTokenType() {
        return tokenType;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public Date getExpiration() {
        return expiration;
    }

    public void setExpiration(Date expiration) {
        this.expiration = expiration;
    }

    public String getAccess() {
        return access;
    }

    public void setAccess(String access) {
        this.access = access;
    }

    public String getAuth_to_access() {
        return auth_to_access;
    }

    public void setAuth_to_access(String auth_to_access) {
        this.auth_to_access = auth_to_access;
    }

    public String getUname_to_access() {
        return uname_to_access;
    }

    public void setUname_to_access(String uname_to_access) {
        this.uname_to_access = uname_to_access;
    }

    public String getClient_id_to_access() {
        return client_id_to_access;
    }

    public void setClient_id_to_access(String client_id_to_access) {
        this.client_id_to_access = client_id_to_access;
    }
        }

然后自定义存储token的jdbc操作,这个地方为什么用jdbc呢?因为没有办法采用注解进去,在AuthorizationServerConfiguration这个授权服务中配置

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .userDetailsService(customUserDetailsService)
                .userApprovalHandler(userApprovalHandler)
                                //这个地方采用注解不能生效
                .tokenEnhancer(new CustomTokenEnhancer())
                .tokenStore(tokenStore);
        //endpoints.pathMapping(String "/oauth/confirm_access","/oauth/my_approval");
                }

由于tokenEnhancer(new CustomTokenEnhancer())这个地方采用注解不能生效,所以我采用了原生态的jdbc方式存储token

    /**
     * 查询token是否存在
     * @param principal
     * @return
     */
    public static String query(String principal) {
        Connection connection = ConnectionUtils.getConn();
        String sql = "SELECT access FROM tokentemplate WHERE principal = ?";
        PreparedStatement preparedStatement = null;
        String access = "gzw";
        try {
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, principal);
            ResultSet resultSet = preparedStatement.executeQuery();
            if (resultSet.next()){
                access = resultSet.getString(1);
            }
            return access;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            ConnectionUtils.releaseConnection(connection);
        }
        return access;
    }

    /**
     * 持久化token到数据库
     *
     * @param tokenTemplate
     */
    public static void update(TokenTemplate tokenTemplate) {
        Connection connection = ConnectionUtils.getConn();
        String sql1 = "INSERT INTO tokentemplate VALUES (?,?,?,?,?,?,?)";
        String sql2 = "UPDATE tokentemplate SET tokenType=?,expiration=?,access=?,auth_to_access=?,uname_to_access=?,client_id_to_access=? WHERE principal=? ";
        PreparedStatement preparedStatement = null;
        if (query(tokenTemplate.getPrincipal()).equals("gzw")){
            try {
                preparedStatement = connection.prepareStatement(sql1);
                preparedStatement.setString(1, tokenTemplate.getPrincipal());
                preparedStatement.setString(2, tokenTemplate.getTokenType());
                preparedStatement.setString(3, dateToString(tokenTemplate.getExpiration()));
                preparedStatement.setString(4, tokenTemplate.getAccess());
                preparedStatement.setString(5, tokenTemplate.getAuth_to_access());
                preparedStatement.setString(6, tokenTemplate.getUname_to_access());
                preparedStatement.setString(7, tokenTemplate.getClient_id_to_access());
                preparedStatement.executeUpdate();
            } catch (SQLException e) {
                e.printStackTrace();
            }finally {
                ConnectionUtils.releaseConnection(connection);
            }
        }else {
            try {
                preparedStatement = connection.prepareStatement(sql2);
                preparedStatement.setString(7, tokenTemplate.getPrincipal());
                preparedStatement.setString(1, tokenTemplate.getTokenType());
                preparedStatement.setString(2, dateToString(tokenTemplate.getExpiration()));
                preparedStatement.setString(3, tokenTemplate.getAccess());
                preparedStatement.setString(4, tokenTemplate.getAuth_to_access());
                preparedStatement.setString(5, tokenTemplate.getUname_to_access());
                preparedStatement.setString(6, tokenTemplate.getClient_id_to_access());
                preparedStatement.executeUpdate();
            } catch (SQLException e) {
                e.printStackTrace();
            }finally {
                ConnectionUtils.releaseConnection(connection);
            }

        }

    }

    public static String dateToString(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat(" yyyy-MM-dd HH:mm:ss ");
        return sdf.format(date);
                }

然后按照以上的第二种方式进行注销

@FrameworkEndpoint
public class RevokeTokenEndpoint {

    @Autowired
    @Qualifier("consumerTokenServices")
    ConsumerTokenServices consumerTokenServices;

    private static final Logger logger = LoggerFactory.getLogger(RevokeTokenEndpoint.class);

    @DeleteMapping("/oauth/exit")
    @ResponseBody
    public JSONObject revokeToken(String principal) {
        String access_token = JdbcOperateUtils.query(principal);
        if (!access_token.equals("gzw")) {
            if (consumerTokenServices.revokeToken(access_token)) {
                logger.info("oauth2 logout success with principal: "+ principal);
                return ResultUtil.toJSONString(ResultEnum.SUCCESS,principal);
            }
        }else {
            logger.info("oauth2 logout fail with principal: "+ principal);
            return ResultUtil.toJSONString(ResultEnum.FAIL,principal);
        }
        return ResultUtil.toJSONString(ResultEnum.UNKONW_ERROR,principal);
    }
        }

这个地方得注意一下,认证授权服务上注销了过后你必须在客户端进行security的/logout进行相应的注销处理,否则会出现问题。

总结:

至此注销sso的流程处理就结束了,相关代码有空我会上传github上供大家一起参考学习,希望各位给我提出错误的观点,一起学习,一起进步!

参考资料链接:

https://github.com/juanzero000/spring-boot-oauth2-sso
https://stackoverflow.com/questions/43071370/spring-boot-oauth2-single-sign-off-logout
http://www.baeldung.com/spring-security-oauth-revoke-tokens
http://www.shangyang.me/2017/06/01/spring-cloud-oauth2-zuul-potholes/

最新代码会尽快更新同步到github:https://github.com/dqqzj/spring4all/tree/master/oauth2


此文转载地址  http://www.spring4all.com/article/956

猜你喜欢

转载自blog.csdn.net/sinat_24798023/article/details/80537082