Spring Security Oauth2系列(五)

时间:2022-12-19 21:05:50

摘要:

本文主要讲一下在企业公司内部利用oauth2的记住我这个问题,本来该系列文章不打算在更新了,可是公司内部需要记住我这个功能的实现,只好花了2天时间在研读了源码,特地分享给大家。

记住我功能实现

请大家参考社区 Spring Security 从入门到进阶系列教程Spring Security源码分析七:Spring Security 记住我

问题:

首先在开发中大家用得比较多的是ajax传输数据,最近几年的Thymeleaf技术可能不那么普遍,在记住我这个功能实现的过程中会进行授权页的跳转流程,由于在前面的文章中我用的是前台生成form表单进行自动跳转,具体代码如下:

@RequestMapping({ "/oauth/my_approval"})
    @ResponseBody
    public JSONObject getAccessConfirmation(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        @SuppressWarnings("unchecked")
        Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes"));
        List<String> scopeList = new ArrayList<>();
        for (String scope : scopes.keySet()) {
            scopeList.add(scope);
        }
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("scopeList",scopeList);
        return jsonObject;
    }
function auth(){
        //定义一个form表单
        var form=$("&lt;form&gt;");
        $(document.body).append(form);
        form.attr("method","post");
        form.attr("action","../oauth/authorize");
        var inputUser=$("&lt;input&gt;");
        inputUser.attr("type","hidden");
        inputUser.attr("name","user_oauth_approval");
        inputUser.attr("value","true");
        for(var i = 0;i&lt;scopeList.length;i++){
            var input = $("&lt;input&gt;");
            input.attr("type","hidden");
            input.attr("name",""+scopeList[i]+"")
            input.attr("value","true");
            form.append(input);
        }
        form.append(inputUser);
        form.submit();//表单提交
    }

具体可以参考我的github相关spring4all中相关代码
细心的人会发现这个跳转是由于前段点击事件造成的,如果是记住我功能的话,不会经过前端的事件触发后自动去登录,然后到这个授权地方来,这样的话我们是无法实现/oauth/authorize认证流程的。参考源码如下:

 //..........省略相关代码
