IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 自定义实现OAuth2.0 授权码模式 -> 正文阅读

[网络协议]自定义实现OAuth2.0 授权码模式

OAuth2.0 授权码模式 实践

本篇文章不适合作为授权码模式的入门文章来阅读,适合想要自己实现授权码模式或体验授权码模式的开发者阅读

依赖知识

术语

  • Resource Owner-用户
  • Resource Server-负责处理对用户资源的请求
  • Authorization Server-获取用户的授权
  • Scope-对用户资源可操作的范围
  • Client-第三方应用

授权码流程

  1. Resource Server提供Resource,Resource Owner具备对这些Resource的访问权限。

    注意 :请不要简单的把Resource想象成用户的信息、头像。因为在RESTful架构中,每一个URI代表一种Resource,理解这一点至关重要

  2. 当第三方应用Client想要访问Resource Owner的Resource时,需要经过Authorization Server的许可,才可以访问Resource Server上有关Resource Owner的Resource
    在这里插入图片描述

详细流程如下:

  • 当Client想要访问Resource Owner 的Resource时,请求Authorization Server。response_type为code,代表为授权码流程,redirect_uri为用户授权后回调Client的url。

    Authorization Server接收到请求后,返回授权范围Scope,并拉起用户授权页面,请求Resource Owner授权

    https://authorization-server.com/auth
     ?response_type=code
     &client_id=29352915982374239857
     &redirect_uri=https://thirdparty-server/callback
     &state=xcoiv98y2kd22vusuye3kch
    
  • Resource Owner允许授权,并将授权范围Scope一并提交请求到Authorization Server。Authorization Server生成授权码,并设置过期时间,并存储在Authorization Server上。同时回调上一步请求中的redirect_uri对应的地址

    https://thirdparty-server/callback
     ?code=g0ZGZmNjVmOWIjNTk2NTk4ZTYyZGI3
     &state=xcoiv98y2kd22vusuye3kch
    
  • 第三方应用Client收到请求,比较state是否一致(防止CSRF攻击)。如果一致,则再次向Authorization Server发起请求,通过code换取access_token。

    grant_type=authorization_code
    code=g0ZGZmNjVmOWIjNTk2NTk4ZTYyZGI3
    client_id=client_id
    client_secret=client_secret
    

认证服务器

主要职责是请求Resource Owner授权和下发token

拉起请求用户授权页面

    @GET
    @DenyAll
    public Response applyForUserAuthorization(@Context HttpServletRequest request,
                                      @Context HttpServletResponse response,
                                      @Context UriInfo uriInfo) throws ServletException, IOException {
        MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
        String state = params.getFirst("state");
        // TODO: 实际业务中使用其他方式存储
        if (state.length() > 0) {
            stateMap.put(1, state);
        }
        //1. client_id
        String clientId = params.getFirst("client_id");
        if (clientId == null || clientId.isEmpty()) {
            return informUserAboutError(request, response, "Invalid client_id :" + clientId);
        }
        // 判断clientId是否在认证服务器中
        Client client = appDataRepository.getClient(clientId);
        if (client == null) {
            return informUserAboutError(request, response, "Invalid client_id :" + clientId);
        }
        //2. Client Authorized Grant Type
        if (client.getAuthorizedGrantTypes() != null && !client.getAuthorizedGrantTypes().contains("authorization_code")) {
            return informUserAboutError(request, response, "Authorization Grant type, authorization_code, is not allowed for this client :" + clientId);
        }
        //3. redirectUri
        String redirectUri = params.getFirst("redirect_uri");
        if (client.getRedirectUrl() != null && !client.getRedirectUrl().isEmpty()) {
            if (redirectUri != null && redirectUri.isEmpty() && !client.getRedirectUrl().equals(redirectUri)) {
                //sould be in the client.redirectUri
                return informUserAboutError(request, response, "redirect_uri is pre-registred and should match");
            }
            redirectUri = client.getRedirectUrl();
//            params.putSingle("resolved_redirect_uri", redirectUri);
        } else {
            if (redirectUri == null || redirectUri.isEmpty()) {
                return informUserAboutError(request, response, "redirect_uri is not pre-registred and should be provided");
            }
            params.putSingle("resolved_redirect_uri", redirectUri);
        }
        request.setAttribute("client", client);

        //4. response_type
        String responseType = params.getFirst("response_type");
        if (!"code".equals(responseType) && !"token".equals(responseType)) {
            return informUserAboutError(request, response, "invalid_grant :" + responseType + ", response_type params should be code or token:");
        }
        //Save params in session
        request.getSession().setAttribute("ORIGINAL_PARAMS", params);

        //4.scope: Optional
        String requestedScope = request.getParameter("scope");
        if (requestedScope == null || requestedScope.isEmpty()) {
            requestedScope = client.getScopes();
        }
        //5. user principal, common userId
        Principal principal = securityContext.getUserPrincipal();
        User user = appDataRepository.getUser(principal.getName());
        String allowedScopes = checkUserScopes(user.getScopes(), requestedScope);
        request.setAttribute("scopes", allowedScopes);
        // 转发至授权页面
        request.getRequestDispatcher("/authorize.jsp").forward(request, response);
        return null;
    }