@FrameworkEndpoint
@SessionAttributes({"authorizationRequest"})
public class AuthorizationEndpoint extends AbstractEndpoint {
   //..........省略相关代码
    @RequestMapping({"/oauth/authorize"})
    public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) {
        AuthorizationRequest authorizationRequest = this.getOAuth2RequestFactory().createAuthorizationRequest(parameters);
        Set<String> responseTypes = authorizationRequest.getResponseTypes();
        if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
            throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
        } else if (authorizationRequest.getClientId() == null) {
            throw new InvalidClientException("A client id must be provided");
        } else {
            try {
                if (principal instanceof Authentication && ((Authentication)principal).isAuthenticated()) {
                    ClientDetails client = this.getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
                    String redirectUriParameter = (String)authorizationRequest.getRequestParameters().get("redirect_uri");
                    String resolvedRedirect = this.redirectResolver.resolveRedirect(redirectUriParameter, client);
                    if (!StringUtils.hasText(resolvedRedirect)) {
                        throw new RedirectMismatchException("A redirectUri must be either supplied or preconfigured in the ClientDetails");
                    } else {
                        authorizationRequest.setRedirectUri(resolvedRedirect);
                        this.oauth2RequestValidator.validateScope(authorizationRequest, client);
                        authorizationRequest = this.userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication)principal);
                        boolean approved = this.userApprovalHandler.isApproved(authorizationRequest, (Authentication)principal);
                        authorizationRequest.setApproved(approved);
                        if (authorizationRequest.isApproved()) {
                            if (responseTypes.contains("token")) {
                                return this.getImplicitGrantResponse(authorizationRequest);
                            }

                            if (responseTypes.contains("code")) {
                                return new ModelAndView(this.getAuthorizationCodeResponse(authorizationRequest, (Authentication)principal));
                            }
                        }

                        model.put("authorizationRequest", authorizationRequest);
                        return this.getUserApprovalPageResponse(model, authorizationRequest, (Authentication)principal);
                    }
                } else {
                    throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorization can be completed.");
                }
            } catch (RuntimeException var11) {
                sessionStatus.setComplete();
                throw var11;
            }
        }
    }

    @RequestMapping(
        value = {"/oauth/authorize"},
        method = {RequestMethod.POST},
        params = {"user_oauth_approval"}
    )
    public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model, SessionStatus sessionStatus, Principal principal) {
        if (!(principal instanceof Authentication)) {
            sessionStatus.setComplete();
            throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorizing an access token.");
        } else {
            AuthorizationRequest authorizationRequest = (AuthorizationRequest)model.get("authorizationRequest");
            if (authorizationRequest == null) {
                sessionStatus.setComplete();
                throw new InvalidRequestException("Cannot approve uninitialized authorization request.");
            } else {
                RedirectView var8;
                try {
                    Set<String> responseTypes = authorizationRequest.getResponseTypes();
                    authorizationRequest.setApprovalParameters(approvalParameters);
                    authorizationRequest = this.userApprovalHandler.updateAfterApproval(authorizationRequest, (Authentication)principal);
                    boolean approved = this.userApprovalHandler.isApproved(authorizationRequest, (Authentication)principal);
                    authorizationRequest.setApproved(approved);
                    if (authorizationRequest.getRedirectUri() == null) {
                        sessionStatus.setComplete();
                        throw new InvalidRequestException("Cannot approve request when no redirect URI is provided.");
                    }

                    if (authorizationRequest.isApproved()) {
                        View var12;
                        if (responseTypes.contains("token")) {
                            var12 = this.getImplicitGrantResponse(authorizationRequest).getView();
                            return var12;
                        }

                        var12 = this.getAuthorizationCodeResponse(authorizationRequest, (Authentication)principal);
                        return var12;
                    }

                    var8 = new RedirectView(this.getUnsuccessfulRedirect(authorizationRequest, new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")), false, true, false);
                } finally {
                    sessionStatus.setComplete();
                }

                return var8;
            }
        }
    }

    private ModelAndView getUserApprovalPageResponse(Map<String, Object> model, AuthorizationRequest authorizationRequest, Authentication principal) {
        this.logger.debug("Loading user approval page: " + this.userApprovalPage);
        model.putAll(this.userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal));
        return new ModelAndView(this.userApprovalPage, model);
    }

    private ModelAndView getImplicitGrantResponse(AuthorizationRequest authorizationRequest) {
        try {
            TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(authorizationRequest, "implicit");
            OAuth2Request storedOAuth2Request = this.getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
            OAuth2AccessToken accessToken = this.getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
            if (accessToken == null) {
                throw new UnsupportedResponseTypeException("Unsupported response type: token");
            } else {
                return new ModelAndView(new RedirectView(this.appendAccessToken(authorizationRequest, accessToken), false, true, false));
            }
        } catch (OAuth2Exception var5) {
            return new ModelAndView(new RedirectView(this.getUnsuccessfulRedirect(authorizationRequest, var5, true), false, true, false));
        }
    }

 //..........省略相关代码

    private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser) {
        try {
            return new RedirectView(this.getSuccessfulRedirect(authorizationRequest, this.generateCode(authorizationRequest, authUser)), false, true, false);
        } catch (OAuth2Exception var4) {
            return new RedirectView(this.getUnsuccessfulRedirect(authorizationRequest, var4, false), false, true, false);
        }
    }
 //..........省略相关代码
    private ModelAndView handleException(Exception e, ServletWebRequest webRequest) throws Exception {
        ResponseEntity<OAuth2Exception> translate = this.getExceptionTranslator().translate(e);
        webRequest.getResponse().setStatus(translate.getStatusCode().value());
        if (!(e instanceof ClientAuthenticationException) && !(e instanceof RedirectMismatchException)) {
            AuthorizationRequest authorizationRequest = null;

            try {
                authorizationRequest = this.getAuthorizationRequestForError(webRequest);
                String requestedRedirectParam = (String)authorizationRequest.getRequestParameters().get("redirect_uri");
                String requestedRedirect = this.redirectResolver.resolveRedirect(requestedRedirectParam, this.getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId()));
                authorizationRequest.setRedirectUri(requestedRedirect);
                String redirect = this.getUnsuccessfulRedirect(authorizationRequest, (OAuth2Exception)translate.getBody(), authorizationRequest.getResponseTypes().contains("token"));
                return new ModelAndView(new RedirectView(redirect, false, true, false));
            } catch (OAuth2Exception var8) {
                return new ModelAndView(this.errorPage, Collections.singletonMap("error", translate.getBody()));
            }
        } else {
            return new ModelAndView(this.errorPage, Collections.singletonMap("error", translate.getBody()));
        }
    }
  //..........省略相关代码
}

源码内部实现了ModelAndView进行跳转,其实可以进行设置让他显示出来,根据这个思想我们自己也可以实现一个View进行跳转

流程处理

思想:无论是正常登陆流程还是记住我功能流程,直接跳转到一个页面将scopeuser_oauth_approval等数据填充,然后自动提交。
首先前台得使用表单提交否则不能生效,这个地方让我卡住了很久,如果是ajax的话会在error函数中返回整个页面的代码,因为在自定义授权页返回的地方是返回的视图。
来看一下具体的代码吧

@RequestMapping({ "/oauth/my_approval"})
    public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
        @SuppressWarnings("unchecked")
        Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes"));
        List<String> list = new ArrayList<>();
        for (String scope : scopes.keySet()) {
            list.add(scope);
        }
        model.put("scopes",list);
        Cookie[] cookies = request.getCookies();
        boolean bool =  Arrays.stream(cookies).anyMatch(x->x.getName().equals("remember-me-cookie-name"));
        Principal principal = request.getUserPrincipal();
        String usernmae = principal.getName();
        model.put("username",usernmae);
        String check = bool==true ? "true" : "false";
        model.put("remember",check);

        return "approval";
    }

看一下我的具体的approval.html视图

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8"/>
</head>
<body>
<form th:action="@{../oauth/authorize}" th:method="post">
    <div th:each="scope:${scopes}">
        <input th:type="hidden" th:name="${scope}" th:value="true"/>
    </div>
    <input th:type="hidden" th:name="user_oauth_approval" th:value="true"/>
</form>
<script src="../../js/jquery.min.js"></script>
<script src="../../js/jquery.cookie.js"></script>
<script th:inline="javascript">
    window.onload=function(){
        var username = [[${username}]];
        var remember = [[${remember}]];
        if(remember == "true"){
            $.cookie('phone', username, { expires:7,path: '/' });
        }else{
            $.cookie('phone', username, { path: '/' });
        }
        document.forms[0].submit();
    }
</script>
</body>
</html>

注意配置文件的更改,因为thymeleaf语法的限制,具体的原因大请大家参考我的另一篇文章 Spring Security模板引擎之异常信息
Spring Security Oauth2系列(五)到了这个地方可能很多人以为就结束了,其实还没有,当你注销的时候你会发现浏览器的cookie还有那个remember-me-cookie-name,当你再次登录的时候它又会执行记住我功能的流程导致登录错误

解决注销登录

最开始很当然的想到的是清除cookie,这个地方我尝试了很久都没有成功,还被我们群里的一个只说不练的大神嘲笑(java是一门学问,简单的东西遇见了不同的情况,未必可以简单的对待),我希望有大神看见这篇文章过后能找到清除sping security oauth2的记住我功能的cookie,最后谈一下我的解决方案,和以前处理token的思想类似,既然浏览器的cookie中保存了用户的一些信息,这些数据还要和数据库的存储的数据进行校验,既然cookie处理不了,那就转移中心去处理数据库的吧
看一下注销登录的代码

//....................
@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) {
           //消除token
        String access_token = JdbcOperateUtils.query(principal);
        if (!access_token.equals("gzw")) {
            if (consumerTokenServices.revokeToken(access_token)) {
                logger.info("oauth2 logout success with principal: "+ principal);
                 //消除cookie的校验数据
                JdbcOperateUtils.exit(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);
    }
}

消除cookie的校验数据代码如下:

 /** * 查询用户是否是用记住我登录 * @param username * @return */
    public static void exit(String username) {
        Connection connection = ConnectionUtils.getConn();
        String sql1 = "SELECT series FROM persistent_logins WHERE username = ? limit 1";
        String sql2 = "DELETE FROM persistent_logins WHERE username = ? ";
        PreparedStatement preparedStatement1 = null;
        PreparedStatement preparedStatement2 = null;
        try {
            preparedStatement1 = connection.prepareStatement(sql1);
            preparedStatement1.setString(1, username);
            ResultSet resultSet = preparedStatement1.executeQuery();
            if (resultSet.next()){
                preparedStatement2 = connection.prepareStatement(sql2);
                preparedStatement2.setString(1, username);
                preparedStatement2.execute();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            ConnectionUtils.releaseConnection(connection);
        }
    }

最后附上建表语句,创建persistent_logins
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);

总结:

希望这篇文章对大家在日常工作中能够有所帮助,我也会在平时工作中把遇见的相关问题继续更新到文章中。后续我会将代码同步到github上供大家一起学习,希望大家给出中肯的意见。

参考本人github地址:https://github.com/dqqzj/spring4all/tree/master/oauth2


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