用户手动授权

在这里插入图片描述

提交授权、生成code

    @DenyAll
    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void userAuthorization(@Context HttpServletRequest request,
                                      @Context HttpServletResponse response,
                                      MultivaluedMap<String, String> params) throws ServletException, IOException {
        MultivaluedMap<String, String> originalParams = (MultivaluedMap<String, String>) request.getSession().getAttribute("ORIGINAL_PARAMS");
        if (originalParams == null) {
             informUserAboutError(request, response, "No pending authorization request.");
        }
//        String redirectUri = originalParams.getFirst("resolved_redirect_uri");
        String redirectUri = "http://localhost:8080/thirdparty-server/third/apply/callback";
        StringBuilder sb = new StringBuilder(redirectUri);
        sb.append("?state=").append(stateMap.get(1));
        String approvalStatus = params.getFirst("approval_status");
        if ("NO".equals(approvalStatus)) {
            URI location = UriBuilder.fromUri(sb.toString())
                    .queryParam("error", "User doesn't approved the request.")
                    .queryParam("error_description", "User doesn't approved the request.")
                    .build();
             Response.seeOther(location).build();
        }

        //==> YES
        List<String> approvedScopes = params.get("scope");
        if (approvedScopes == null || approvedScopes.isEmpty()) {
            URI location = UriBuilder.fromUri(sb.toString())
                    .queryParam("error", "User doesn't approved the request.")
                    .queryParam("error_description", "User doesn't approved the request.")
                    .build();
             Response.seeOther(location).build();
        }
        String responseType = originalParams.getFirst("response_type");
        String clientId = originalParams.getFirst("client_id");
        if ("code".equals(responseType)) {
            String userId = securityContext.getUserPrincipal().getName();
            AuthorizationCode authorizationCode = new AuthorizationCode();
            authorizationCode.setCode(RandomString.make(15));
            authorizationCode.setClientId(clientId);
            authorizationCode.setUserId(userId);
            authorizationCode.setApprovedScopes(String.join(" ", approvedScopes));
            authorizationCode.setExpirationDate(LocalDateTime.now().plusMinutes(10));
            authorizationCode.setRedirectUrl(redirectUri);
            appDataRepository.save(authorizationCode);
            String code = authorizationCode.getCode();
            sb.append("&code=").append(code);
        }
        // 回调第三方应用
        response.sendRedirect(sb.toString());
    }

下发Token

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.APPLICATION_JSON)
    @DenyAll
    public Response token(@HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader,
                          MultivaluedMap<String, String> params) {
        //Check grant_type params
        String grantType = params.getFirst("grant_type");
        if (grantType == null || grantType.isEmpty()) {
            return responseError("Invalid_request", "grant_type is required", Response.Status.BAD_REQUEST);
        }
        if (!supportedGrantTypes.contains(grantType)) {
            return responseError("unsupported_grant_type", "grant_type should be one of :" + supportedGrantTypes, Response.Status.BAD_REQUEST);
        }

        //Client Authentication
        String[] clientCredentials = extract(authHeader);
        if (clientCredentials.length != 2) {
            return responseError("Invalid_request", "Bad Credentials client_id/client_secret", Response.Status.BAD_REQUEST);
        }
        String clientId = clientCredentials[0];
        Client client = appDataRepository.getClient(clientId);
        if (client == null) {
            return responseError("Invalid_request", "Invalid client_id", Response.Status.BAD_REQUEST);
        }
        String clientSecret = clientCredentials[1];
        if (!clientSecret.equals(client.getClientSecret())) {
            return responseError("Invalid_request", "Invalid client_secret", Response.Status.UNAUTHORIZED);
        }
        AuthorizationGrantTypeHandler authorizationGrantTypeHandler = authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();
        TokenVO tokenResponse = null;
        try {
            tokenResponse = authorizationGrantTypeHandler.createAccessToken(clientId, params);
        }catch (Exception ex) {
            log.log(Level.WARNING, "acquire token failed", ex);
        }
        return Response.ok(tokenResponse)
                .header("Cache-Control", "no-store")
                .header("Pragma", "no-cache")
                .build();
    }

这里只给出生成access_token的简单代码

@Named("authorization_code")
public class AuthorizationCodeGrantTypeHandler extends AbstractGrantTypeHandler{

    private EntityManager entityManager = JPAUtil.acquireEntityManager();

    @Inject
    private AppDataRepository appDataRepository;

    @Override
    public TokenVO createAccessToken(String clientId, MultivaluedMap<String, String> params) throws Exception {
        //1. code is required
        String code = params.getFirst("code");
        if (code == null || "".equals(code)) {
            throw new WebApplicationException("invalid_grant");
        }
        AuthorizationCode authorizationCode = entityManager.find(AuthorizationCode.class, code);
        if (!authorizationCode.getExpirationDate().isAfter(LocalDateTime.now())) {
            throw new WebApplicationException("code Expired !");
        }
        String redirectUri = params.getFirst("redirect_uri");
        //redirecturi match
        if (authorizationCode.getRedirectUrl() != null && !authorizationCode.getRedirectUrl().equals(redirectUri)) {
            //redirectUri params should be the same as the requested redirectUri.
            throw new WebApplicationException("invalid_grant");
        }
        //client match
        if (!clientId.equals(authorizationCode.getClientId())) {
            throw new WebApplicationException("invalid_grant");
        }
        String accessToken = generateAccessToken(clientId, authorizationCode.getUserId(), authorizationCode.getApprovedScopes());
        String refreshToken = generateRefreshToken(clientId, authorizationCode.getUserId(), authorizationCode.getApprovedScopes());
        TokenVO result = new TokenVO();
        result.setAccess_token(accessToken);
        result.setExpires_in(expiresInMilliseconds);
        result.setScope(authorizationCode.getApprovedScopes());
        result.setRefresh_token(refreshToken);
        return result;
    }
}

第三方应用

收到code并请求Token

    @GET
    @Path("callback")
    @Produces(MediaType.APPLICATION_JSON)
    @SneakyThrows
    public Response callback(@Context HttpServletRequest request,
                             @Context HttpServletResponse response) {
    
        String clientId = "webappclient";
        String clientSecret = "webappclientsecret";
    
        //Error:
        String error = request.getParameter("error");
        if (error != null) {
            request.setAttribute("error", error);
            return Response.status(Status.INTERNAL_SERVER_ERROR).entity("获取access_token失败").build();
        }
        String localState = (String) request.getSession().getAttribute("CLIENT_LOCAL_STATE");
        if (!localState.equals(request.getParameter("state"))) {
            request.setAttribute("error", "The state attribute doesn't match !!");
            return Response.status(Status.INTERNAL_SERVER_ERROR).entity("校验state失败").build();
        }
    
        String code = request.getParameter("code");
        // 内部直接调用authorization-server获取token
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target("http://localhost:8080/authorization-server/auth/token");
    
        Form form = new Form();
        form.param("grant_type", "authorization_code");
        form.param("code", code);
        form.param("redirect_uri", redirectUri);
        TokenVO tokenResponse = target.request(MediaType.APPLICATION_JSON_TYPE)
            .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue(clientId, clientSecret))
            .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), TokenVO.class);

        request.setAttribute("token", tokenResponse);
        // 将获取到的token显示在页面上
        request.getRequestDispatcher("/success.jsp").forward(request, response);
        return null;
    
    }

访问受保护的资源

利用刚才拿到的token,访问 Resource Server。Resource Server会根据token中的scope进行权限的校验,一般使用一个全局Filter来实现。通过对JWT的解析来判断该请求是否拥有对Resource访问的权限。

@Log
@Provider
public class SecurityFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;


//    @Context
//    private HttpServletRequest httpServletRequest;

    @Override
    @SneakyThrows
    public void filter(ContainerRequestContext containerRequestContext) throws IOException {
        Method method = resourceInfo.getResourceMethod();
        //TODO: Question 还没找到解决方案,使用httpServletRequest的话,表单提交那里获取不到参数
//        String userId = httpServletRequest.getParameter("userId");
        String userId = "appuser";
        // 构造SecurityContext, 以便AuthorizationResource使用
        if (userId != null && userId.length() > 0) {
            containerRequestContext.setSecurityContext(new SecurityContext() {
                @Override
                public Principal getUserPrincipal() {
                    return () -> userId;
                }

                @Override
                public boolean isUserInRole(String role) {
                    return false;
                }

                @Override
                public boolean isSecure() {
                    return false;
                }

                @Override
                public String getAuthenticationScheme() {
                    return "CLIENT_CERT";
                }
            });
            return;
        }
         // no need to check permissions
        if (method.isAnnotationPresent(DenyAll.class)) {
            log.info("no need to check permission");
            return;
        }
        // 特殊情况都处理完毕之后,开始正常处理token的解析和权限的校验
        verifyTokenAndPermission(containerRequestContext, method);
    }

    @SneakyThrows
    private void verifyTokenAndPermission(final ContainerRequestContext containerRequestContext, final Method method) {
        MultivaluedMap<String, String> headers = containerRequestContext.getHeaders();
        List<String> authorization = headers.get("Authorization");
        String token = authorization.get(0).substring("Bearer".length()).trim();
        // verify token
        JWSVerifier verifier = generateRsaJwsVerifier();
        SignedJWT jwt = SignedJWT.parse(token);
        if (!jwt.verify(verifier)) {
            containerRequestContext.abortWith(buildResponse(Response.Status.FORBIDDEN));
        }
        Map<String, Object> claims = jwt.getJWTClaimsSet().getClaims();
        String userId = jwt.getJWTClaimsSet().getSubject();
        String scopes = (String) claims.get("scope");
        log.info("scopes is:\n" + scopes);
        List<String> parsedScopes = Arrays.asList(scopes.split("\\s+"));


        // verify permission
        if (method.isAnnotationPresent(RolesAllowed.class)) {
            RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
            String[] roles = rolesAnnotation.value();
            if (!parsedScopes.containsAll(Arrays.asList(roles))) {
                containerRequestContext.abortWith(buildResponse(Response.Status.FORBIDDEN));
            }
            containerRequestContext.setSecurityContext(new SecurityContext() {
                @Override
                public Principal getUserPrincipal() {
                    return () -> userId;
                }

                @Override
                public boolean isUserInRole(String role) {
                    return parsedScopes.contains(role);
                }

                @Override
                public boolean isSecure() {
                    return true;
                }

                @Override
                public String getAuthenticationScheme() {
                    return "CLIENT_CERT";
                }
            });
        }
    }

    private JWSVerifier generateRsaJwsVerifier() throws Exception{
        String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString("rsa/publish-key.pem");
        RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);
        return new RSASSAVerifier(rsaKey);
    }

    private Response buildResponse(Response.Status status) {
        return Response
                .status(status)
                .entity("{\"errmsg\": \"\"}")
                .type(MediaType.APPLICATION_JSON_TYPE)
                .build();
    }
}

真正访问Resource上的Resource

@Path("user/protect")
public class UserResource {

    @GET
    @Path("read")
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed("resource.read")
    public String readProtectedInfo() {
        return "Read Success";
    }

    @POST
    @Path("write")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed("resource.write")
    public String writeProtectedInfo(@FormParam("writeInfo") String writeInfo) {
        return "Write Success \n" + writeInfo;
    }
}

项目结构

在这里插入图片描述

项目部署

先在authorization-common下执行

mvn install

再使用Tomcat9进行部署
在这里插入图片描述

项目完整代码

仓库地址

相关文章

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-12-16 18:03:49  更:2021-12-16 18:05:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年10日历 -2024/10/5 7:37:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码